From 451688bbbe66f972564e07a577c5e185b6f9b00a Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Mon, 1 Sep 2025 14:38:29 +0530 Subject: [PATCH 1/4] working money --- main.py | 106 ++++++++++++++++++++++++-- mssql_python/cursor.py | 53 ++++++++++--- mssql_python/pybind/ddbc_bindings.cpp | 73 +++++++++++++----- 3 files changed, 197 insertions(+), 35 deletions(-) diff --git a/main.py b/main.py index b45b88d73..5cb7188d6 100644 --- a/main.py +++ b/main.py @@ -2,20 +2,112 @@ from mssql_python import setup_logging import os import decimal +# import pyodbc -setup_logging('stdout') +# setup_logging('stdout') -conn_str = os.getenv("DB_CONNECTION_STRING") +# conn_str = os.getenv("DB_CONNECTION_STRING") +conn_str = "Server=Saumya;DATABASE=master;UID=sa;PWD=HappyPass1234;Trust_Connection=yes;TrustServerCertificate=yes;" conn = connect(conn_str) - +# conn_str = "DRIVER={ODBC Driver 18 for SQL Server};Server=Saumya;DATABASE=master;UID=sa;PWD=HappyPass1234;Trust_Connection=yes;TrustServerCertificate=yes;" +# conn = pyodbc.connect(conn_str) # conn.autocommit = True cursor = conn.cursor() -cursor.execute("SELECT database_id, name from sys.databases;") -rows = cursor.fetchall() +# cursor.execute("SELECT database_id, name from sys.databases;") +# rows = cursor.fetchall() + +# for row in rows: +# print(f"Database ID: {row[0]}, Name: {row[1]}") + +# cursor.execute("DROP TABLE IF EXISTS #pytest_nvarchar_chunk") +# cursor.execute("CREATE TABLE #pytest_nvarchar_chunk (col NVARCHAR(MAX))") +# conn.commit() + +# chunk_size = 8192 # bytes +# # test_str = "😄" * ((chunk_size // 4) + 3) # slightly > 1 chunk +# test_str = "😄" * 50000 + +# cursor.execute("INSERT INTO #pytest_nvarchar_chunk (col) VALUES (?)", [test_str]) +# conn.commit() + +# cursor.execute("CREATE TABLE #pytest_empty_batch (id INT, data NVARCHAR(50))") +# conn.commit() + +# # Insert multiple rows with mix of empty and non-empty +# test_data = [ +# (1, ''), +# (2, 'non-empty'), +# (3, ''), +# (4, 'another'), +# (5, ''), +# ] +# cursor.executemany("INSERT INTO #pytest_empty_batch VALUES (?, ?)", test_data) +# conn.commit() + +# # Test fetchmany with different batch sizes +# cursor.execute("SELECT id, data FROM #pytest_empty_batch ORDER BY id") + +# # Fetch in batches of 2 +# batch1 = cursor.fetchall() +# print(batch1) + + +# Drop & recreate table +cursor.execute("IF OBJECT_ID('dbo.money_test', 'U') IS NOT NULL DROP TABLE dbo.money_test;") +conn.commit() + +cursor.execute(""" + CREATE TABLE money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY, + d DECIMAL(19,4), + n NUMERIC(10,4) + ) +""") +conn.commit() + +# Insert valid rows covering MONEY & SMALLMONEY ranges +cursor.execute(""" + INSERT INTO money_test (m, sm, d, n) VALUES + -- Max values + (922337203685477.5807, 214748.3647, 9999999999999.9999, 1234.5678), + -- Min values + (-922337203685477.5808, -214748.3648, -9999999999999.9999, -1234.5678), + -- Typical mid values + (1234567.8901, 12345.6789, 42.4242, 3.1415), + -- Nulls + (NULL, NULL, NULL, NULL) +""") +conn.commit() + +# Fetch rows one by one +cursor.execute("SELECT m, sm, d, n FROM money_test ORDER BY id") + +while True: + row = cursor.fetchone() + if not row: + break + print("Row:", row) + for idx, col in enumerate(row, 1): + print(f" col{idx}: {col!r} ({type(col)})") + +# Roundtrip check with Decimal parameters +cursor.execute( + "INSERT INTO money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + ( + decimal.Decimal("123.4567"), + decimal.Decimal("99.9999"), + decimal.Decimal("42.4242"), + decimal.Decimal("3.1415"), + ), +) +conn.commit() -for row in rows: - print(f"Database ID: {row[0]}, Name: {row[1]}") +cursor.execute("SELECT TOP 1 m, sm, d, n FROM money_test ORDER BY id DESC") +row = cursor.fetchone() +print("Inserted decimal roundtrip ->", row, [type(c) for c in row]) cursor.close() conn.close() \ No newline at end of file diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index f3c76853f..14ecf262f 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -282,18 +282,49 @@ def _map_sql_type(self, param, parameters_list, i): 0, False, ) - + if isinstance(param, decimal.Decimal): - parameters_list[i] = self._get_numeric_data( - param - ) # Replace the parameter with the dictionary - return ( - ddbc_sql_const.SQL_NUMERIC.value, - ddbc_sql_const.SQL_C_NUMERIC.value, - parameters_list[i].precision, - parameters_list[i].scale, - False, - ) + # Detect MONEY / SMALLMONEY range + if -214748.3648 <= param <= 214748.3647: + # smallmoney + parameters_list[i] = str(param) # let SQL Server handle rounding to 4 decimals + return ( + ddbc_sql_const.SQL_VARCHAR.value, + ddbc_sql_const.SQL_C_CHAR.value, + len(parameters_list[i]), + 0, + ) + elif -922337203685477.5808 <= param <= 922337203685477.5807: + # money + parameters_list[i] = str(param) + return ( + ddbc_sql_const.SQL_VARCHAR.value, + ddbc_sql_const.SQL_C_CHAR.value, + len(parameters_list[i]), + 0, + ) + else: + # fallback to generic numeric binding + parameters_list[i] = self._get_numeric_data(param) + return ( + ddbc_sql_const.SQL_NUMERIC.value, + ddbc_sql_const.SQL_C_NUMERIC.value, + parameters_list[i].precision, + parameters_list[i].scale, + ) + + + # if isinstance(param, decimal.Decimal): + # parameters_list[i] = self._get_numeric_data( + # param + # ) # Replace the parameter with the dictionary + # return ( + # ddbc_sql_const.SQL_NUMERIC.value, + # ddbc_sql_const.SQL_C_NUMERIC.value, + # parameters_list[i].precision, + # parameters_list[i].scale, + # False, + # ) if isinstance(param, str): if ( diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index bbc3a2f52..00c8a7102 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -474,11 +474,22 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, decimalPtr->precision = decimalParam.precision; decimalPtr->scale = decimalParam.scale; decimalPtr->sign = decimalParam.sign; - // Convert the integer decimalParam.val to char array - std::memset(static_cast(decimalPtr->val), 0, sizeof(decimalPtr->val)); - std::memcpy(static_cast(decimalPtr->val), - reinterpret_cast(&decimalParam.val), - sizeof(decimalParam.val)); + // // Convert the integer decimalParam.val to char array + // std::memset(static_cast(decimalPtr->val), 0, sizeof(decimalPtr->val)); + // std::memcpy(static_cast(decimalPtr->val), + // reinterpret_cast(&decimalParam.val), + // sizeof(decimalParam.val)); + + + // Zero out the byte array + std::memset(decimalPtr->val, 0, sizeof(decimalPtr->val)); + + // Encode val into little-endian byte array + uint64_t scaledVal = decimalParam.val; + for (size_t i = 0; i < sizeof(decimalPtr->val) && scaledVal > 0; i++) { + decimalPtr->val[i] = static_cast(scaledVal & 0xFF); + scaledVal >>= 8; + } dataPtr = static_cast(decimalPtr); // TODO: Remove these lines //strLenOrIndPtr = AllocateParamBuffer(paramBuffers); @@ -1910,31 +1921,59 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } break; } + // case SQL_DECIMAL: + // case SQL_NUMERIC: { + // SQLCHAR numericStr[MAX_DIGITS_IN_NUMERIC] = {0}; + // SQLLEN indicator; + // ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, numericStr, sizeof(numericStr), &indicator); + + // if (SQL_SUCCEEDED(ret)) { + // try{ + // // Convert numericStr to py::decimal.Decimal and append to row + // row.append(py::module_::import("decimal").attr("Decimal")( + // std::string(reinterpret_cast(numericStr), indicator))); + // } catch (const py::error_already_set& e) { + // // If the conversion fails, append None + // LOG("Error converting to decimal: {}", e.what()); + // row.append(py::none()); + // } + // } + // else { + // LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " + // "code - {}. Returning NULL value instead", + // i, dataType, ret); + // row.append(py::none()); + // } + // break; + // } case SQL_DECIMAL: case SQL_NUMERIC: { SQLCHAR numericStr[MAX_DIGITS_IN_NUMERIC] = {0}; - SQLLEN indicator; + SQLLEN indicator = 0; + ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, numericStr, sizeof(numericStr), &indicator); if (SQL_SUCCEEDED(ret)) { - try{ - // Convert numericStr to py::decimal.Decimal and append to row - row.append(py::module_::import("decimal").attr("Decimal")( - std::string(reinterpret_cast(numericStr), indicator))); - } catch (const py::error_already_set& e) { - // If the conversion fails, append None - LOG("Error converting to decimal: {}", e.what()); + if (indicator == SQL_NULL_DATA) { row.append(py::none()); + } else { + try { + std::string s(reinterpret_cast(numericStr)); + auto Decimal = py::module_::import("decimal").attr("Decimal"); + row.append(Decimal(s)); + } catch (const py::error_already_set& e) { + LOG("Error converting to Decimal: {}", e.what()); + row.append(py::none()); + } } - } - else { - LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " - "code - {}. Returning NULL value instead", + } else { + LOG("Error retrieving data for column - {}, data type - {}, SQLGetData rc - {}", i, dataType, ret); row.append(py::none()); } break; } + case SQL_DOUBLE: case SQL_FLOAT: { SQLDOUBLE doubleValue; From f81e07d9202cc08758cf07c45c1f610771d12af0 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Tue, 9 Sep 2025 15:14:03 +0530 Subject: [PATCH 2/4] added tests --- mssql_python/cursor.py | 3 + tests/test_004_cursor.py | 219 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 14ecf262f..c1d8819cc 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -293,6 +293,7 @@ def _map_sql_type(self, param, parameters_list, i): ddbc_sql_const.SQL_C_CHAR.value, len(parameters_list[i]), 0, + False, ) elif -922337203685477.5808 <= param <= 922337203685477.5807: # money @@ -302,6 +303,7 @@ def _map_sql_type(self, param, parameters_list, i): ddbc_sql_const.SQL_C_CHAR.value, len(parameters_list[i]), 0, + False, ) else: # fallback to generic numeric binding @@ -311,6 +313,7 @@ def _map_sql_type(self, param, parameters_list, i): ddbc_sql_const.SQL_C_NUMERIC.value, parameters_list[i].precision, parameters_list[i].scale, + False, ) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 864a42a9d..fe5cd4a17 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -6548,6 +6548,225 @@ def test_only_null_and_empty_binary(cursor, db_connection): drop_table_if_exists(cursor, "#pytest_null_empty_binary") db_connection.commit() + +def test_money_smallmoney_insert_fetch(cursor, db_connection): + """Test inserting and retrieving valid MONEY and SMALLMONEY values including boundaries and typical data""" + try: + drop_table_if_exists(cursor, "dbo.money_test") + cursor.execute(""" + CREATE TABLE dbo.money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY, + d DECIMAL(19,4), + n NUMERIC(10,4) + ) + """) + db_connection.commit() + + # Max values + cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + (decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647"), + decimal.Decimal("9999999999999.9999"), decimal.Decimal("1234.5678"))) + + # Min values + cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + (decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648"), + decimal.Decimal("-9999999999999.9999"), decimal.Decimal("-1234.5678"))) + + # Typical values + cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + (decimal.Decimal("1234567.8901"), decimal.Decimal("12345.6789"), + decimal.Decimal("42.4242"), decimal.Decimal("3.1415"))) + + # NULL values + cursor.execute("INSERT INTO dbo.money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", + (None, None, None, None)) + + db_connection.commit() + + cursor.execute("SELECT m, sm, d, n FROM dbo.money_test ORDER BY id") + results = cursor.fetchall() + assert len(results) == 4, f"Expected 4 rows, got {len(results)}" + + expected = [ + (decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647"), + decimal.Decimal("9999999999999.9999"), decimal.Decimal("1234.5678")), + (decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648"), + decimal.Decimal("-9999999999999.9999"), decimal.Decimal("-1234.5678")), + (decimal.Decimal("1234567.8901"), decimal.Decimal("12345.6789"), + decimal.Decimal("42.4242"), decimal.Decimal("3.1415")), + (None, None, None, None) + ] + + for i, (row, exp) in enumerate(zip(results, expected)): + for j, (val, exp_val) in enumerate(zip(row, exp), 1): + if exp_val is None: + assert val is None, f"Row {i+1} col{j}: expected None, got {val}" + else: + assert val == exp_val, f"Row {i+1} col{j}: expected {exp_val}, got {val}" + assert isinstance(val, decimal.Decimal), f"Row {i+1} col{j}: expected Decimal, got {type(val)}" + + except Exception as e: + pytest.fail(f"MONEY and SMALLMONEY insert/fetch test failed: {e}") + finally: + drop_table_if_exists(cursor, "dbo.money_test") + db_connection.commit() + + +def test_money_smallmoney_null_handling(cursor, db_connection): + """Test that NULL values for MONEY and SMALLMONEY are stored and retrieved correctly""" + try: + drop_table_if_exists(cursor, "dbo.money_test") + cursor.execute(""" + CREATE TABLE dbo.money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY + ) + """) + db_connection.commit() + + # Row with both NULLs + cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", (None, None)) + + # Row with m filled, sm NULL + cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + (decimal.Decimal("123.4500"), None)) + + # Row with m NULL, sm filled + cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + (None, decimal.Decimal("67.8900"))) + + db_connection.commit() + + cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id") + results = cursor.fetchall() + assert len(results) == 3, f"Expected 3 rows, got {len(results)}" + + expected = [ + (None, None), + (decimal.Decimal("123.4500"), None), + (None, decimal.Decimal("67.8900")) + ] + + for i, (row, exp) in enumerate(zip(results, expected)): + for j, (val, exp_val) in enumerate(zip(row, exp), 1): + if exp_val is None: + assert val is None, f"Row {i+1} col{j}: expected None, got {val}" + else: + assert val == exp_val, f"Row {i+1} col{j}: expected {exp_val}, got {val}" + assert isinstance(val, decimal.Decimal), f"Row {i+1} col{j}: expected Decimal, got {type(val)}" + + except Exception as e: + pytest.fail(f"MONEY and SMALLMONEY NULL handling test failed: {e}") + finally: + drop_table_if_exists(cursor, "dbo.money_test") + db_connection.commit() + + +def test_money_smallmoney_roundtrip(cursor, db_connection): + """Test inserting and retrieving MONEY and SMALLMONEY using decimal.Decimal roundtrip""" + try: + drop_table_if_exists(cursor, "dbo.money_test") + cursor.execute(""" + CREATE TABLE dbo.money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY + ) + """) + db_connection.commit() + + values = (decimal.Decimal("12345.6789"), decimal.Decimal("987.6543")) + cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", values) + db_connection.commit() + + cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id DESC") + row = cursor.fetchone() + for i, (val, exp_val) in enumerate(zip(row, values), 1): + assert val == exp_val, f"col{i} roundtrip mismatch, got {val}, expected {exp_val}" + assert isinstance(val, decimal.Decimal), f"col{i} should be Decimal, got {type(val)}" + + except Exception as e: + pytest.fail(f"MONEY and SMALLMONEY roundtrip test failed: {e}") + finally: + drop_table_if_exists(cursor, "dbo.money_test") + db_connection.commit() + + +def test_money_smallmoney_boundaries(cursor, db_connection): + """Test boundary values for MONEY and SMALLMONEY types are handled correctly""" + try: + drop_table_if_exists(cursor, "dbo.money_test") + cursor.execute(""" + CREATE TABLE dbo.money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY + ) + """) + db_connection.commit() + + # Insert max boundary + cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + (decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647"))) + + # Insert min boundary + cursor.execute("INSERT INTO dbo.money_test (m, sm) VALUES (?, ?)", + (decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648"))) + + db_connection.commit() + + cursor.execute("SELECT m, sm FROM dbo.money_test ORDER BY id DESC") + results = cursor.fetchall() + expected = [ + (decimal.Decimal("-922337203685477.5808"), decimal.Decimal("-214748.3648")), + (decimal.Decimal("922337203685477.5807"), decimal.Decimal("214748.3647")) + ] + for i, (row, exp_row) in enumerate(zip(results, expected), 1): + for j, (val, exp_val) in enumerate(zip(row, exp_row), 1): + assert val == exp_val, f"Row {i} col{j} mismatch, got {val}, expected {exp_val}" + assert isinstance(val, decimal.Decimal), f"Row {i} col{j} should be Decimal, got {type(val)}" + + except Exception as e: + pytest.fail(f"MONEY and SMALLMONEY boundary values test failed: {e}") + finally: + drop_table_if_exists(cursor, "dbo.money_test") + db_connection.commit() + + +def test_money_smallmoney_invalid_values(cursor, db_connection): + """Test that invalid or out-of-range MONEY and SMALLMONEY values raise errors""" + try: + drop_table_if_exists(cursor, "dbo.money_test") + cursor.execute(""" + CREATE TABLE dbo.money_test ( + id INT IDENTITY PRIMARY KEY, + m MONEY, + sm SMALLMONEY + ) + """) + db_connection.commit() + + # Out of range MONEY + with pytest.raises(Exception): + cursor.execute("INSERT INTO dbo.money_test (m) VALUES (?)", (decimal.Decimal("922337203685477.5808"),)) + + # Out of range SMALLMONEY + with pytest.raises(Exception): + cursor.execute("INSERT INTO dbo.money_test (sm) VALUES (?)", (decimal.Decimal("214748.3648"),)) + + # Invalid string + with pytest.raises(Exception): + cursor.execute("INSERT INTO dbo.money_test (m) VALUES (?)", ("invalid_string",)) + + except Exception as e: + pytest.fail(f"MONEY and SMALLMONEY invalid values test failed: {e}") + finally: + drop_table_if_exists(cursor, "dbo.money_test") + db_connection.commit() + def test_close(db_connection): """Test closing the cursor""" try: From 76b60125223267a1dba81e680fd564b00ad8d3d6 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Tue, 9 Sep 2025 15:22:12 +0530 Subject: [PATCH 3/4] cleanup --- main.py | 106 ++------------------------ mssql_python/cursor.py | 15 +--- mssql_python/pybind/ddbc_bindings.cpp | 35 --------- 3 files changed, 8 insertions(+), 148 deletions(-) diff --git a/main.py b/main.py index 5cb7188d6..b45b88d73 100644 --- a/main.py +++ b/main.py @@ -2,112 +2,20 @@ from mssql_python import setup_logging import os import decimal -# import pyodbc -# setup_logging('stdout') +setup_logging('stdout') -# conn_str = os.getenv("DB_CONNECTION_STRING") -conn_str = "Server=Saumya;DATABASE=master;UID=sa;PWD=HappyPass1234;Trust_Connection=yes;TrustServerCertificate=yes;" +conn_str = os.getenv("DB_CONNECTION_STRING") conn = connect(conn_str) -# conn_str = "DRIVER={ODBC Driver 18 for SQL Server};Server=Saumya;DATABASE=master;UID=sa;PWD=HappyPass1234;Trust_Connection=yes;TrustServerCertificate=yes;" -# conn = pyodbc.connect(conn_str) + # conn.autocommit = True cursor = conn.cursor() -# cursor.execute("SELECT database_id, name from sys.databases;") -# rows = cursor.fetchall() - -# for row in rows: -# print(f"Database ID: {row[0]}, Name: {row[1]}") - -# cursor.execute("DROP TABLE IF EXISTS #pytest_nvarchar_chunk") -# cursor.execute("CREATE TABLE #pytest_nvarchar_chunk (col NVARCHAR(MAX))") -# conn.commit() - -# chunk_size = 8192 # bytes -# # test_str = "😄" * ((chunk_size // 4) + 3) # slightly > 1 chunk -# test_str = "😄" * 50000 - -# cursor.execute("INSERT INTO #pytest_nvarchar_chunk (col) VALUES (?)", [test_str]) -# conn.commit() - -# cursor.execute("CREATE TABLE #pytest_empty_batch (id INT, data NVARCHAR(50))") -# conn.commit() - -# # Insert multiple rows with mix of empty and non-empty -# test_data = [ -# (1, ''), -# (2, 'non-empty'), -# (3, ''), -# (4, 'another'), -# (5, ''), -# ] -# cursor.executemany("INSERT INTO #pytest_empty_batch VALUES (?, ?)", test_data) -# conn.commit() - -# # Test fetchmany with different batch sizes -# cursor.execute("SELECT id, data FROM #pytest_empty_batch ORDER BY id") - -# # Fetch in batches of 2 -# batch1 = cursor.fetchall() -# print(batch1) - - -# Drop & recreate table -cursor.execute("IF OBJECT_ID('dbo.money_test', 'U') IS NOT NULL DROP TABLE dbo.money_test;") -conn.commit() - -cursor.execute(""" - CREATE TABLE money_test ( - id INT IDENTITY PRIMARY KEY, - m MONEY, - sm SMALLMONEY, - d DECIMAL(19,4), - n NUMERIC(10,4) - ) -""") -conn.commit() - -# Insert valid rows covering MONEY & SMALLMONEY ranges -cursor.execute(""" - INSERT INTO money_test (m, sm, d, n) VALUES - -- Max values - (922337203685477.5807, 214748.3647, 9999999999999.9999, 1234.5678), - -- Min values - (-922337203685477.5808, -214748.3648, -9999999999999.9999, -1234.5678), - -- Typical mid values - (1234567.8901, 12345.6789, 42.4242, 3.1415), - -- Nulls - (NULL, NULL, NULL, NULL) -""") -conn.commit() - -# Fetch rows one by one -cursor.execute("SELECT m, sm, d, n FROM money_test ORDER BY id") - -while True: - row = cursor.fetchone() - if not row: - break - print("Row:", row) - for idx, col in enumerate(row, 1): - print(f" col{idx}: {col!r} ({type(col)})") - -# Roundtrip check with Decimal parameters -cursor.execute( - "INSERT INTO money_test (m, sm, d, n) VALUES (?, ?, ?, ?)", - ( - decimal.Decimal("123.4567"), - decimal.Decimal("99.9999"), - decimal.Decimal("42.4242"), - decimal.Decimal("3.1415"), - ), -) -conn.commit() +cursor.execute("SELECT database_id, name from sys.databases;") +rows = cursor.fetchall() -cursor.execute("SELECT TOP 1 m, sm, d, n FROM money_test ORDER BY id DESC") -row = cursor.fetchone() -print("Inserted decimal roundtrip ->", row, [type(c) for c in row]) +for row in rows: + print(f"Database ID: {row[0]}, Name: {row[1]}") cursor.close() conn.close() \ No newline at end of file diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index c1d8819cc..77af5fc57 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -287,7 +287,7 @@ def _map_sql_type(self, param, parameters_list, i): # Detect MONEY / SMALLMONEY range if -214748.3648 <= param <= 214748.3647: # smallmoney - parameters_list[i] = str(param) # let SQL Server handle rounding to 4 decimals + parameters_list[i] = str(param) return ( ddbc_sql_const.SQL_VARCHAR.value, ddbc_sql_const.SQL_C_CHAR.value, @@ -316,19 +316,6 @@ def _map_sql_type(self, param, parameters_list, i): False, ) - - # if isinstance(param, decimal.Decimal): - # parameters_list[i] = self._get_numeric_data( - # param - # ) # Replace the parameter with the dictionary - # return ( - # ddbc_sql_const.SQL_NUMERIC.value, - # ddbc_sql_const.SQL_C_NUMERIC.value, - # parameters_list[i].precision, - # parameters_list[i].scale, - # False, - # ) - if isinstance(param, str): if ( param.startswith("POINT") diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index 00c8a7102..d947da2fe 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -474,13 +474,6 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, decimalPtr->precision = decimalParam.precision; decimalPtr->scale = decimalParam.scale; decimalPtr->sign = decimalParam.sign; - // // Convert the integer decimalParam.val to char array - // std::memset(static_cast(decimalPtr->val), 0, sizeof(decimalPtr->val)); - // std::memcpy(static_cast(decimalPtr->val), - // reinterpret_cast(&decimalParam.val), - // sizeof(decimalParam.val)); - - // Zero out the byte array std::memset(decimalPtr->val, 0, sizeof(decimalPtr->val)); @@ -491,9 +484,6 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, scaledVal >>= 8; } dataPtr = static_cast(decimalPtr); - // TODO: Remove these lines - //strLenOrIndPtr = AllocateParamBuffer(paramBuffers); - //*strLenOrIndPtr = sizeof(SQL_NUMERIC_STRUCT); break; } case SQL_C_GUID: { @@ -1921,31 +1911,6 @@ SQLRETURN SQLGetData_wrap(SqlHandlePtr StatementHandle, SQLUSMALLINT colCount, p } break; } - // case SQL_DECIMAL: - // case SQL_NUMERIC: { - // SQLCHAR numericStr[MAX_DIGITS_IN_NUMERIC] = {0}; - // SQLLEN indicator; - // ret = SQLGetData_ptr(hStmt, i, SQL_C_CHAR, numericStr, sizeof(numericStr), &indicator); - - // if (SQL_SUCCEEDED(ret)) { - // try{ - // // Convert numericStr to py::decimal.Decimal and append to row - // row.append(py::module_::import("decimal").attr("Decimal")( - // std::string(reinterpret_cast(numericStr), indicator))); - // } catch (const py::error_already_set& e) { - // // If the conversion fails, append None - // LOG("Error converting to decimal: {}", e.what()); - // row.append(py::none()); - // } - // } - // else { - // LOG("Error retrieving data for column - {}, data type - {}, SQLGetData return " - // "code - {}. Returning NULL value instead", - // i, dataType, ret); - // row.append(py::none()); - // } - // break; - // } case SQL_DECIMAL: case SQL_NUMERIC: { SQLCHAR numericStr[MAX_DIGITS_IN_NUMERIC] = {0}; From 84d03bc60228f58301ccb543f9fd0bc85d4903e1 Mon Sep 17 00:00:00 2001 From: gargsaumya Date: Thu, 11 Sep 2025 17:32:39 +0530 Subject: [PATCH 4/4] review comment --- mssql_python/cursor.py | 8 ++++++-- mssql_python/pybind/ddbc_bindings.cpp | 14 +++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 77af5fc57..fe9cc4356 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -20,6 +20,10 @@ # Constants for string handling MAX_INLINE_CHAR = 4000 # NVARCHAR/VARCHAR inline limit; this triggers NVARCHAR(MAX)/VARCHAR(MAX) + DAE +SMALLMONEY_MIN = decimal.Decimal('-214748.3648') +SMALLMONEY_MAX = decimal.Decimal('214748.3647') +MONEY_MIN = decimal.Decimal('-922337203685477.5808') +MONEY_MAX = decimal.Decimal('922337203685477.5807') class Cursor: """ @@ -285,7 +289,7 @@ def _map_sql_type(self, param, parameters_list, i): if isinstance(param, decimal.Decimal): # Detect MONEY / SMALLMONEY range - if -214748.3648 <= param <= 214748.3647: + if SMALLMONEY_MIN <= param <= SMALLMONEY_MAX: # smallmoney parameters_list[i] = str(param) return ( @@ -295,7 +299,7 @@ def _map_sql_type(self, param, parameters_list, i): 0, False, ) - elif -922337203685477.5808 <= param <= 922337203685477.5807: + elif MONEY_MIN <= param <= MONEY_MAX: # money parameters_list[i] = str(param) return ( diff --git a/mssql_python/pybind/ddbc_bindings.cpp b/mssql_python/pybind/ddbc_bindings.cpp index d947da2fe..0fc56fd97 100644 --- a/mssql_python/pybind/ddbc_bindings.cpp +++ b/mssql_python/pybind/ddbc_bindings.cpp @@ -474,15 +474,11 @@ SQLRETURN BindParameters(SQLHANDLE hStmt, const py::list& params, decimalPtr->precision = decimalParam.precision; decimalPtr->scale = decimalParam.scale; decimalPtr->sign = decimalParam.sign; - // Zero out the byte array - std::memset(decimalPtr->val, 0, sizeof(decimalPtr->val)); - - // Encode val into little-endian byte array - uint64_t scaledVal = decimalParam.val; - for (size_t i = 0; i < sizeof(decimalPtr->val) && scaledVal > 0; i++) { - decimalPtr->val[i] = static_cast(scaledVal & 0xFF); - scaledVal >>= 8; - } + // Convert the integer decimalParam.val to char array + std::memset(static_cast(decimalPtr->val), 0, sizeof(decimalPtr->val)); + std::memcpy(static_cast(decimalPtr->val), + reinterpret_cast(&decimalParam.val), + sizeof(decimalParam.val)); dataPtr = static_cast(decimalPtr); break; }