Skip to content

Commit

Permalink
Very large integers and non-numeric floats as parameters (#671)
Browse files Browse the repository at this point in the history
* checks when converting int and float parameters

* make sure test table exists after exception

* test all non-numeric floats

* negative NaN is meaningless
  • Loading branch information
keitherskine authored Feb 8, 2020
1 parent 4957828 commit 1aef4bc
Show file tree
Hide file tree
Showing 3 changed files with 57 additions and 16 deletions.
39 changes: 23 additions & 16 deletions src/params.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -858,36 +858,36 @@ 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;
info.StrLen_or_Ind = 8;
}
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
Expand All @@ -898,33 +898,40 @@ 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;
info.StrLen_or_Ind = 8;
}
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;
Expand Down
17 changes: 17 additions & 0 deletions tests2/sqlservertests.py
Original file line number Diff line number Diff line change
Expand Up @@ -891,6 +891,15 @@ 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, bigger than an 8 byte int can contain
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, [])

def test_float(self):
value = 1234.567
self.cursor.execute("create table t1(n float)")
Expand All @@ -912,6 +921,14 @@ def test_negative_float(self):
result = self.cursor.execute("select n from t1").fetchone()[0]
self.assertEqual(value, result)

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')):
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
Expand Down
17 changes: 17 additions & 0 deletions tests3/sqlservertests.py
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,15 @@ 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, bigger than an 8 byte int can contain
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, [])

def test_float(self):
value = 1234.567
self.cursor.execute("create table t1(n float)")
Expand All @@ -804,6 +813,14 @@ def test_negative_float(self):
result = self.cursor.execute("select n from t1").fetchone()[0]
self.assertEqual(value, result)

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')):
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
#
Expand Down

0 comments on commit 1aef4bc

Please sign in to comment.