Skip to content

Commit 4ee4c77

Browse files
authored
Merge branch 'main' into saumya/streaming-fetchone
2 parents 960edef + a3dd3b8 commit 4ee4c77

16 files changed

+1596
-535
lines changed

.coveragerc

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
[run]
22
omit =
3-
mssql_python/testing_ddbc_bindings.py
3+
main.py
4+
setup.py
5+
bcp_options.py
46
tests/*
57

68
[report]

.github/workflows/pr-format-check.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ jobs:
5757
// Extract the summary content
5858
const summaryContent = summaryMatch[1];
5959
60-
// Remove all HTML comments including the template placeholder
60+
// Remove all HTML comments including unclosed ones (template placeholders)
6161
const contentWithoutComments =
62-
summaryContent.replace(/<!--[\s\S]*?-->/g, '');
62+
summaryContent.replace(/<!--[\s\S]*?(?:-->|$)/g, '');
6363
6464
// Remove whitespace and check if there's actual text content
6565
const trimmedContent = contentWithoutComments.trim();

.gitignore

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# Ignore all files in the pybind/build directory
2-
mssql_python/pybind/build/
3-
41
# Ignore pycache files and folders
52
__pycache__/
63
**/__pycache__/
@@ -23,6 +20,7 @@ test-*.xml
2320

2421
# Ignore the build & mssql_python.egg-info directories
2522
build/
23+
**/build/
2624
mssql_python.egg-info/
2725

2826
# Python bytecode
@@ -46,4 +44,19 @@ build/
4644
*.swp
4745

4846
# .DS_Store files
49-
.DS_Store
47+
.DS_Store
48+
49+
# wheel files
50+
*.whl
51+
*.tar.gz
52+
*.zip
53+
54+
# Dockerfiles and images (root only)
55+
/Dockerfile*
56+
/docker-compose.yml
57+
/docker-compose.override.yml
58+
/docker-compose.*.yml
59+
60+
# Virtual environments
61+
*venv*/
62+
**/*venv*/

PyPI_Description.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
11
# mssql-python
22

3-
This is a new Python driver for Microsoft SQL Server currently in Alpha phase.
3+
This is a new Python driver for Microsoft SQL Server currently in Public Preview phase.
44

55
## Public Preview Release
66

7-
We are making progress - The Public Preview of our driver is now available! This marks a significant milestone in our development journey. While we saw a few early adopters of our alpha release, we are introducing the following functionalities to support your applications in a more robust and reliable manner.
7+
We are making progress - The Public Preview of our driver is now available! This marks a significant milestone in our development journey. While we saw a few early adopters of our public preview release, we are introducing the following functionalities to support your applications in a more robust and reliable manner.
88

99
### What's Included:
1010

1111
- Everything from previous releases
12-
- **Alpine Linux Support:** Added full support for Alpine Linux distribution (musllinux) with specialized driver handling and fixes for musl libc compatibility.
13-
- **Connection Management Improvements:** Fixed autocommit to be False by default and added automatic rollback on connection close for better transaction control.
14-
- **PyODBC Compatibility:** Enhanced type objects and constructor compatibility with pyodbc for seamless migration and interoperability.
12+
- **SUSE Linux Support:** Added full support for SUSE and openSUSE distributions alongside existing other Linux distros support, broadening enterprise Linux compatibility.
13+
- **Context Manager Support:** Implemented Python `with` statement support for Connection and Cursor classes with automatic transaction management and resource cleanup.
14+
- **Large Text Streaming:** Added Data At Execution (DAE) support for streaming large text parameters (`NVARCHAR(MAX)`, `VARCHAR(MAX)`), eliminating memory constraints for bulk text `execute()` operations.
15+
- `VARBINARY(MAX)` support to follow alongwith streaming support for fetch operations.
16+
- **Enhanced Unicode Handling:** Improved emoji and international character support with robust UTF-16 encoding for reliable multilingual data processing.
17+
- **PyODBC Compatibility:** Enhanced API compatibility with pyodbc including:
18+
- DB-API 2.0 exception classes: `Warning`, `Error`, `InterfaceError`, `DatabaseError`, `DataError`, `OperationalError`, `IntegrityError`, `InternalError`, `ProgrammingError`, `NotSupportedError`
19+
- Context manager support with `with` statements for Connection and Cursor
20+
- Encoding configuration APIs: `setencoding()`, `getencoding()`, `setdecoding()`, `getdecoding()`
21+
- Cursor navigation APIs: `next()`, `__iter__()`, `scroll()`, `skip()`, `fetchval()`
22+
- Cursor attributes: `rownumber`, `messages`
23+
- Additional methods: `cursor.commit()`, `cursor.rollback()`, `table()`
1524

1625
For more information, please visit the project link on Github: https://github.com/microsoft/mssql-python
1726

27+
If you have any feedback, questions or need support please mail us at [email protected].
28+
1829
### What's Next:
1930

2031
As we continue to develop and refine the driver, you can expect regular updates that will introduce new features, optimizations, and bug fixes. We encourage you to contribute, provide feedback and report any issues you encounter, as this will help us improve the driver for the final release.

eng/pipelines/build-whl-pipeline.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,9 @@ jobs:
258258
# Install CMake on macOS
259259
- script: |
260260
brew update
261+
# Uninstall existing CMake to avoid tap conflicts
262+
brew uninstall cmake --ignore-dependencies || echo "CMake not installed or already removed"
263+
# Install CMake from homebrew/core
261264
brew install cmake
262265
displayName: 'Install CMake'
263266
@@ -337,7 +340,7 @@ jobs:
337340
python -m pytest -v
338341
displayName: 'Run Pytest to validate bindings'
339342
env:
340-
DB_CONNECTION_STRING: 'Driver=ODBC Driver 18 for SQL Server;Server=localhost;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'
343+
DB_CONNECTION_STRING: 'Driver=ODBC Driver 18 for SQL Server;Server=tcp:127.0.0.1,1433;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'
341344
342345
# Build wheel package for universal2
343346
- script: |

eng/pipelines/pr-validation-pipeline.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ jobs:
152152
python -m pytest -v --junitxml=test-results.xml --cov=. --cov-report=xml --capture=tee-sys --cache-clear
153153
displayName: 'Run pytest with coverage'
154154
env:
155-
DB_CONNECTION_STRING: 'Driver=ODBC Driver 18 for SQL Server;Server=localhost;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'
155+
DB_CONNECTION_STRING: 'Driver=ODBC Driver 18 for SQL Server;Server=tcp:127.0.0.1,1433;Database=master;Uid=SA;Pwd=$(DB_PASSWORD);TrustServerCertificate=yes'
156156
DB_PASSWORD: $(DB_PASSWORD)
157157
158158
- task: PublishTestResults@2
-21.7 MB
Binary file not shown.

mssql_python/cursor.py

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -380,33 +380,21 @@ def _map_sql_type(self, param, parameters_list, i):
380380
)
381381

382382
if isinstance(param, bytes):
383-
if len(param) > 8000: # Assuming VARBINARY(MAX) for long byte arrays
384-
return (
385-
ddbc_sql_const.SQL_VARBINARY.value,
386-
ddbc_sql_const.SQL_C_BINARY.value,
387-
len(param),
388-
0,
389-
False,
390-
)
383+
# Use VARBINARY for Python bytes/bytearray since they are variable-length by nature.
384+
# This avoids storage waste from BINARY's zero-padding and matches Python's semantics.
391385
return (
392-
ddbc_sql_const.SQL_BINARY.value,
386+
ddbc_sql_const.SQL_VARBINARY.value,
393387
ddbc_sql_const.SQL_C_BINARY.value,
394388
len(param),
395389
0,
396390
False,
397391
)
398392

399393
if isinstance(param, bytearray):
400-
if len(param) > 8000: # Assuming VARBINARY(MAX) for long byte arrays
401-
return (
402-
ddbc_sql_const.SQL_VARBINARY.value,
403-
ddbc_sql_const.SQL_C_BINARY.value,
404-
len(param),
405-
0,
406-
True,
407-
)
394+
# Use VARBINARY for Python bytes/bytearray since they are variable-length by nature.
395+
# This avoids storage waste from BINARY's zero-padding and matches Python's semantics.
408396
return (
409-
ddbc_sql_const.SQL_BINARY.value,
397+
ddbc_sql_const.SQL_VARBINARY.value,
410398
ddbc_sql_const.SQL_C_BINARY.value,
411399
len(param),
412400
0,
@@ -471,17 +459,16 @@ def _reset_cursor(self) -> None:
471459

472460
def close(self) -> None:
473461
"""
474-
Close the cursor now (rather than whenever __del__ is called).
462+
Close the connection now (rather than whenever .__del__() is called).
463+
Idempotent: subsequent calls have no effect and will be no-ops.
475464
476465
The cursor will be unusable from this point forward; an InterfaceError
477-
will be raised if any operation is attempted with the cursor.
478-
479-
Note:
480-
Unlike the current behavior, this method can be called multiple times safely.
481-
Subsequent calls to close() on an already closed cursor will have no effect.
466+
will be raised if any operation (other than close) is attempted with the cursor.
467+
This is a deviation from pyodbc, which raises an exception if the cursor is already closed.
482468
"""
483469
if self.closed:
484-
return
470+
# Do nothing - not calling _check_closed() here since we want this to be idempotent
471+
return
485472

486473
# Clear messages per DBAPI
487474
self.messages = []
@@ -498,12 +485,12 @@ def _check_closed(self):
498485
Check if the cursor is closed and raise an exception if it is.
499486
500487
Raises:
501-
InterfaceError: If the cursor is closed.
488+
ProgrammingError: If the cursor is closed.
502489
"""
503490
if self.closed:
504-
raise InterfaceError(
505-
driver_error="Operation cannot be performed: the cursor is closed.",
506-
ddbc_error="Operation cannot be performed: the cursor is closed."
491+
raise ProgrammingError(
492+
driver_error="Operation cannot be performed: The cursor is closed.",
493+
ddbc_error=""
507494
)
508495

509496
def _create_parameter_types_list(self, parameter, param_info, parameters_list, i):
@@ -849,6 +836,8 @@ def _select_best_sample_value(column):
849836
return max(non_nulls, key=lambda s: len(str(s)))
850837
if all(isinstance(v, datetime.datetime) for v in non_nulls):
851838
return datetime.datetime.now()
839+
if all(isinstance(v, (bytes, bytearray)) for v in non_nulls):
840+
return max(non_nulls, key=lambda b: len(b))
852841
if all(isinstance(v, datetime.date) for v in non_nulls):
853842
return datetime.date.today()
854843
return non_nulls[0] # fallback
@@ -1185,13 +1174,19 @@ def __del__(self):
11851174
Destructor to ensure the cursor is closed when it is no longer needed.
11861175
This is a safety net to ensure resources are cleaned up
11871176
even if close() was not called explicitly.
1177+
If the cursor is already closed, it will not raise an exception during cleanup.
11881178
"""
1189-
if "_closed" not in self.__dict__ or not self._closed:
1179+
if "closed" not in self.__dict__ or not self.closed:
11901180
try:
11911181
self.close()
11921182
except Exception as e:
11931183
# Don't raise an exception in __del__, just log it
1194-
log('error', "Error during cursor cleanup in __del__: %s", e)
1184+
# If interpreter is shutting down, we might not have logging set up
1185+
import sys
1186+
if sys and sys._is_finalizing():
1187+
# Suppress logging during interpreter shutdown
1188+
return
1189+
log('debug', "Exception during cursor cleanup in __del__: %s", e)
11951190

11961191
def scroll(self, value: int, mode: str = 'relative') -> None:
11971192
"""
@@ -1430,4 +1425,4 @@ def tables(self, table=None, catalog=None, schema=None, tableType=None):
14301425
row = Row(row_data, self.description, column_map)
14311426
result_rows.append(row)
14321427

1433-
return result_rows
1428+
return result_rows

mssql_python/exceptions.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,14 @@ class Exception(Exception):
1717
def __init__(self, driver_error, ddbc_error) -> None:
1818
self.driver_error = driver_error
1919
self.ddbc_error = truncate_error_message(ddbc_error)
20-
self.message = (
21-
f"Driver Error: {self.driver_error}; DDBC Error: {self.ddbc_error}"
22-
)
20+
if self.ddbc_error:
21+
# Both driver and DDBC errors are present
22+
self.message = (
23+
f"Driver Error: {self.driver_error}; DDBC Error: {self.ddbc_error}"
24+
)
25+
else:
26+
# Errors raised by the driver itself should not have a DDBC error message
27+
self.message = f"Driver Error: {self.driver_error}"
2328
super().__init__(self.message)
2429

2530

mssql_python/msvcp140.dll

-562 KB
Binary file not shown.

0 commit comments

Comments
 (0)