Skip to content

Commit 3ef7fc4

Browse files
bewithgauravSumit Sarabhai
authored andcommitted
Merged PR 5315: Added Tests, Major Fixes, Logging and Support Datatypes
#### AI description (iteration 1) #### PR Classification Bug fix and new feature implementation. #### PR Summary This pull request includes major fixes, logging enhancements, and support for new data types, along with added tests. - `mssql_python/exceptions.py`: Refactored exception handling, added logging, and improved error message truncation. - `mssql_python/cursor.py`: Enhanced logging for query execution and parameter handling, fixed data type handling for `decimal.Decimal` and `datetime`. - Added new test files `tests/test_005_exceptions.py` and `tests/test_types.py` to cover exception handling and data type support. - `mssql_python/pybind/ddbc_bindings.cpp`: Improved handling of datetime and error messages, added detailed logging. - `mssql_python/helpers.py`: Updated error checking to raise custom exceptions with detailed logging. <!-- GitOpsUserAgent=GitOps.Apps.Server.pullrequestcopilot --> Related work items: #33809, #33810
1 parent d89d308 commit 3ef7fc4

13 files changed

Lines changed: 190 additions & 102 deletions

mssql_python/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@
2323

2424
# Connection Objects
2525
from .connection import Connection
26+
from .db_connection import connect
2627

2728
# Cursor Objects
2829
from .cursor import Cursor
29-
30-
from .db_connection import connect

mssql_python/cursor.py

Lines changed: 25 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -222,16 +222,6 @@ def _get_numeric_data(self, param):
222222
numeric_data.sign = param.as_tuple().sign
223223
numeric_data.val = str(param)
224224

225-
print("NUMERIC DATA!!!", numeric_data.precision, numeric_data.scale, numeric_data.sign, numeric_data.val)
226-
# precision = param.as_tuple().digits
227-
# scale = param.as_tuple().exponent * -1
228-
# sign = param.as_tuple().sign
229-
# numeric_data = {
230-
# 'precision': len(precision),
231-
# 'scale': scale,
232-
# 'sign': sign,
233-
# 'value': param
234-
# }
235225
return numeric_data
236226

237227
def _map_sql_type(self, param, parameters_list, i):
@@ -259,13 +249,12 @@ def _map_sql_type(self, param, parameters_list, i):
259249
return odbc_sql_const.SQL_FLOAT.value, odbc_sql_const.SQL_C_DOUBLE.value, 15, 0
260250

261251
elif isinstance(param, decimal.Decimal):
262-
if param.as_tuple().exponent == -4: # Scale is 4
263-
if -214748.3648 <= param <= 214748.3647:
264-
return odbc_sql_const.SQL_SMALLMONEY.value, odbc_sql_const.SQL_C_NUMERIC.value, 10, 4
265-
elif -922337203685477.5808 <= param <= 922337203685477.5807:
266-
return odbc_sql_const.SQL_MONEY.value, odbc_sql_const.SQL_C_NUMERIC.value, 19, 4
252+
# if param.as_tuple().exponent == -4: # Scale is 4
253+
# if -214748.3648 <= param <= 214748.3647:
254+
# return odbc_sql_const.SQL_SMALLMONEY.value, odbc_sql_const.SQL_C_NUMERIC.value, 10, 4
255+
# elif -922337203685477.5808 <= param <= 922337203685477.5807:
256+
# return odbc_sql_const.SQL_MONEY.value, odbc_sql_const.SQL_C_NUMERIC.value, 19, 4
267257
parameters_list[i] = self._get_numeric_data(param) # Replace the parameter with the dictionary
268-
print("DECIMAL!!!", odbc_sql_const.SQL_DECIMAL.value, odbc_sql_const.SQL_C_NUMERIC.value)
269258
return odbc_sql_const.SQL_DECIMAL.value, odbc_sql_const.SQL_C_NUMERIC.value, len(param.as_tuple().digits), param.as_tuple().exponent * -1
270259

271260
elif isinstance(param, str):
@@ -319,12 +308,13 @@ def _map_sql_type(self, param, parameters_list, i):
319308
elif isinstance(param, uuid.UUID): # Handle uniqueidentifier
320309
return odbc_sql_const.SQL_GUID.value, odbc_sql_const.SQL_C_GUID.value, 36, 0
321310

322-
elif isinstance(param, datetime.date):
323-
return odbc_sql_const.SQL_DATE.value, odbc_sql_const.SQL_C_TYPE_DATE.value, 10, 0
324-
325311
elif isinstance(param, datetime.datetime):
312+
# Always keep datetime.datetime check before datetime.date check since datetime.datetime is a subclass of datetime (isinstance(datetime.datetime, datetime.date) returns True)
326313
return odbc_sql_const.SQL_TIMESTAMP.value, odbc_sql_const.SQL_C_TYPE_TIMESTAMP.value, 23, 3
327314

315+
elif isinstance(param, datetime.date):
316+
return odbc_sql_const.SQL_DATE.value, odbc_sql_const.SQL_C_TYPE_DATE.value, 10, 0
317+
328318
elif isinstance(param, datetime.time):
329319
return odbc_sql_const.SQL_TIME.value, odbc_sql_const.SQL_C_TYPE_TIME.value, 8, 0
330320

@@ -336,11 +326,7 @@ def _initialize_cursor(self) -> None:
336326
"""
337327
Initialize the ODBC statement handle.
338328
"""
339-
# Allocate the statement handle
340-
# try:
341329
self._allocate_statement_handle()
342-
# except Exception as e:
343-
# logging.error("An error occurred during initialization: %s", e)
344330

345331
def _allocate_statement_handle(self):
346332
"""
@@ -504,6 +490,22 @@ def execute(self, operation: str, *parameters, use_prepare: bool = True, reset_c
504490
'''
505491
Execute SQL Statement - (SQLExecute)
506492
'''
493+
if ENABLE_LOGGING:
494+
# TODO - Need to evaluate encrypted logs for query parameters
495+
logging.debug("Executing query: %s", operation)
496+
for i, param in enumerate(parameters):
497+
logging.debug(
498+
"Parameter number: %s, Parameter: %s, Param Python Type: %s, ParamInfo: %s, %s, %s, %s, %s",
499+
i+1,
500+
param,
501+
str(type(param)),
502+
parameters_type[i].paramSQLType,
503+
parameters_type[i].paramCType,
504+
parameters_type[i].columnSize,
505+
parameters_type[i].decimalDigits,
506+
parameters_type[i].inputOutputType
507+
)
508+
507509
ret = ddbc_bindings.DDBCSQLExecute(self.hstmt.value, operation, parameters, parameters_type,
508510
self.is_stmt_prepared, use_prepare)
509511
check_error(odbc_sql_const.SQL_HANDLE_STMT.value, self.hstmt.value, ret)

mssql_python/exceptions.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class Exception(Exception):
99
"""
1010
def __init__(self, driver_error, ddbc_error) -> None:
1111
self.driver_error = driver_error
12-
self.ddbc_error = ddbc_error
12+
self.ddbc_error = truncate_error_message(ddbc_error)
1313
self.message = f"Driver Error: {self.driver_error}; DDBC Error: {self.ddbc_error}"
1414
super().__init__(self.message)
1515

@@ -218,13 +218,8 @@ def sqlstate_to_exception(sqlstate: str, ddbc_error: str) -> Exception:
218218

219219
def truncate_error_message(error_message: str) -> str:
220220
'''
221-
Sample Error Message:
222-
mssql_python.exceptions.ProgrammingError: Driver Error: Syntax error or access violation; DDBC Error: [Microsoft][ODBC Driver 18 for SQL Server][SQL Server]Incorrect syntax near the keyword 'from'.
223-
Remove [ODBC Driver 18 for SQL Server] from the error message.
224-
- The Driver Error message is the message that is returned by the ODBC driver.
225-
- It will always start with [ODBC Driver <version> for SQL Server].
226-
- The DDBC Error message is the message that is returned by the database server.
227-
- this section will always be at the start of the message.
221+
- The Driver Error message is the message that is returned by the Internal driver.
222+
- This section will always be at the start of the message.
228223
'''
229224
try:
230225
if not error_message.startswith('[Microsoft]'):
@@ -252,10 +247,9 @@ def raise_exception(sqlstate: str, ddbc_error: str) -> None:
252247
Raises:
253248
DatabaseError: If the SQLSTATE code is not found in the mapping.
254249
"""
255-
exception_class = sqlstate_to_exception(
256-
sqlstate,
257-
truncate_error_message(ddbc_error)
258-
)
250+
exception_class = sqlstate_to_exception(sqlstate, ddbc_error)
259251
if exception_class:
252+
if ENABLE_LOGGING:
253+
logging.error(exception_class)
260254
raise exception_class
261255
raise DatabaseError(driver_error="An error occurred with SQLSTATE code", ddbc_error=f"Unknown DDBC error: {sqlstate}")

mssql_python/helpers.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,14 +59,12 @@ def check_error(handle_type, handle, ret):
5959
Raises:
6060
RuntimeError: If an error is found.
6161
"""
62-
if ENABLE_LOGGING:
63-
logging.debug(f"Checking error for handle type: {handle_type}, handle: {handle}, ret: {ret}")
64-
if ret == ConstantsODBC.SQL_ERROR.value:
62+
if ret < 0:
6563
error_info = ddbc_bindings.DDBCSQLCheckError(handle_type, handle, ret)
6664
if ENABLE_LOGGING:
6765
logging.error(f"Error: {error_info.ddbcErrorMsg}")
6866
raise_exception(error_info.sqlState, error_info.ddbcErrorMsg)
69-
67+
7068
def add_driver_name_to_app_parameter(connection_string):
7169
"""
7270
Modifies the input connection string by appending the APP name.

mssql_python/logging_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
# Variable to enable or disable logging
66
ENABLE_LOGGING = False
77

8-
def setup_logging(log_level=logging.INFO):
8+
def setup_logging(log_level=logging.DEBUG):
99
"""
1010
Set up logging configuration.
1111

mssql_python/pybind/ddbc_bindings.cpp

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <pybind11/chrono.h>
1313
#include <pybind11/complex.h>
1414
#include <pybind11/functional.h>
15+
#include <pybind11/pytypes.h> // Add this line for datetime support
1516
#include <pybind11/stl.h>
1617
#include <windows.h> // windows.h needs to be included before sql.h
1718
#include <sql.h>
@@ -957,7 +958,13 @@ SQLRETURN SQLGetData_wrap(intptr_t StatementHandle, SQLUSMALLINT colCount, py::l
957958
ret =
958959
SQLGetData_ptr(hStmt, i, SQL_C_TYPE_DATE, &dateValue, sizeof(dateValue), NULL);
959960
if (SQL_SUCCEEDED(ret)) {
960-
row.append(py::make_tuple(dateValue.year, dateValue.month, dateValue.day));
961+
row.append(
962+
py::module_::import("datetime").attr("date")(
963+
dateValue.year,
964+
dateValue.month,
965+
dateValue.day
966+
)
967+
);
961968
} else {
962969
row.append(py::none());
963970
}
@@ -970,7 +977,13 @@ SQLRETURN SQLGetData_wrap(intptr_t StatementHandle, SQLUSMALLINT colCount, py::l
970977
ret =
971978
SQLGetData_ptr(hStmt, i, SQL_C_TYPE_TIME, &timeValue, sizeof(timeValue), NULL);
972979
if (SQL_SUCCEEDED(ret)) {
973-
row.append(py::make_tuple(timeValue.hour, timeValue.minute, timeValue.second));
980+
row.append(
981+
py::module_::import("datetime").attr("time")(
982+
timeValue.hour,
983+
timeValue.minute,
984+
timeValue.second
985+
)
986+
);
974987
} else {
975988
row.append(py::none());
976989
}
@@ -983,9 +996,16 @@ SQLRETURN SQLGetData_wrap(intptr_t StatementHandle, SQLUSMALLINT colCount, py::l
983996
ret = SQLGetData_ptr(hStmt, i, SQL_C_TYPE_TIMESTAMP, &timestampValue,
984997
sizeof(timestampValue), NULL);
985998
if (SQL_SUCCEEDED(ret)) {
986-
row.append(py::make_tuple(timestampValue.year, timestampValue.month,
987-
timestampValue.day, timestampValue.hour,
988-
timestampValue.minute, timestampValue.second));
999+
row.append(
1000+
py::module_::import("datetime").attr("datetime")(
1001+
timestampValue.year,
1002+
timestampValue.month,
1003+
timestampValue.day,
1004+
timestampValue.hour,
1005+
timestampValue.minute,
1006+
timestampValue.second
1007+
)
1008+
);
9891009
} else {
9901010
row.append(py::none());
9911011
}
@@ -1305,27 +1325,40 @@ SQLRETURN FetchBatchData(SQLHSTMT hStmt, ColumnBuffers& buffers, py::list& colum
13051325
case SQL_TIMESTAMP:
13061326
case SQL_TYPE_TIMESTAMP:
13071327
case SQL_DATETIME:
1308-
row.append(py::make_tuple(buffers.timestampBuffers[col - 1][i].year,
1309-
buffers.timestampBuffers[col - 1][i].month,
1310-
buffers.timestampBuffers[col - 1][i].day,
1311-
buffers.timestampBuffers[col - 1][i].hour,
1312-
buffers.timestampBuffers[col - 1][i].minute,
1313-
buffers.timestampBuffers[col - 1][i].second));
1328+
row.append(
1329+
py::module_::import("datetime").attr("datetime")(
1330+
buffers.timestampBuffers[col - 1][i].year,
1331+
buffers.timestampBuffers[col - 1][i].month,
1332+
buffers.timestampBuffers[col - 1][i].day,
1333+
buffers.timestampBuffers[col - 1][i].hour,
1334+
buffers.timestampBuffers[col - 1][i].minute,
1335+
buffers.timestampBuffers[col - 1][i].second
1336+
)
1337+
1338+
);
13141339
break;
13151340
case SQL_BIGINT:
13161341
row.append(buffers.bigIntBuffers[col - 1][i]);
13171342
break;
13181343
case SQL_TYPE_DATE:
1319-
row.append(py::make_tuple(buffers.dateBuffers[col - 1][i].year,
1320-
buffers.dateBuffers[col - 1][i].month,
1321-
buffers.dateBuffers[col - 1][i].day));
1344+
row.append(
1345+
py::module_::import("datetime").attr("date")(
1346+
buffers.dateBuffers[col - 1][i].year,
1347+
buffers.dateBuffers[col - 1][i].month,
1348+
buffers.dateBuffers[col - 1][i].day
1349+
)
1350+
);
13221351
break;
13231352
case SQL_TIME:
13241353
case SQL_TYPE_TIME:
13251354
case SQL_SS_TIME2:
1326-
row.append(py::make_tuple(buffers.timeBuffers[col - 1][i].hour,
1327-
buffers.timeBuffers[col - 1][i].minute,
1328-
buffers.timeBuffers[col - 1][i].second));
1355+
row.append(
1356+
py::module_::import("datetime").attr("time")(
1357+
buffers.timeBuffers[col - 1][i].hour,
1358+
buffers.timeBuffers[col - 1][i].minute,
1359+
buffers.timeBuffers[col - 1][i].second
1360+
)
1361+
);
13291362
break;
13301363
case SQL_GUID:
13311364
row.append(py::bytes(

mssql_python/type.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,13 @@ def TimeFromTicks(ticks: int) -> datetime.time:
6666
"""
6767
Generates a time object from ticks.
6868
"""
69-
return datetime.time(*time.localtime(ticks)[3:6])
69+
return datetime.time(*time.gmtime(ticks)[3:6])
7070

7171
def TimestampFromTicks(ticks: int) -> datetime.datetime:
7272
"""
7373
Generates a timestamp object from ticks.
7474
"""
75-
return datetime.datetime.fromtimestamp(ticks)
75+
return datetime.datetime.fromtimestamp(ticks, datetime.UTC)
7676

7777
def Binary(string: str) -> bytes:
7878
"""

tests/conftest.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,3 @@ def db_connection(conn_str):
3838
def cursor(db_connection):
3939
cursor = db_connection.cursor()
4040
yield cursor
41-
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,4 @@ def test_threadsafety():
2121

2222
def test_paramstyle():
2323
# Check if paramstyle has the expected value
24-
assert paramstyle == "qmark", "paramstyle should be 'qmark'"
24+
assert paramstyle == "qmark", "paramstyle should be 'qmark'"

tests/test_002_types.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import pytest
2+
import datetime
3+
from mssql_python.type import STRING, BINARY, NUMBER, DATETIME, ROWID, Date, Time, Timestamp, DateFromTicks, TimeFromTicks, TimestampFromTicks, Binary
4+
5+
def test_string_type():
6+
assert STRING().type == "STRING", "STRING type mismatch"
7+
8+
def test_binary_type():
9+
assert BINARY().type == "BINARY", "BINARY type mismatch"
10+
11+
def test_number_type():
12+
assert NUMBER().type == "NUMBER", "NUMBER type mismatch"
13+
14+
def test_datetime_type():
15+
assert DATETIME().type == "DATETIME", "DATETIME type mismatch"
16+
17+
def test_rowid_type():
18+
assert ROWID().type == "ROWID", "ROWID type mismatch"
19+
20+
def test_date_constructor():
21+
date = Date(2023, 10, 5)
22+
assert isinstance(date, datetime.date), "Date constructor did not return a date object"
23+
assert date.year == 2023 and date.month == 10 and date.day == 5, "Date constructor returned incorrect date"
24+
25+
def test_time_constructor():
26+
time = Time(12, 30, 45)
27+
assert isinstance(time, datetime.time), "Time constructor did not return a time object"
28+
assert time.hour == 12 and time.minute == 30 and time.second == 45, "Time constructor returned incorrect time"
29+
30+
def test_timestamp_constructor():
31+
timestamp = Timestamp(2023, 10, 5, 12, 30, 45)
32+
assert isinstance(timestamp, datetime.datetime), "Timestamp constructor did not return a datetime object"
33+
assert timestamp.year == 2023 and timestamp.month == 10 and timestamp.day == 5, "Timestamp constructor returned incorrect date"
34+
assert timestamp.hour == 12 and timestamp.minute == 30 and timestamp.second == 45, "Timestamp constructor returned incorrect time"
35+
36+
def test_date_from_ticks():
37+
ticks = 1696500000 # Corresponds to 2023-10-05
38+
date = DateFromTicks(ticks)
39+
assert isinstance(date, datetime.date), "DateFromTicks did not return a date object"
40+
assert date == datetime.date(2023, 10, 5), "DateFromTicks returned incorrect date"
41+
42+
def test_time_from_ticks():
43+
ticks = 1696500000 # Corresponds to 10:00:00
44+
time = TimeFromTicks(ticks)
45+
assert isinstance(time, datetime.time), "TimeFromTicks did not return a time object"
46+
assert time == datetime.time(10, 0, 0), "TimeFromTicks returned incorrect time"
47+
48+
def test_timestamp_from_ticks():
49+
ticks = 1696500000 # Corresponds to 2023-10-05 10:00:00
50+
timestamp = TimestampFromTicks(ticks)
51+
assert isinstance(timestamp, datetime.datetime), "TimestampFromTicks did not return a datetime object"
52+
assert timestamp == datetime.datetime(2023, 10, 5, 10, 0, 0, tzinfo=datetime.timezone.utc), "TimestampFromTicks returned incorrect timestamp"
53+
54+
def test_binary_constructor():
55+
binary = Binary("test")
56+
assert isinstance(binary, bytes), "Binary constructor did not return a bytes object"
57+
assert binary == b"test", "Binary constructor returned incorrect bytes"

0 commit comments

Comments
 (0)