|
7 | 7 | belongs to the separate LOB decoder and is not covered by this regression. |
8 | 8 | """ |
9 | 9 |
|
| 10 | +import os |
| 11 | +import subprocess |
10 | 12 | import sys |
| 13 | +import textwrap |
11 | 14 |
|
12 | 15 | import pytest |
13 | 16 |
|
14 | | -from mssql_python import SQL_CHAR, SQL_WCHAR |
| 17 | +from mssql_python import SQL_CHAR, SQL_WCHAR, ddbc_bindings as ddbc |
15 | 18 |
|
16 | 19 |
|
17 | 20 | @pytest.fixture(scope="module") |
@@ -294,3 +297,122 @@ def getdata(handle, column, ctype, buffer, capacity, indicator): |
294 | 297 | check=False, |
295 | 298 | ) |
296 | 299 | assert result.returncode == 0, result.stdout + result.stderr |
| 300 | + |
| 301 | + |
| 302 | +def _run_fetch_script(conn_str, script, *arguments, timeout=45): |
| 303 | + result = subprocess.run( |
| 304 | + [sys.executable, "-c", script, *arguments], |
| 305 | + env={**os.environ, "DB_CONNECTION_STRING": conn_str}, |
| 306 | + capture_output=True, |
| 307 | + text=True, |
| 308 | + timeout=timeout, |
| 309 | + check=False, |
| 310 | + ) |
| 311 | + assert result.returncode == 0, result.stdout + result.stderr |
| 312 | + |
| 313 | + |
| 314 | +@pytest.mark.parametrize("size", (1, 2)) |
| 315 | +def test_getdata_decode_error_keeps_preexisting_and_completed_cells(cursor, size): |
| 316 | + cursor.execute("SELECT 7, CAST(0x00D8 AS NVARCHAR(1))") |
| 317 | + assert ddbc.DDBCSQLFetch(cursor.hstmt) == 0 |
| 318 | + marker = object() |
| 319 | + row = [marker] |
| 320 | + if size == 1: |
| 321 | + assert ddbc.DDBCSQLGetData(cursor.hstmt, size, row, "utf-16le", "utf-16le", -8) == 0 |
| 322 | + else: |
| 323 | + with pytest.raises(UnicodeDecodeError): |
| 324 | + ddbc.DDBCSQLGetData(cursor.hstmt, size, row, "utf-16le", "utf-16le", -8) |
| 325 | + assert row == [marker, 7] |
| 326 | + cursor.execute("SELECT 42") |
| 327 | + assert cursor.fetchval() == 42 |
| 328 | + |
| 329 | + |
| 330 | +@pytest.mark.skipif( |
| 331 | + sys.platform == "win32", |
| 332 | + reason="Windows does not export the native driver function-pointer globals", |
| 333 | +) |
| 334 | +@pytest.mark.parametrize("payload", ("utf8", "empty", "null", "invalid", "warning")) |
| 335 | +def test_narrow_getdata_decoding_in_subprocess(conn_str, payload): |
| 336 | + script = textwrap.dedent(""" |
| 337 | + import ctypes as c |
| 338 | + import os |
| 339 | + import sys |
| 340 | + import mssql_python |
| 341 | + from mssql_python import ddbc_bindings as ddbc |
| 342 | +
|
| 343 | + mode = sys.argv[1] |
| 344 | + data = b"\\xff" if mode == "invalid" else ( |
| 345 | + b"" if mode == "empty" else "\\ufeffA\\x00\\U0001f600".encode("utf-8") |
| 346 | + ) |
| 347 | + pointer, short, length = c.c_void_p, c.c_short, c.c_ssize_t |
| 348 | + get_type = c.CFUNCTYPE(short, pointer, c.c_ushort, short, pointer, length, pointer) |
| 349 | + diag_type = c.CFUNCTYPE( |
| 350 | + short, short, pointer, short, pointer, pointer, pointer, short, pointer |
| 351 | + ) |
| 352 | + library = c.CDLL(ddbc.module.__file__) |
| 353 | + slot = pointer.in_dll(library, "SQLGetData_ptr") |
| 354 | + diag_slot = pointer.in_dll(library, "SQLGetDiagRec_ptr") |
| 355 | + calls, errors = [], [] |
| 356 | +
|
| 357 | + @diag_type |
| 358 | + def diagnostic(handle_type, handle, record, state, native, message, capacity, size): |
| 359 | + try: |
| 360 | + if record > 1: |
| 361 | + return 100 |
| 362 | + text = "getdata warning".encode("utf-16le") |
| 363 | + assert capacity > len(text) // 2 |
| 364 | + c.memmove(state, "01000\\0".encode("utf-16le"), 12) |
| 365 | + c.memmove(message, text + b"\\0\\0", len(text) + 2) |
| 366 | + c.cast(native, c.POINTER(c.c_int))[0] = 0 |
| 367 | + c.cast(size, c.POINTER(short))[0] = len(text) // 2 |
| 368 | + return 0 |
| 369 | + except BaseException as error: |
| 370 | + errors.append(type(error).__name__) |
| 371 | + return -1 |
| 372 | +
|
| 373 | + @get_type |
| 374 | + def getdata(handle, column, ctype, buffer, capacity, indicator): |
| 375 | + try: |
| 376 | + assert ctype == 1 # SQL_C_CHAR |
| 377 | + calls.append(column) |
| 378 | + result = original(handle, column, ctype, buffer, capacity, indicator) |
| 379 | + assert result == 0 and capacity > len(data) |
| 380 | + c.memmove(buffer, data + b"\\0", len(data) + 1) |
| 381 | + c.cast(indicator, c.POINTER(length))[0] = -1 if mode == "null" else len(data) |
| 382 | + return 1 if mode == "warning" else result |
| 383 | + except BaseException as error: |
| 384 | + errors.append(type(error).__name__) |
| 385 | + return -1 |
| 386 | +
|
| 387 | + with mssql_python.connect(os.environ["DB_CONNECTION_STRING"]) as connection: |
| 388 | + with connection.cursor() as cursor: |
| 389 | + cursor.execute("SELECT CAST('text' AS VARCHAR(30))") |
| 390 | + saved, saved_diag = slot.value, diag_slot.value |
| 391 | + assert saved and saved_diag |
| 392 | + original = get_type(saved) |
| 393 | + assert ddbc.DDBCSQLFetch(cursor.hstmt) == 0 |
| 394 | + marker = object() |
| 395 | + row = [marker] |
| 396 | + slot.value = c.cast(getdata, pointer).value |
| 397 | + if mode == "warning": |
| 398 | + diag_slot.value = c.cast(diagnostic, pointer).value |
| 399 | + try: |
| 400 | + ret = ddbc.DDBCSQLGetData( |
| 401 | + cursor.hstmt, 1, row, "utf-8", "utf-16le", 1, cursor.messages |
| 402 | + ) |
| 403 | + assert ret == (1 if mode == "warning" else 0) |
| 404 | + finally: |
| 405 | + slot.value, diag_slot.value = saved, saved_diag |
| 406 | + expected = None if mode == "null" else ( |
| 407 | + data if mode == "invalid" else data.decode("utf-8") |
| 408 | + ) |
| 409 | + assert row == [marker, expected] |
| 410 | + assert type(row[1]) is type(expected) |
| 411 | + assert calls == [1] and not errors, (calls, errors) |
| 412 | + assert cursor.messages == ( |
| 413 | + [("[01000] (0)", "getdata warning")] if mode == "warning" else [] |
| 414 | + ) |
| 415 | + cursor.execute("SELECT 42") |
| 416 | + assert cursor.fetchval() == 42 |
| 417 | + """) |
| 418 | + _run_fetch_script(conn_str, script, payload) |
0 commit comments