From 2b76607e3ca72d1207f36c15df5b29031166ac8f Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Mon, 6 Jan 2020 00:22:41 -0600 Subject: [PATCH 1/4] checks when converting int and float parameters --- src/params.cpp | 39 +++++++++++++++++++++++---------------- tests2/sqlservertests.py | 15 +++++++++++++++ tests3/sqlservertests.py | 15 +++++++++++++++ 3 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/params.cpp b/src/params.cpp index c14bb4b4..20548c2d 100644 --- a/src/params.cpp +++ b/src/params.cpp @@ -858,21 +858,23 @@ static bool GetTimeInfo(Cursor* cur, Py_ssize_t index, PyObject* param, ParamInf } -inline bool NeedsBigInt(PyObject* p) +inline bool NeedsBigInt(long long ll) { // NOTE: Smallest 32-bit int should be -214748368 but the MS compiler v.1900 AMD64 // says that (10 < -2147483648). Perhaps I miscalculated the minimum? - long long ll = PyLong_AsLongLong(p); return ll < -2147483647 || ll > 2147483647; } #if PY_MAJOR_VERSION < 3 static bool GetIntInfo(Cursor* cur, Py_ssize_t index, PyObject* param, ParamInfo& info, bool isTVP) { - if (isTVP || NeedsBigInt(param)) - { - info.Data.i64 = (INT64)PyLong_AsLongLong(param); + long long value = PyLong_AsLongLong(param); + if (PyErr_Occurred()) + return false; + if (isTVP || NeedsBigInt(value)) + { + info.Data.i64 = (INT64)value; info.ValueType = SQL_C_SBIGINT; info.ParameterType = SQL_BIGINT; info.ParameterValuePtr = &info.Data.i64; @@ -880,14 +882,12 @@ static bool GetIntInfo(Cursor* cur, Py_ssize_t index, PyObject* param, ParamInfo } else { - info.Data.i32 = (int)PyLong_AsLong(param); - + info.Data.i32 = (int)value; info.ValueType = SQL_C_LONG; info.ParameterType = SQL_INTEGER; info.ParameterValuePtr = &info.Data.i32; info.StrLen_or_Ind = 4; } - return true; } #endif @@ -898,10 +898,13 @@ static bool GetLongInfo(Cursor* cur, Py_ssize_t index, PyObject* param, ParamInf // Unfortunately this may mean that we end up with two execution plans for the same SQL. // We could use SQLDescribeParam but that's kind of expensive. - if (isTVP || NeedsBigInt(param)) - { - info.Data.i64 = (INT64)PyLong_AsLongLong(param); + long long value = PyLong_AsLongLong(param); + if (PyErr_Occurred()) + return false; + if (isTVP || NeedsBigInt(value)) + { + info.Data.i64 = (INT64)value; info.ValueType = SQL_C_SBIGINT; info.ParameterType = SQL_BIGINT; info.ParameterValuePtr = &info.Data.i64; @@ -909,22 +912,26 @@ static bool GetLongInfo(Cursor* cur, Py_ssize_t index, PyObject* param, ParamInf } else { - info.Data.i32 = (int)PyLong_AsLong(param); - + info.Data.i32 = (int)value; info.ValueType = SQL_C_LONG; info.ParameterType = SQL_INTEGER; info.ParameterValuePtr = &info.Data.i32; info.StrLen_or_Ind = 4; } - return true; } static bool GetFloatInfo(Cursor* cur, Py_ssize_t index, PyObject* param, ParamInfo& info) { - // TODO: Overflow? - info.Data.dbl = PyFloat_AsDouble(param); + // Python floats are usually numeric values, but can also be "Infinity" or "NaN". + // https://docs.python.org/3/library/functions.html#float + // PyFloat_AsDouble() does not generate an error for Infinity/NaN, and it is not + // easy to check for those values. Typically, the database will reject them. + double value = PyFloat_AsDouble(param); + if (PyErr_Occurred()) + return false; + info.Data.dbl = value; info.ValueType = SQL_C_DOUBLE; info.ParameterType = SQL_DOUBLE; info.ParameterValuePtr = &info.Data.dbl; diff --git a/tests2/sqlservertests.py b/tests2/sqlservertests.py index e00d15fc..bcd57c7f 100755 --- a/tests2/sqlservertests.py +++ b/tests2/sqlservertests.py @@ -891,6 +891,14 @@ def test_bigint(self): result = self.cursor.execute("select d from t1").fetchone()[0] self.assertEqual(result, input) + def test_overflow_int(self): + # python allows integers of any size + input = 9999999999999999999999999999999999999 + self.cursor.execute("create table t1(d bigint)") + self.assertRaises(OverflowError, self.cursor.execute, "insert into t1 values (?)", input) + result = self.cursor.execute("select * from t1").fetchall() + self.assertEqual(result, []) + def test_float(self): value = 1234.567 self.cursor.execute("create table t1(n float)") @@ -912,6 +920,13 @@ def test_negative_float(self): result = self.cursor.execute("select n from t1").fetchone()[0] self.assertEqual(value, result) + def test_overflow_float(self): + input = float('infinity') + self.cursor.execute("create table t1(d float)") + self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) + result = self.cursor.execute("select * from t1").fetchall() + self.assertEqual(result, []) + # # stored procedures diff --git a/tests3/sqlservertests.py b/tests3/sqlservertests.py index 5a52b732..de29a35b 100644 --- a/tests3/sqlservertests.py +++ b/tests3/sqlservertests.py @@ -783,6 +783,14 @@ def test_bigint(self): result = self.cursor.execute("select d from t1").fetchone()[0] self.assertEqual(result, input) + def test_overflow_int(self): + # python allows integers of any size + input = 9999999999999999999999999999999999999 + self.cursor.execute("create table t1(d bigint)") + self.assertRaises(OverflowError, self.cursor.execute, "insert into t1 values (?)", input) + result = self.cursor.execute("select * from t1").fetchall() + self.assertEqual(result, []) + def test_float(self): value = 1234.567 self.cursor.execute("create table t1(n float)") @@ -804,6 +812,13 @@ def test_negative_float(self): result = self.cursor.execute("select n from t1").fetchone()[0] self.assertEqual(value, result) + def test_overflow_float(self): + input = float('infinity') + self.cursor.execute("create table t1(d float)") + self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) + result = self.cursor.execute("select * from t1").fetchall() + self.assertEqual(result, []) + # # stored procedures # From b82acc56d6726175ca9e3cd58d3720e022abecd5 Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Mon, 6 Jan 2020 00:40:49 -0600 Subject: [PATCH 2/4] make sure test table exists after exception --- tests2/sqlservertests.py | 2 ++ tests3/sqlservertests.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests2/sqlservertests.py b/tests2/sqlservertests.py index bcd57c7f..dccf5a77 100755 --- a/tests2/sqlservertests.py +++ b/tests2/sqlservertests.py @@ -895,6 +895,7 @@ def test_overflow_int(self): # python allows integers of any size input = 9999999999999999999999999999999999999 self.cursor.execute("create table t1(d bigint)") + self.cnxn.commit() self.assertRaises(OverflowError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, []) @@ -923,6 +924,7 @@ def test_negative_float(self): def test_overflow_float(self): input = float('infinity') self.cursor.execute("create table t1(d float)") + self.cnxn.commit() self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, []) diff --git a/tests3/sqlservertests.py b/tests3/sqlservertests.py index de29a35b..232ef2da 100644 --- a/tests3/sqlservertests.py +++ b/tests3/sqlservertests.py @@ -787,6 +787,7 @@ def test_overflow_int(self): # python allows integers of any size input = 9999999999999999999999999999999999999 self.cursor.execute("create table t1(d bigint)") + self.cnxn.commit() self.assertRaises(OverflowError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, []) @@ -815,6 +816,7 @@ def test_negative_float(self): def test_overflow_float(self): input = float('infinity') self.cursor.execute("create table t1(d float)") + self.cnxn.commit() self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, []) From b837380f86d8d558daa469c3e8fdb9927f19e725 Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Mon, 6 Jan 2020 08:46:38 -0600 Subject: [PATCH 3/4] test all non-numeric floats --- tests2/sqlservertests.py | 8 ++++---- tests3/sqlservertests.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests2/sqlservertests.py b/tests2/sqlservertests.py index dccf5a77..5cb9349d 100755 --- a/tests2/sqlservertests.py +++ b/tests2/sqlservertests.py @@ -892,7 +892,7 @@ def test_bigint(self): self.assertEqual(result, input) def test_overflow_int(self): - # python allows integers of any size + # python allows integers of any size, bigger than an 8 byte int can contain input = 9999999999999999999999999999999999999 self.cursor.execute("create table t1(d bigint)") self.cnxn.commit() @@ -921,11 +921,11 @@ def test_negative_float(self): result = self.cursor.execute("select n from t1").fetchone()[0] self.assertEqual(value, result) - def test_overflow_float(self): - input = float('infinity') + def test_non_numeric_float(self): self.cursor.execute("create table t1(d float)") self.cnxn.commit() - self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) + for input in (float('infinity'), float('-infinity'), float('nan'), float('-nan')): + self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, []) diff --git a/tests3/sqlservertests.py b/tests3/sqlservertests.py index 232ef2da..4e4f3659 100644 --- a/tests3/sqlservertests.py +++ b/tests3/sqlservertests.py @@ -784,7 +784,7 @@ def test_bigint(self): self.assertEqual(result, input) def test_overflow_int(self): - # python allows integers of any size + # python allows integers of any size, bigger than an 8 byte int can contain input = 9999999999999999999999999999999999999 self.cursor.execute("create table t1(d bigint)") self.cnxn.commit() @@ -813,11 +813,11 @@ def test_negative_float(self): result = self.cursor.execute("select n from t1").fetchone()[0] self.assertEqual(value, result) - def test_overflow_float(self): - input = float('infinity') + def test_non_numeric_float(self): self.cursor.execute("create table t1(d float)") self.cnxn.commit() - self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) + for input in (float('infinity'), float('-infinity'), float('nan'), float('-nan')): + self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, []) From c1d73bc4a9a5ac2355cd3af6e10b90d4f1c5be7d Mon Sep 17 00:00:00 2001 From: Keith Erskine Date: Mon, 6 Jan 2020 18:50:24 -0600 Subject: [PATCH 4/4] negative NaN is meaningless --- tests2/sqlservertests.py | 2 +- tests3/sqlservertests.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests2/sqlservertests.py b/tests2/sqlservertests.py index 5cb9349d..3ffbbcfb 100755 --- a/tests2/sqlservertests.py +++ b/tests2/sqlservertests.py @@ -924,7 +924,7 @@ def test_negative_float(self): def test_non_numeric_float(self): self.cursor.execute("create table t1(d float)") self.cnxn.commit() - for input in (float('infinity'), float('-infinity'), float('nan'), float('-nan')): + for input in (float('+Infinity'), float('-Infinity'), float('NaN')): self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, []) diff --git a/tests3/sqlservertests.py b/tests3/sqlservertests.py index 4e4f3659..cc1854f3 100644 --- a/tests3/sqlservertests.py +++ b/tests3/sqlservertests.py @@ -816,7 +816,7 @@ def test_negative_float(self): def test_non_numeric_float(self): self.cursor.execute("create table t1(d float)") self.cnxn.commit() - for input in (float('infinity'), float('-infinity'), float('nan'), float('-nan')): + for input in (float('+Infinity'), float('-Infinity'), float('NaN')): self.assertRaises(pyodbc.ProgrammingError, self.cursor.execute, "insert into t1 values (?)", input) result = self.cursor.execute("select * from t1").fetchall() self.assertEqual(result, [])