Skip to content

Commit 63edccd

Browse files
Fix issue where string dtype singleton elsize was accidentally changed (#73)
* Fix issue where string dtype singleton elsize was accidentally changed * Update apt before installing deps
1 parent da315e9 commit 63edccd

3 files changed

Lines changed: 60 additions & 9 deletions

File tree

.github/workflows/ubuntu_build.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,9 @@ jobs:
6969
echo "*** cat /etc/odbc.ini"
7070
cat /etc/odbc.ini
7171
72-
- name: Install libicu dependency
72+
- name: Update apt and install libicu dependency
7373
run: |
74+
sudo apt update
7475
sudo apt install -y libicu-dev
7576
7677
- name: Install ODBC driver for SQL Server

src/npcontainer.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -728,21 +728,30 @@ int coerce_column_desc_types(column_desc &cd, bool unicode, PyArray_Descr *descr
728728
Py_INCREF(descr);
729729

730730
switch (descr->type_num) {
731+
// Since we modify the elsize for string and unicode types, we need to create an entirely
732+
// new descr (to be sure we aren't modifying the global numpy unicode/bytestring descr
733+
// singletons)
731734
case NPY_STRING:
732735
cd.sql_c_type_ = SQL_C_BINARY;
736+
cd.npy_type_descr_ = PyArray_DescrNew(descr);
737+
Py_DECREF(descr);
738+
733739
PyDataType_SET_ELSIZE(
734-
descr,
740+
cd.npy_type_descr_,
735741
static_cast<npy_int>(cd.sql_size_)
736742
);
737-
cd.element_buffer_size_ = PyDataType_ELSIZE(descr);
743+
cd.element_buffer_size_ = PyDataType_ELSIZE(cd.npy_type_descr_);
738744
break;
739745
case NPY_UNICODE:
740746
cd.sql_c_type_ = SQL_C_WCHAR;
747+
cd.npy_type_descr_ = PyArray_DescrNew(descr);
748+
Py_DECREF(descr);
749+
741750
PyDataType_SET_ELSIZE(
742-
descr,
751+
cd.npy_type_descr_,
743752
static_cast<npy_int>(cd.sql_size_)
744753
);
745-
cd.element_buffer_size_ = PyDataType_ELSIZE(descr);
754+
cd.element_buffer_size_ = PyDataType_ELSIZE(cd.npy_type_descr_);
746755
break;
747756
case NPY_INT8:
748757
cd.sql_c_type_ = SQL_C_STINYINT;
@@ -953,7 +962,7 @@ map_column_desc_types(column_desc &cd, bool unicode)
953962
case SQL_BINARY:
954963
case SQL_VARBINARY:
955964
case SQL_LONGVARBINARY:
956-
dtype = PyArray_DescrFromType(NPY_STRING);
965+
dtype = PyArray_DescrNewFromType(NPY_STRING);
957966
if (dtype != NULL) {
958967
// Set the element size for numpy
959968
PyDataType_SET_ELSIZE(
@@ -1163,6 +1172,7 @@ query_desc::translate_types(bool use_unicode, PyObject *target_dtypes, int &unsu
11631172
Py_XDECREF(descr);
11641173
return -1;
11651174
}
1175+
11661176
// coerce_column_desc_types takes ownership of descr; no Py_DECREF needed
11671177
unsupported_fields += coerce_column_desc_types(column, use_unicode, descr);
11681178
}

tests/test_numpy.py

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def connection() -> npyodbc.Connection:
1919

2020
@pytest.fixture(scope="module")
2121
def cursor(connection) -> npyodbc.Cursor:
22-
"""Return a cursor for a SQLite3 database."""
22+
"""Return a new cursor for a SQLite3 database."""
2323
return connection.cursor()
2424

2525

@@ -34,11 +34,13 @@ def cleanup(cursor: npyodbc.Cursor):
3434
cursor : npyodbc.Cursor
3535
Cursor for the database to clean up
3636
"""
37-
for table in cursor.execute('SELECT tbl_name FROM sqlite_schema;').fetchall():
37+
for table in cursor.execute(
38+
"SELECT tbl_name FROM sqlite_schema UNION ALL "
39+
"SELECT tbl_name FROM sqlite_temp_schema;"
40+
).fetchall():
3841
cursor.execute(f'DROP TABLE {table[0]};')
3942
assert len(cursor.execute('SELECT tbl_name FROM sqlite_schema;').fetchall()) == 0
4043

41-
4244
def assert_result_close(a: np.ndarray, b: np.ndarray, dtype: Optional[np.dtype] = None):
4345
"""Assert that a is close to b.
4446
@@ -374,3 +376,41 @@ def test_null_strings(cursor):
374376
assert_array_equal(res['f_isnull'], [False, True])
375377

376378
cleanup(cursor)
379+
380+
381+
@pytest.mark.parametrize(
382+
"dtype", [
383+
"<S256",
384+
"<U256"
385+
]
386+
)
387+
def test_string_dtype_itemsize_corruption(cursor, dtype):
388+
"""Test that the string and unicode dtype singletons don't get modified by npyodbc.
389+
390+
This happens if the PyArray_Descr for the string or unicode dtype has its itemsize
391+
modified; all modifications should happen on _copies_ of that singleton dtype, i.e.
392+
we should be calling
393+
394+
PyArray_DescrNewFromType(NPY_STRING) // <-- Returns a copy of the singleton
395+
396+
rather than
397+
398+
PyArray_DescrFromType(NPY_STRING) // <-- Returns the singleton string dtype
399+
"""
400+
cursor.execute("""
401+
create temp table binary_type(
402+
binary_type_n BLOB,
403+
binary_type BLOB NOT NULL
404+
)
405+
""")
406+
407+
cursor.execute("insert into binary_type values (1, 2)")
408+
cursor.execute("insert into binary_type values (NULL, 5)")
409+
cursor.execute("select * from binary_type")
410+
411+
itemsize_before = np.array(["abcdef"], dtype=dtype).itemsize
412+
_ = cursor.fetchdictarray()
413+
itemsize_after = np.array(["abcdef"], dtype=dtype).itemsize
414+
415+
assert itemsize_before == itemsize_after
416+
cleanup(cursor)

0 commit comments

Comments
 (0)