Skip to content

Commit c340068

Browse files
committed
Merged PR 5238: Feat: Connection properties
#### AI description (iteration 1) #### PR Classification New feature #### PR Summary This pull request adds new connection properties to the `Connection` class and updates the `connect` function to support these properties. - `mssql_python/connection.py`: Added new parameters to the `Connection` class constructor and implemented methods to handle these parameters. - `mssql_python/db_connection.py`: Updated the `connect` function to accept and pass new connection parameters. - `mssql_python/constants.py`: Added new ODBC constants for connection attributes. - `README.md`: Removed outdated build status badges. Related work items: #33534
1 parent 4b89f3a commit c340068

File tree

7 files changed

+150
-37
lines changed

7 files changed

+150
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# MSSQL-Python
22

3-
This repository contains the source code for the MSSQL-Python project, which provides Python bindings for the Microsoft SQL Server ODBC driver using `pybind11`. The project includes a C++ extension module that wraps the ODBC API and exposes it to Python, allowing for efficient database interactions.
3+
This repository contains the source code for the MSSQL-Python project, which provides Python bindings for the Microsoft SQL Server driver using `pybind11`. The project includes a C++ extension module that wraps the DDBC API and exposes it to Python, allowing for efficient database interactions.
44

55
## Build Status (`main`)
66

mssql_python/connection.py

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,45 +26,81 @@ class Connection:
2626
close() -> None:
2727
"""
2828

29-
def __init__(self, connection_str: str, autocommit: bool = True) -> None:
29+
def __init__(self, connection_str: str, autocommit: bool = False, **kwargs) -> None:
3030
"""
31-
Initialize the connection object with the specified connection string.
31+
Initialize the connection object with the specified connection string and parameters.
3232
3333
Args:
34-
connection_str (str): The connection_str to connect to.
35-
34+
- connection_str (str): The connection string to connect to.
35+
- autocommit (bool): If True, causes a commit to be performed after each SQL statement.
36+
**kwargs: Additional key/value pairs for the connection string.
37+
Not including below properties since we are driver doesn't support this:
38+
3639
Returns:
3740
None
3841
3942
Raises:
40-
ValueError: If the connection_str is invalid or connection fails.
43+
ValueError: If the connection string is invalid or connection fails.
4144
4245
This method sets up the initial state for the connection object,
4346
preparing it for further operations such as connecting to the database, executing queries, etc.
4447
"""
4548
self.henv = ctypes.c_void_p()
4649
self.hdbc = ctypes.c_void_p()
47-
self.connection_str = add_driver_to_connection_str(connection_str)
50+
self.connection_str = self._construct_connection_string(connection_str, **kwargs)
4851
self._initializer()
4952
self._autocommit = autocommit
5053
self.setautocommit(autocommit)
5154

55+
def _construct_connection_string(self, connection_str: str, **kwargs) -> str:
56+
"""
57+
Construct the connection string by concatenating the connection string with key/value pairs from kwargs.
58+
59+
Args:
60+
connection_str (str): The base connection string.
61+
**kwargs: Additional key/value pairs for the connection string.
62+
63+
Returns:
64+
str: The constructed connection string.
65+
"""
66+
# Add the driver attribute to the connection string
67+
conn_str = add_driver_to_connection_str(connection_str)
68+
# Add additional key-value pairs to the connection string
69+
for key, value in kwargs.items():
70+
if key.lower() == "host":
71+
key = "Server"
72+
elif key.lower() == "user":
73+
key = "Uid"
74+
elif key.lower() == "password":
75+
key = "Pwd"
76+
elif key.lower() == "database":
77+
key = "Database"
78+
elif key.lower() == "encrypt":
79+
key = "Encrypt"
80+
elif key.lower() == "trust_server_certificate":
81+
key = "TrustServerCertificate"
82+
else:
83+
continue
84+
conn_str += f"{key}={value};"
85+
return conn_str
86+
5287
def _initializer(self) -> None:
5388
"""
54-
Initialize the ODBC environment and connection handles.
89+
Initialize the environment and connection handles.
5590
56-
This method is responsible for setting up the ODBC environment and connection
91+
This method is responsible for setting up the environment and connection
5792
handles, allocating memory for them, and setting the necessary attributes.
5893
It should be called before establishing a connection to the database.
5994
"""
6095
self._allocate_environment_handle()
6196
self._set_environment_attributes()
6297
self._allocate_connection_handle()
98+
self._set_connection_attributes()
6399
self._connect_to_db()
64100

65101
def _allocate_environment_handle(self):
66102
"""
67-
Allocate the ODBC environment handle.
103+
Allocate the environment handle.
68104
"""
69105
ret = ddbc_bindings.DDBCSQLAllocHandle(
70106
odbc_sql_const.SQL_HANDLE_ENV.value, #SQL environment handle type
@@ -75,7 +111,7 @@ def _allocate_environment_handle(self):
75111

76112
def _set_environment_attributes(self):
77113
"""
78-
Set the ODBC environment attributes.
114+
Set the environment attributes.
79115
"""
80116
ret = ddbc_bindings.DDBCSQLSetEnvAttr(
81117
self.henv.value, # Environment handle
@@ -87,7 +123,7 @@ def _set_environment_attributes(self):
87123

88124
def _allocate_connection_handle(self):
89125
"""
90-
Allocate the ODBC connection handle.
126+
Allocate the connection handle.
91127
"""
92128
ret = ddbc_bindings.DDBCSQLAllocHandle(
93129
odbc_sql_const.SQL_HANDLE_DBC.value, # SQL connection handle type
@@ -96,6 +132,19 @@ def _allocate_connection_handle(self):
96132
)
97133
check_error(odbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
98134

135+
def _set_connection_attributes(self):
136+
"""
137+
Set the connection attributes before connecting.
138+
"""
139+
if self.autocommit:
140+
ret = ddbc_bindings.DDBCSQLSetConnectAttr(
141+
self.hdbc.value,
142+
odbc_sql_const.SQL_ATTR_AUTOCOMMIT.value,
143+
odbc_sql_const.SQL_AUTOCOMMIT_ON.value,
144+
0
145+
)
146+
check_error(odbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, ret)
147+
99148
def _connect_to_db(self) -> None:
100149
"""
101150
Establish a connection to the database.
@@ -133,8 +182,9 @@ def autocommit(self) -> bool:
133182
)
134183
check_error(odbc_sql_const.SQL_HANDLE_DBC.value, self.hdbc.value, autocommit_mode)
135184
return autocommit_mode == odbc_sql_const.SQL_AUTOCOMMIT_ON.value
136-
137-
def setautocommit(self, value: bool) -> None:
185+
186+
@autocommit.setter
187+
def autocommit(self, value: bool) -> None:
138188
"""
139189
Set the autocommit mode of the connection.
140190
Args:
@@ -155,6 +205,18 @@ def setautocommit(self, value: bool) -> None:
155205
if ENABLE_LOGGING:
156206
logger.info("Autocommit mode set to %s.", value)
157207

208+
def setautocommit(self, value: bool = True) -> None:
209+
"""
210+
Set the autocommit mode of the connection.
211+
Args:
212+
value (bool): True to enable autocommit, False to disable it.
213+
Returns:
214+
None
215+
Raises:
216+
DatabaseError: If there is an error while setting the autocommit mode.
217+
"""
218+
self.autocommit = value
219+
158220
def cursor(self) -> Cursor:
159221
"""
160222
Return a new Cursor object using the connection.

mssql_python/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@ class ConstantsODBC(Enum):
1717
SQL_ATTR_ODBC_VERSION = 200
1818
SQL_ATTR_ASYNC_ENABLE = 4
1919
SQL_ATTR_ASYNC_STMT_EVENT = 29
20-
SQL_ATTR_AUTOCOMMIT = 102
2120
SQL_ERROR = -1
2221
SQL_INVALID_HANDLE = -2
2322
SQL_NULL_HANDLE = 0
2423
SQL_OV_ODBC3 = 3
2524
SQL_COMMIT = 0
2625
SQL_ROLLBACK = 1
26+
SQL_ATTR_AUTOCOMMIT = 102
2727
SQL_SMALLINT = 5
2828
SQL_CHAR = 1
2929
SQL_WCHAR = -8

mssql_python/db_connection.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,22 @@
11
from mssql_python.exceptions import DatabaseError, InterfaceError
22
from mssql_python.connection import Connection
33

4-
def connect(connection_str: str) -> Connection:
4+
def connect(connection_str: str, autocommit: bool = True, **kwargs) -> Connection:
55
"""
66
Constructor for creating a connection to the database.
77
88
Args:
9-
connection_str (str): The connection_str to connect to.
9+
connection_str (str): The connection string to connect to.
10+
autocommit (bool): If True, causes a commit to be performed after each SQL statement.
11+
TODO: Add the following parameters to the function signature:
12+
timeout (int): The timeout for the connection attempt, in seconds.
13+
readonly (bool): If True, the connection is set to read-only.
14+
attrs_before (dict): A dictionary of connection attributes to set before connecting.
15+
Keyword Args:
16+
**kwargs: Additional key/value pairs for the connection string.
17+
Below attributes are not implemented in the internal driver:
18+
- encoding (str): The encoding for the connection string.
19+
- ansi (bool): If True, indicates the driver does not support Unicode.
1020
1121
Returns:
1222
Connection: A new connection object to interact with the database.
@@ -19,5 +29,9 @@ def connect(connection_str: str) -> Connection:
1929
be used to perform database operations such as executing queries, committing
2030
transactions, and closing the connection.
2131
"""
22-
conn = Connection(connection_str)
23-
return conn
32+
conn = Connection(
33+
connection_str,
34+
autocommit=autocommit,
35+
**kwargs
36+
)
37+
return conn

mssql_python/error_handling.py

Whitespace-only changes.

mssql_python/helpers.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from mssql_python.constants import ConstantsODBC
21
from mssql_python import ddbc_bindings
32
from mssql_python.exceptions import raise_exception
43
from mssql_python.logging_config import get_logger, ENABLE_LOGGING

tests/test_003_connection.py

Lines changed: 55 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,27 +30,65 @@ def test_connection(db_connection):
3030
cursor = db_connection.cursor()
3131
assert cursor is not None, "Database connection failed - Cursor cannot be None"
3232

33+
34+
def test_construct_connection_string(db_connection):
35+
# Check if the connection string is constructed correctly with kwargs
36+
conn_str = db_connection._construct_connection_string("",host="localhost", user="me", password="mypwd", database="mydb", encrypt="yes", trust_server_certificate="yes")
37+
assert "Server=localhost;" in conn_str, "Connection string should contain 'Server=localhost;'"
38+
assert "Uid=me;" in conn_str, "Connection string should contain 'Uid=me;'"
39+
assert "Pwd=mypwd;" in conn_str, "Connection string should contain 'Pwd=mypwd;'"
40+
assert "Database=mydb;" in conn_str, "Connection string should contain 'Database=mydb;'"
41+
assert "Encrypt=yes;" in conn_str, "Connection string should contain 'Encrypt=yes;'"
42+
assert "TrustServerCertificate=yes;" in conn_str, "Connection string should contain 'TrustServerCertificate=yes;'"
43+
assert "APP=MSSQL-Python" in conn_str, "Connection string should contain 'APP=MSSQL-Python'"
44+
assert "Driver={ODBC Driver 18 for SQL Server}" in conn_str, "Connection string should contain 'Driver={ODBC Driver 18 for SQL Server}'"
45+
assert "Driver={ODBC Driver 18 for SQL Server};;APP=MSSQL-Python;Server=localhost;Uid=me;Pwd=mypwd;Database=mydb;Encrypt=yes;TrustServerCertificate=yes;" == conn_str, "Connection string is incorrect"
46+
3347
def test_autocommit_default(db_connection):
3448
assert db_connection.autocommit is True, "Autocommit should be True by default"
3549

36-
# def test_autocommit_property(db_connection):
37-
# db_connection.autocommit = True
38-
# assert db_connection.autocommit is True, "Autocommit should be True"
39-
# # Create a new cursor to check if the autocommit property is set in the server
40-
# cursor = db_connection.cursor()
41-
# cursor.execute("DBCC USEROPTIONS;")
42-
# result = cursor.fetchall()
43-
# implicit_transactions_status = any(option[0] == 'implicit_transactions' and option[1] == 'SET' for option in result)
44-
# assert not implicit_transactions_status, "Autocommit failed: Implicit transactions should be off"
45-
46-
# db_connection.autocommit = False
47-
# assert db_connection.autocommit is False, "Autocommit should be False"
48-
# cursor.execute("DBCC USEROPTIONS;")
49-
# result = cursor.fetchall()
50-
# implicit_transactions_status = any(option[0] == 'implicit_transactions' and option[1] == 'SET' for option in result)
51-
# assert implicit_transactions_status, "Autocommit failed: Implicit transactions should be on"
50+
def test_autocommit_setter(db_connection):
51+
db_connection.autocommit = True
52+
cursor = db_connection.cursor()
53+
# Make a transaction and check if it is autocommited
54+
drop_table_if_exists(cursor, "pytest_test_autocommit")
55+
try:
56+
cursor.execute("CREATE TABLE pytest_test_autocommit (id INT PRIMARY KEY, value VARCHAR(50));")
57+
cursor.execute("INSERT INTO pytest_test_autocommit (id, value) VALUES (1, 'test');")
58+
cursor.execute("SELECT * FROM pytest_test_autocommit WHERE id = 1;")
59+
result = cursor.fetchone()
60+
assert result is not None, "Autocommit failed: No data found"
61+
assert result[1] == 'test', "Autocommit failed: Incorrect data"
62+
except Exception as e:
63+
pytest.fail(f"Autocommit failed: {e}")
64+
finally:
65+
cursor.execute("DROP TABLE pytest_test_autocommit;")
66+
db_connection.commit()
67+
assert db_connection.autocommit is True, "Autocommit should be True"
68+
69+
db_connection.autocommit = False
70+
cursor = db_connection.cursor()
71+
# Make a transaction and check if it is not autocommited
72+
drop_table_if_exists(cursor, "pytest_test_autocommit")
73+
try:
74+
cursor.execute("CREATE TABLE pytest_test_autocommit (id INT PRIMARY KEY, value VARCHAR(50));")
75+
cursor.execute("INSERT INTO pytest_test_autocommit (id, value) VALUES (1, 'test');")
76+
cursor.execute("SELECT * FROM pytest_test_autocommit WHERE id = 1;")
77+
result = cursor.fetchone()
78+
assert result is not None, "Autocommit failed: No data found"
79+
assert result[1] == 'test', "Autocommit failed: Incorrect data"
80+
db_connection.commit()
81+
cursor.execute("SELECT * FROM pytest_test_autocommit WHERE id = 1;")
82+
result = cursor.fetchone()
83+
assert result is not None, "Autocommit failed: No data found after commit"
84+
assert result[1] == 'test', "Autocommit failed: Incorrect data after commit"
85+
except Exception as e:
86+
pytest.fail(f"Autocommit failed: {e}")
87+
finally:
88+
cursor.execute("DROP TABLE pytest_test_autocommit;")
89+
db_connection.commit()
5290

53-
def test_set_autcommit(db_connection):
91+
def test_set_autocommit(db_connection):
5492
db_connection.setautocommit(True)
5593
assert db_connection.autocommit is True, "Autocommit should be True"
5694
db_connection.setautocommit(False)

0 commit comments

Comments
 (0)