Hello community, here is the log from the commit of package python-pyodbc for openSUSE:Factory checked in at 2019-03-11 11:17:53 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/python-pyodbc (Old) and /work/SRC/openSUSE:Factory/.python-pyodbc.new.28833 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-pyodbc" Mon Mar 11 11:17:53 2019 rev:4 rq:683721 version:4.0.26 Changes: -------- --- /work/SRC/openSUSE:Factory/python-pyodbc/python-pyodbc.changes 2019-02-02 21:50:03.539938732 +0100 +++ /work/SRC/openSUSE:Factory/.python-pyodbc.new.28833/python-pyodbc.changes 2019-03-11 11:17:57.397284479 +0100 @@ -1,0 +2,9 @@ +Sun Mar 10 16:04:24 UTC 2019 - Dirk Hartmann <[email protected]> + +- Update to version 4.0.26: + * Issue #506 uncovered a potentially serious error where + Unicode strings may not get a NULL terminator when being converted. + * Issue #504 was a double decref in the error return path of executemany. + + +------------------------------------------------------------------- Old: ---- pyodbc-4.0.25.tar.gz New: ---- pyodbc-4.0.26.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ python-pyodbc.spec ++++++ --- /var/tmp/diff_new_pack.dhwPlY/_old 2019-03-11 11:17:58.673284019 +0100 +++ /var/tmp/diff_new_pack.dhwPlY/_new 2019-03-11 11:17:58.673284019 +0100 @@ -18,7 +18,7 @@ %{?!python_module:%define python_module() python-%{**} python3-%{**}} Name: python-pyodbc -Version: 4.0.25 +Version: 4.0.26 Release: 0 Summary: Python ODBC API License: MIT ++++++ pyodbc-4.0.25.tar.gz -> pyodbc-4.0.26.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/PKG-INFO new/pyodbc-4.0.26/PKG-INFO --- old/pyodbc-4.0.25/PKG-INFO 2018-12-14 03:56:00.000000000 +0100 +++ new/pyodbc-4.0.26/PKG-INFO 2019-02-23 20:17:56.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyodbc -Version: 4.0.25 +Version: 4.0.26 Summary: DB API Module for ODBC Home-page: https://github.com/mkleehammer/pyodbc Author: Michael Kleehammer diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/pyodbc.egg-info/PKG-INFO new/pyodbc-4.0.26/pyodbc.egg-info/PKG-INFO --- old/pyodbc-4.0.25/pyodbc.egg-info/PKG-INFO 2018-12-14 03:55:58.000000000 +0100 +++ new/pyodbc-4.0.26/pyodbc.egg-info/PKG-INFO 2019-02-23 20:17:55.000000000 +0100 @@ -1,6 +1,6 @@ Metadata-Version: 1.1 Name: pyodbc -Version: 4.0.25 +Version: 4.0.26 Summary: DB API Module for ODBC Home-page: https://github.com/mkleehammer/pyodbc Author: Michael Kleehammer diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/src/connection.cpp new/pyodbc-4.0.26/src/connection.cpp --- old/pyodbc-4.0.25/src/connection.cpp 2018-09-28 20:47:41.000000000 +0200 +++ new/pyodbc-4.0.26/src/connection.cpp 2019-02-23 19:31:35.000000000 +0100 @@ -1259,6 +1259,45 @@ Py_RETURN_NONE; } +static char conv_get_doc[] = + "get_output_converter(sqltype) --> <class 'function'>\n" + "\n" + "Get the output converter function that was registered with\n" + "add_output_converter. It is safe to call if no converter is\n" + "registered for the type (returns None).\n" + "\n" + "sqltype\n" + " The integer SQL type value being converted, which can be one of the defined\n" + " standard constants (e.g. pyodbc.SQL_VARCHAR) or a database-specific value\n" + " (e.g. -151 for the SQL Server 2008 geometry data type).\n" + ; + +static PyObject* _get_converter(PyObject* self, SQLSMALLINT sqltype) +{ + Connection* cnxn = (Connection*)self; + + if (cnxn->conv_count) + { + for (int i = 0; i < cnxn->conv_count; i++) + { + if (cnxn->conv_types[i] == sqltype) + { + return cnxn->conv_funcs[i]; + } + } + } + Py_RETURN_NONE; +} + +static PyObject* Connection_conv_get(PyObject* self, PyObject* args) +{ + int sqltype; + if (!PyArg_ParseTuple(args, "i", &sqltype)) + return 0; + + return _get_converter(self, (SQLSMALLINT)sqltype); +} + static void NormalizeCodecName(const char* src, char* dest, size_t cbDest) { // Copies the codec name to dest, lowercasing it and replacing underscores with dashes. @@ -1564,6 +1603,7 @@ { "getinfo", Connection_getinfo, METH_VARARGS, getinfo_doc }, { "add_output_converter", Connection_conv_add, METH_VARARGS, conv_add_doc }, { "remove_output_converter", Connection_conv_remove, METH_VARARGS, conv_remove_doc }, + { "get_output_converter", Connection_conv_get, METH_VARARGS, conv_get_doc }, { "clear_output_converters", Connection_conv_clear, METH_NOARGS, conv_clear_doc }, { "setdecoding", (PyCFunction)Connection_setdecoding, METH_VARARGS|METH_KEYWORDS, setdecoding_doc }, { "setencoding", (PyCFunction)Connection_setencoding, METH_VARARGS|METH_KEYWORDS, 0 }, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/src/params.cpp new/pyodbc-4.0.26/src/params.cpp --- old/pyodbc-4.0.25/src/params.cpp 2018-12-14 03:52:58.000000000 +0100 +++ new/pyodbc-4.0.26/src/params.cpp 2019-02-23 19:31:35.000000000 +0100 @@ -1813,12 +1813,14 @@ // "schema change" or conversion error. Try again on next batch. rowptr--; Py_XDECREF(colseq); + colseq = 0; // Finish this batch of rows and attempt to execute before starting another. goto DoExecute; } } rows_converted++; Py_XDECREF(colseq); + colseq = 0; r++; if ( r >= rowcount ) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/src/textenc.cpp new/pyodbc-4.0.26/src/textenc.cpp --- old/pyodbc-4.0.25/src/textenc.cpp 2017-10-20 21:32:12.000000000 +0200 +++ new/pyodbc-4.0.26/src/textenc.cpp 2019-02-23 19:31:35.000000000 +0100 @@ -3,6 +3,66 @@ #include "wrapper.h" #include "textenc.h" + +static PyObject* nulls = PyBytes_FromStringAndSize("\0\0\0\0", 4); + + +void SQLWChar::init(PyObject* src, const TextEnc& enc) +{ + // Initialization code common to all of the constructors. + + if (src == 0 || src == Py_None) + { + psz = 0; + isNone = true; + return; + } + + isNone = false; + + // If there are optimized encodings that don't require a temporary object, use them. +#if PY_MAJOR_VERSION < 3 + if (enc.optenc == OPTENC_RAW && PyString_Check(src)) + { + psz = (SQLWCHAR*)PyString_AS_STRING(src); + return; + } +#endif + +#if PY_MAJOR_VERSION >= 3 + if (enc.optenc == OPTENC_UTF8 && PyUnicode_Check(src)) + { + psz = (SQLWCHAR*)PyUnicode_AsUTF8(src); + return; + } +#endif + + PyObject* pb = PyUnicode_AsEncodedString(src, enc.name, "strict"); + if (pb) + { + // Careful: Some encodings don't return bytes. + + if (!PyBytes_Check(pb)) + { + // REVIEW: Error or just return null? + psz = 0; + Py_DECREF(pb); + return; + } + + PyBytes_Concat(&pb, nulls); + if (!pb) + { + psz = 0; + return; + } + + psz = (SQLWCHAR*)PyBytes_AS_STRING(pb); + bytes.Attach(pb); + } +} + + PyObject* TextEnc::Encode(PyObject* obj) const { #if PY_MAJOR_VERSION < 3 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/src/textenc.h new/pyodbc-4.0.26/src/textenc.h --- old/pyodbc-4.0.25/src/textenc.h 2018-09-28 20:42:45.000000000 +0200 +++ new/pyodbc-4.0.26/src/textenc.h 2019-02-23 19:31:35.000000000 +0100 @@ -83,8 +83,20 @@ // // Note: This does *not* increment the refcount! + // IMPORTANT: I've made the conscious decision *not* to determine the character count. If + // we only had to follow the ODBC specification, it would simply be the number of + // characters in the string and would be the bytelen / 2. The problem is drivers that + // don't follow the specification and expect things like UTF-8. What length do these + // drivers expect? Very, very likely they want the number of *bytes*, not the actual + // number of characters. I'm simply going to null terminate and pass SQL_NTS. + // + // This is a performance penalty when using utf16 since we have to copy the string just to + // add the null terminator bytes, but we don't use it very often. If this becomes a + // bottleneck, we'll have to revisit this design. + SQLWCHAR* psz; bool isNone; + Object bytes; // A temporary object holding the decoded bytes if we can't use a pointer into the original // object. @@ -108,42 +120,6 @@ init(src, enc); } - void init(PyObject* src, const TextEnc& enc) - { - if (src == 0 || src == Py_None) - { - psz = 0; - isNone = true; - return; - } - - isNone = false; - - // If there are optimized encodings that don't require a temporary object, use them. - #if PY_MAJOR_VERSION < 3 - if (enc.optenc == OPTENC_RAW && PyString_Check(src)) - { - psz = (SQLWCHAR*)PyString_AS_STRING(src); - return; - } - #endif - - #if PY_MAJOR_VERSION >= 3 - if (enc.optenc == OPTENC_UTF8 && PyUnicode_Check(src)) - { - psz = (SQLWCHAR*)PyUnicode_AsUTF8(src); - return; - } - #endif - - bytes.Attach(PyUnicode_AsEncodedString(src, enc.name, "strict")); - if (bytes) - { - // Careful: Some encodings don't return bytes. Don't use AS_STRING macro. - psz = (SQLWCHAR*)PyBytes_AsString(bytes.Get()); - } - } - bool isValidOrNone() { // Returns true if this object is a valid string *or* None. @@ -156,6 +132,8 @@ } private: + void init(PyObject* src, const TextEnc& enc); + SQLWChar(const SQLWChar&) {} void operator=(const SQLWChar&) {} }; diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/tests2/sqlservertests.py new/pyodbc-4.0.26/tests2/sqlservertests.py --- old/pyodbc-4.0.25/tests2/sqlservertests.py 2018-12-14 03:52:58.000000000 +0100 +++ new/pyodbc-4.0.26/tests2/sqlservertests.py 2019-02-23 19:31:35.000000000 +0100 @@ -36,6 +36,12 @@ from warnings import warn from testutils import * +# Some tests have fallback code for known driver issues. +# Change this value to False to bypass the fallback code, e.g., to see +# if a newer version of the driver has fixed the underlying issue. +# +handle_known_issues = True + _TESTSTR = '0123456789-abcdefghijklmnopqrstuvwxyz-' def _generate_test_string(length): @@ -87,6 +93,54 @@ elif type_name == 'freetds': return ('tdsodbc' in driver_name) + def handle_known_issues_for(self, type_name, print_reminder=False): + """ + Checks driver `type_name` and "killswitch" variable `handle_known_issues` to see if + known issue handling should be bypassed. Optionally prints a reminder message to + help identify tests that previously had issues but may have been fixed by a newer + version of the driver. + + Usage examples: + + # 1. print reminder at beginning of test (before any errors can occur) + # + def test_some_feature(self): + self.handle_known_issues_for('freetds', print_reminder=True) + # (continue with test code) + + # 2. conditional execution of fallback code + # + try: + # (some test code) + except pyodbc.DataError: + if self.handle_known_issues_for('freetds'): + # FREETDS_KNOWN_ISSUE + # + # (fallback code to work around exception) + else: + raise + """ + if self.driver_type_is(type_name): + if handle_known_issues: + return True + else: + if print_reminder: + print("Known issue handling is disabled. Does this test still fail?") + return False + + def driver_type_is(self, type_name): + recognized_types = { + 'msodbcsql': '(Microsoft) ODBC Driver xx for SQL Server', + 'freetds': 'FreeTDS ODBC', + } + if not type_name in recognized_types.keys(): + raise KeyError('"{0}" is not a recognized driver type: {1}'.format(type_name, list(recognized_types.keys()))) + driver_name = self.cnxn.getinfo(pyodbc.SQL_DRIVER_NAME).lower() + if type_name == 'msodbcsql': + return ('msodbcsql' in driver_name) or ('sqlncli' in driver_name) or ('sqlsrv32.dll' == driver_name) + elif type_name == 'freetds': + return ('tdsodbc' in driver_name) + def get_sqlserver_version(self): """ Returns the major version: 8-->2000, 9-->2005, 10-->2008 @@ -1651,6 +1705,10 @@ # pyodbc supports queries with table valued parameters in sql server # + if self.handle_known_issues_for('freetds', print_reminder=True): + warn('FREETDS_KNOWN_ISSUE - test_tvp: test cancelled.') + return + # (Don't use "if exists" since older SQL Servers don't support it.) try: self.cursor.execute("drop procedure SelectTVP") diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/pyodbc-4.0.25/tests3/sqlservertests.py new/pyodbc-4.0.26/tests3/sqlservertests.py --- old/pyodbc-4.0.25/tests3/sqlservertests.py 2018-12-14 03:52:58.000000000 +0100 +++ new/pyodbc-4.0.26/tests3/sqlservertests.py 2019-02-23 19:55:23.000000000 +0100 @@ -1346,15 +1346,19 @@ def test_output_conversion(self): - def convert(value): + def convert1(value): # The value is the raw bytes (as a bytes object) read from the # database. We'll simply add an X at the beginning at the end. return 'X' + value.decode('latin1') + 'X' + def convert2(value): + # Same as above, but add a Y at the beginning at the end. + return 'Y' + value.decode('latin1') + 'Y' + self.cursor.execute("create table t1(n int, v varchar(10))") self.cursor.execute("insert into t1 values (1, '123.45')") - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert) + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1) value = self.cursor.execute("select v from t1").fetchone()[0] self.assertEqual(value, 'X123.45X') @@ -1364,7 +1368,7 @@ self.assertEqual(value, '123.45') # Same but clear using remove_output_converter. - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert) + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1) value = self.cursor.execute("select v from t1").fetchone()[0] self.assertEqual(value, 'X123.45X') @@ -1372,15 +1376,44 @@ value = self.cursor.execute("select v from t1").fetchone()[0] self.assertEqual(value, '123.45') - # And lastly, clear by passing None for the converter. - self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert) + # Clear via add_output_converter, passing None for the converter function. + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1) value = self.cursor.execute("select v from t1").fetchone()[0] self.assertEqual(value, 'X123.45X') self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, None) value = self.cursor.execute("select v from t1").fetchone()[0] self.assertEqual(value, '123.45') - + + # retrieve and temporarily replace converter (get_output_converter) + # + # case_1: converter already registered + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert1) + value = self.cursor.execute("select v from t1").fetchone()[0] + self.assertEqual(value, 'X123.45X') + prev_converter = self.cnxn.get_output_converter(pyodbc.SQL_VARCHAR) + self.assertNotEqual(prev_converter, None) + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert2) + value = self.cursor.execute("select v from t1").fetchone()[0] + self.assertEqual(value, 'Y123.45Y') + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, prev_converter) + value = self.cursor.execute("select v from t1").fetchone()[0] + self.assertEqual(value, 'X123.45X') + # + # case_2: no converter already registered + self.cnxn.clear_output_converters() + value = self.cursor.execute("select v from t1").fetchone()[0] + self.assertEqual(value, '123.45') + prev_converter = self.cnxn.get_output_converter(pyodbc.SQL_VARCHAR) + self.assertEqual(prev_converter, None) + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, convert2) + value = self.cursor.execute("select v from t1").fetchone()[0] + self.assertEqual(value, 'Y123.45Y') + self.cnxn.add_output_converter(pyodbc.SQL_VARCHAR, prev_converter) + value = self.cursor.execute("select v from t1").fetchone()[0] + self.assertEqual(value, '123.45') + + def test_too_large(self): """Ensure error raised if insert fails due to truncation""" value = 'x' * 1000 @@ -1550,6 +1583,26 @@ assert row.type_name == 'varchar' assert row.column_size == 4, row.column_size + # <test null termination fix (issue #506)> + for i in range(8, 16): + table_name = 'pyodbc_89abcdef'[:i] + + self.cursor.execute("""\ + BEGIN TRY + DROP TABLE {0}; + END TRY + BEGIN CATCH + END CATCH + CREATE TABLE {0} (id INT PRIMARY KEY); + """.format(table_name)) + + col_count = len([col.column_name for col in self.cursor.columns(table_name)]) + # print('table [{}] ({} characters): {} columns{}'.format(table_name, i, col_count, ' <-' if col_count == 0 else '')) + self.assertEqual(col_count, 1) + + self.cursor.execute("DROP TABLE {};".format(table_name)) + # </test null termination fix (issue #506)> + def test_cancel(self): # I'm not sure how to reliably cause a hang to cancel, so for now we'll settle with # making sure SQLCancel is called correctly. @@ -1578,6 +1631,10 @@ # pyodbc supports queries with table valued parameters in sql server # + if self.handle_known_issues_for('freetds', print_reminder=True): + warn('FREETDS_KNOWN_ISSUE - test_tvp: test cancelled.') + return + # (Don't use "if exists" since older SQL Servers don't support it.) try: self.cursor.execute("drop procedure SelectTVP")
