From e339c4e5bb045890d506d1ba84c4dcae4c44d6a0 Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Tue, 11 Nov 2025 17:39:35 +0530 Subject: [PATCH 1/4] fix for length issue for Decimal data type --- mssql_python/cursor.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 62572642a..8b84fb83c 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1717,9 +1717,19 @@ def _compute_column_type(self, column): sample_value = None for v in non_nulls: - if not sample_value or ( - hasattr(v, "__len__") and len(v) > len(sample_value) - ): + if not sample_value: + sample_value = v + elif hasattr(v, "__len__") and hasattr(sample_value, "__len__") and len(v) > len(sample_value): + # For objects with length (strings, bytes, etc.), prefer the longer one + sample_value = v + elif isinstance(v, decimal.Decimal) and isinstance(sample_value, decimal.Decimal): + # For Decimal objects, prefer the one with higher precision (more digits) + v_tuple = v.as_tuple() + sample_tuple = sample_value.as_tuple() + if len(v_tuple.digits) > len(sample_tuple.digits): + sample_value = v + elif isinstance(v, decimal.Decimal) and not isinstance(sample_value, decimal.Decimal): + # If comparing Decimal to non-Decimal, prefer Decimal for better type inference sample_value = v return sample_value, None, None From 003ab1481197b36bf0d7370dbce2a48b065db4af Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Tue, 11 Nov 2025 18:47:37 +0530 Subject: [PATCH 2/4] added test case for Decimal data type --- tests/test_004_cursor.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 83f61e066..0b90cd73e 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -1663,6 +1663,22 @@ def test_executemany_empty_parameter_list(cursor, db_connection): db_connection.commit() +def test_executemany_Decimal_list(cursor, db_connection): + """Test executemany with an empty parameter list.""" + try: + cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(30, 20))") + data = [(decimal.Decimal('35.1128407822'),), (decimal.Decimal('40000.5640564065406'),)] + cursor.executemany("INSERT INTO #pytest_empty_params VALUES (?)", data) + db_connection.commit() + + cursor.execute("SELECT COUNT(*) FROM #pytest_empty_params") + count = cursor.fetchone()[0] + assert count == 0 + finally: + cursor.execute("DROP TABLE IF EXISTS #pytest_empty_params") + db_connection.commit() + + def test_nextset(cursor): """Test nextset""" cursor.execute("SELECT * FROM #pytest_all_data_types WHERE id = 1;") From 54726d0209bc42c1cf19b70a9d052042d81d2c21 Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Thu, 13 Nov 2025 14:44:34 +0530 Subject: [PATCH 3/4] Resolving review comments --- mssql_python/cursor.py | 27 +++++++++++++++++---- tests/test_004_cursor.py | 52 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 72 insertions(+), 7 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index 8b84fb83c..d84a5b50f 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1719,14 +1719,31 @@ def _compute_column_type(self, column): for v in non_nulls: if not sample_value: sample_value = v - elif hasattr(v, "__len__") and hasattr(sample_value, "__len__") and len(v) > len(sample_value): - # For objects with length (strings, bytes, etc.), prefer the longer one - sample_value = v + elif isinstance(v, (str, bytes, bytearray)) and isinstance(sample_value, (str, bytes, bytearray)): + # For string/binary objects, prefer the longer one + # Use safe length comparison to avoid exceptions from custom __len__ implementations + try: + if len(v) > len(sample_value): + sample_value = v + except (TypeError, ValueError, AttributeError): + # If length comparison fails, keep the current sample_value + pass elif isinstance(v, decimal.Decimal) and isinstance(sample_value, decimal.Decimal): - # For Decimal objects, prefer the one with higher precision (more digits) + # For Decimal objects, prefer the one that requires higher precision or scale v_tuple = v.as_tuple() sample_tuple = sample_value.as_tuple() - if len(v_tuple.digits) > len(sample_tuple.digits): + + # Calculate precision (total digits) and scale (decimal places) + v_precision = len(v_tuple.digits) + v_scale = max(0, -v_tuple.exponent) if v_tuple.exponent < 0 else 0 + + sample_precision = len(sample_tuple.digits) + sample_scale = max(0, -sample_tuple.exponent) if sample_tuple.exponent < 0 else 0 + + # Prefer the decimal that requires higher precision or scale + # This ensures we can accommodate all values in the column + if (v_precision > sample_precision or + (v_precision == sample_precision and v_scale > sample_scale)): sample_value = v elif isinstance(v, decimal.Decimal) and not isinstance(sample_value, decimal.Decimal): # If comparing Decimal to non-Decimal, prefer Decimal for better type inference diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index 0b90cd73e..bb3e385ac 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -1664,7 +1664,7 @@ def test_executemany_empty_parameter_list(cursor, db_connection): def test_executemany_Decimal_list(cursor, db_connection): - """Test executemany with an empty parameter list.""" + """Test executemany with an decimal parameter list.""" try: cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(30, 20))") data = [(decimal.Decimal('35.1128407822'),), (decimal.Decimal('40000.5640564065406'),)] @@ -1673,7 +1673,55 @@ def test_executemany_Decimal_list(cursor, db_connection): cursor.execute("SELECT COUNT(*) FROM #pytest_empty_params") count = cursor.fetchone()[0] - assert count == 0 + assert count == 2 + finally: + cursor.execute("DROP TABLE IF EXISTS #pytest_empty_params") + db_connection.commit() + + +def test_executemany_DecimalString_list(cursor, db_connection): + """Test executemany with an string of decimal parameter list.""" + try: + cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(30, 20))") + data = [(str(decimal.Decimal('35.1128407822')),), (str(decimal.Decimal('40000.5640564065406')),)] + cursor.executemany("INSERT INTO #pytest_empty_params VALUES (?)", data) + db_connection.commit() + + cursor.execute("SELECT COUNT(*) FROM #pytest_empty_params where val IN (35.1128407822,40000.5640564065406)") + count = cursor.fetchone()[0] + assert count == 2 + finally: + cursor.execute("DROP TABLE IF EXISTS #pytest_empty_params") + db_connection.commit() + + +def test_executemany_DecimalPrecision_list(cursor, db_connection): + """Test executemany with an decimal parameter list.""" + try: + cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(30, 20))") + data = [(decimal.Decimal('35112'),), (decimal.Decimal('35.112'),)] + cursor.executemany("INSERT INTO #pytest_empty_params VALUES (?)", data) + db_connection.commit() + + cursor.execute("SELECT COUNT(*) FROM #pytest_empty_params where val IN (35112,35.112)") + count = cursor.fetchone()[0] + assert count == 2 + finally: + cursor.execute("DROP TABLE IF EXISTS #pytest_empty_params") + db_connection.commit() + + +def test_executemany_Decimal_Batch_List(cursor, db_connection): + """Test executemany with an decimal parameter list.""" + try: + cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(10, 4))") + data = [(decimal.Decimal('1.2345'),), (decimal.Decimal('9999.0000'),)] + cursor.executemany("INSERT INTO #pytest_empty_params VALUES (?)", data) + db_connection.commit() + + cursor.execute("SELECT COUNT(*) FROM #pytest_empty_params where val IN (1.2345,9999.0000)") + count = cursor.fetchone()[0] + assert count == 2 finally: cursor.execute("DROP TABLE IF EXISTS #pytest_empty_params") db_connection.commit() From c055cecda60c30281e3eb9ea2d179d5319d2ff1e Mon Sep 17 00:00:00 2001 From: Subrata Paitandi Date: Thu, 13 Nov 2025 15:50:32 +0530 Subject: [PATCH 4/4] Mix Precision decimal data type validation - fix --- mssql_python/cursor.py | 28 +++++++++++++++++++++++----- tests/test_004_cursor.py | 27 +++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 7 deletions(-) diff --git a/mssql_python/cursor.py b/mssql_python/cursor.py index d84a5b50f..3c6135b7b 100644 --- a/mssql_python/cursor.py +++ b/mssql_python/cursor.py @@ -1733,17 +1733,35 @@ def _compute_column_type(self, column): v_tuple = v.as_tuple() sample_tuple = sample_value.as_tuple() - # Calculate precision (total digits) and scale (decimal places) + # Calculate precision (total significant digits) and scale (decimal places) + # For a number like 0.000123456789, we need precision = 9, scale = 12 + # The precision is the number of significant digits (len(digits)) + # The scale is the number of decimal places needed to represent the number + v_precision = len(v_tuple.digits) - v_scale = max(0, -v_tuple.exponent) if v_tuple.exponent < 0 else 0 + if v_tuple.exponent < 0: + v_scale = -v_tuple.exponent + else: + v_scale = 0 sample_precision = len(sample_tuple.digits) - sample_scale = max(0, -sample_tuple.exponent) if sample_tuple.exponent < 0 else 0 + if sample_tuple.exponent < 0: + sample_scale = -sample_tuple.exponent + else: + sample_scale = 0 + + # For SQL DECIMAL(precision, scale), we need: + # precision >= number of significant digits + # scale >= number of decimal places + # For 0.000123456789: precision needs to be at least 12 (to accommodate 12 decimal places) + # So we need to adjust precision to be at least as large as scale + v_required_precision = max(v_precision, v_scale) + sample_required_precision = max(sample_precision, sample_scale) # Prefer the decimal that requires higher precision or scale # This ensures we can accommodate all values in the column - if (v_precision > sample_precision or - (v_precision == sample_precision and v_scale > sample_scale)): + if (v_required_precision > sample_required_precision or + (v_required_precision == sample_required_precision and v_scale > sample_scale)): sample_value = v elif isinstance(v, decimal.Decimal) and not isinstance(sample_value, decimal.Decimal): # If comparing Decimal to non-Decimal, prefer Decimal for better type inference diff --git a/tests/test_004_cursor.py b/tests/test_004_cursor.py index bed54e126..93bdcf5ff 100644 --- a/tests/test_004_cursor.py +++ b/tests/test_004_cursor.py @@ -1696,7 +1696,7 @@ def test_executemany_DecimalString_list(cursor, db_connection): def test_executemany_DecimalPrecision_list(cursor, db_connection): - """Test executemany with an decimal parameter list.""" + """Test executemany with an decimal Precision parameter list.""" try: cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(30, 20))") data = [(decimal.Decimal('35112'),), (decimal.Decimal('35.112'),)] @@ -1712,7 +1712,7 @@ def test_executemany_DecimalPrecision_list(cursor, db_connection): def test_executemany_Decimal_Batch_List(cursor, db_connection): - """Test executemany with an decimal parameter list.""" + """Test executemany with an decimal Batch parameter list.""" try: cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(10, 4))") data = [(decimal.Decimal('1.2345'),), (decimal.Decimal('9999.0000'),)] @@ -1727,6 +1727,29 @@ def test_executemany_Decimal_Batch_List(cursor, db_connection): db_connection.commit() +def test_executemany_DecimalMix_List(cursor, db_connection): + """Test executemany with an Decimal Mixed precision parameter list.""" + try: + cursor.execute("CREATE TABLE #pytest_empty_params (val DECIMAL(30, 20))") + # Test with mixed precision and scale requirements + data = [ + (decimal.Decimal('1.2345'),), # 5 digits, 4 decimal places + (decimal.Decimal('999999.12'),), # 8 digits, 2 decimal places + (decimal.Decimal('0.000123456789'),), # 12 digits, 12 decimal places + (decimal.Decimal('1234567890'),), # 10 digits, 0 decimal places + (decimal.Decimal('99.999999999'),) # 11 digits, 9 decimal places + ] + cursor.executemany("INSERT INTO #pytest_empty_params VALUES (?)", data) + db_connection.commit() + + cursor.execute("SELECT COUNT(*) FROM #pytest_empty_params") + count = cursor.fetchone()[0] + assert count == 5 + finally: + cursor.execute("DROP TABLE IF EXISTS #pytest_empty_params") + db_connection.commit() + + def test_nextset(cursor): """Test nextset""" cursor.execute("SELECT * FROM #pytest_all_data_types WHERE id = 1;")