Implements end-to-end round-trip for BYTE (type 11) and TEXT (type 12)
columns. Python bytes/bytearray map to BYTE; str is auto-encoded as
ISO-8859-1 for TEXT.
Wire protocol — write side:
* SQ_BIND payload carries a 56-byte blob descriptor with size at offset
[16..19] (per IfxBlob.toIfx). NULL is byte 39=1.
* After all per-param blocks, SQ_BBIND (41) declares blob count, then
chunked SQ_BLOB (39) messages stream the actual bytes (max 1024
bytes/chunk per JDBC), terminated by zero-length SQ_BLOB.
* Then SQ_EXECUTE proceeds normally.
Wire protocol — read side:
* SQ_TUPLE returns only the 56-byte descriptor; actual bytes live in
the blobspace.
* For each BYTE/TEXT column in each row, send SQ_FETCHBLOB with the
descriptor and read SQ_BLOB chunks until zero-length terminator.
* The locator is only valid while the cursor is open — must dereference
BEFORE sending CLOSE. Doing it after returns -602 (Cannot open blob).
Server-side prerequisites (one-time setup):
1. blobspace: onspaces -c -b blobspace1 -p /path -o 0 -s 50000
2. logged DB: CREATE DATABASE testdb WITH LOG
3. config + archive:
onmode -wm LTAPEDEV=/dev/null
onmode -wm TAPEDEV=/dev/null
onmode -l
ontape -s -L 0 -t /dev/null
Without #3, JDBC fails identically to our driver with "BLOB pages can't
be allocated from a chunk until chunk add is logged". This identical
failure was the diagnostic confirmation that our protocol bytes were
correct — same server response = byte-for-byte parity.
Tests: 9 integration tests in tests/test_blob.py — single-chunk,
multi-chunk (5120 bytes), NULL, multi-row, binary-safe, TEXT roundtrip,
ISO-8859-1, NULL TEXT, mixed columns. Plus the Phase 4
test_unsupported_param_type_raises was updated since bytes is no longer
the canonical unsupported type — switched to a custom class.
Total: 53 unit + 107 integration = 160 tests.
The smart-LOB family (BLOB/CLOB) is a separate state-machine extension
deferred to Phase 9 — it uses IfxLocator + LO_OPEN/LO_READ session
protocol against sbspace, not the BBIND/BLOB stream.
215 lines
8.3 KiB
Python
215 lines
8.3 KiB
Python
"""Phase 4 integration tests — parameter binding (SQ_BIND).
|
|
|
|
Tests cover ``?`` and ``:N`` placeholder styles, the supported Python
|
|
type set (int, float, str, bool, None), and round-tripping through INSERT
|
|
+ SELECT to verify both encode AND decode paths.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import informix_db
|
|
from tests.conftest import ConnParams
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
def _connect(conn_params: ConnParams) -> informix_db.Connection:
|
|
return informix_db.connect(
|
|
host=conn_params.host,
|
|
port=conn_params.port,
|
|
user=conn_params.user,
|
|
password=conn_params.password,
|
|
database=conn_params.database,
|
|
server=conn_params.server,
|
|
connect_timeout=10.0,
|
|
read_timeout=10.0,
|
|
)
|
|
|
|
|
|
def test_insert_with_qmark_params(conn_params: ConnParams) -> None:
|
|
"""``?`` placeholder style."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_p_a (id INTEGER, name VARCHAR(50))")
|
|
cur.execute("INSERT INTO t_p_a VALUES (?, ?)", (42, "hello"))
|
|
assert cur.rowcount == 1
|
|
|
|
cur.execute("SELECT id, name FROM t_p_a")
|
|
assert cur.fetchall() == [(42, "hello")]
|
|
|
|
|
|
def test_insert_with_numeric_params(conn_params: ConnParams) -> None:
|
|
"""``:1`` placeholder style (paramstyle="numeric")."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_p_b (id INTEGER, name VARCHAR(50))")
|
|
cur.execute("INSERT INTO t_p_b VALUES (:1, :2)", (99, "world"))
|
|
assert cur.rowcount == 1
|
|
|
|
cur.execute("SELECT id, name FROM t_p_b")
|
|
assert cur.fetchall() == [(99, "world")]
|
|
|
|
|
|
def test_int_float_str_round_trip(conn_params: ConnParams) -> None:
|
|
"""All three core types in one INSERT, verified via SELECT."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_p_c (i INTEGER, f FLOAT, s VARCHAR(20))")
|
|
cur.execute("INSERT INTO t_p_c VALUES (?, ?, ?)", (123, 4.5, "alpha"))
|
|
cur.execute("INSERT INTO t_p_c VALUES (?, ?, ?)", (-7, -1.25, "beta"))
|
|
|
|
cur.execute("SELECT i, f, s FROM t_p_c ORDER BY i")
|
|
rows = cur.fetchall()
|
|
assert rows == [(-7, -1.25, "beta"), (123, 4.5, "alpha")]
|
|
|
|
|
|
def test_update_with_params(conn_params: ConnParams) -> None:
|
|
"""UPDATE with parameter values in both SET and WHERE clauses."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_p_d (id INTEGER, name VARCHAR(50))")
|
|
cur.execute("INSERT INTO t_p_d VALUES (?, ?)", (1, "old"))
|
|
cur.execute("INSERT INTO t_p_d VALUES (?, ?)", (2, "old"))
|
|
|
|
cur.execute("UPDATE t_p_d SET name = ? WHERE id = ?", ("new", 2))
|
|
|
|
cur.execute("SELECT id, name FROM t_p_d ORDER BY id")
|
|
assert cur.fetchall() == [(1, "old"), (2, "new")]
|
|
|
|
|
|
def test_delete_with_param(conn_params: ConnParams) -> None:
|
|
"""DELETE with a parameter in the WHERE clause."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_p_e (id INTEGER, name VARCHAR(50))")
|
|
for i in range(1, 6):
|
|
cur.execute("INSERT INTO t_p_e VALUES (?, ?)", (i, f"row{i}"))
|
|
|
|
cur.execute("DELETE FROM t_p_e WHERE id = ?", (3,))
|
|
cur.execute("SELECT id FROM t_p_e ORDER BY id")
|
|
assert cur.fetchall() == [(1,), (2,), (4,), (5,)]
|
|
|
|
|
|
def test_unsupported_param_type_raises(conn_params: ConnParams) -> None:
|
|
"""Driver accepts a known set of Python types; other types raise.
|
|
|
|
Phase 8 added ``bytes``/``bytearray``, so the canonical "unsupported"
|
|
sentinel is now an arbitrary Python class with no encoder dispatch.
|
|
"""
|
|
class CustomType:
|
|
pass
|
|
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_p_f (id INTEGER)")
|
|
with pytest.raises(NotImplementedError, match="CustomType"):
|
|
cur.execute("INSERT INTO t_p_f VALUES (?)", (CustomType(),))
|
|
|
|
|
|
def test_parameterized_select_int(conn_params: ConnParams) -> None:
|
|
"""Parameterized SELECT with an int parameter."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT tabname FROM systables WHERE tabid = ?", (1,))
|
|
assert cur.fetchone() == ("systables",)
|
|
|
|
|
|
def test_parameterized_select_multiple_params(conn_params: ConnParams) -> None:
|
|
"""Parameterized SELECT with two int parameters bounding a range."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT tabname FROM systables WHERE tabid >= ? AND tabid <= ? ORDER BY tabid",
|
|
(1, 3),
|
|
)
|
|
assert cur.fetchall() == [("systables",), ("syscolumns",), ("sysindices",)]
|
|
|
|
|
|
def test_parameterized_select_string_param(conn_params: ConnParams) -> None:
|
|
"""Parameterized SELECT with a string parameter."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT tabid FROM systables WHERE tabname = ?", ("systables",))
|
|
assert cur.fetchone() == (1,)
|
|
|
|
|
|
def test_parameterized_select_numeric_style(conn_params: ConnParams) -> None:
|
|
"""``:1`` style works for SELECT too."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("SELECT tabname FROM systables WHERE tabid = :1", (2,))
|
|
assert cur.fetchone() == ("syscolumns",)
|
|
|
|
|
|
def test_executemany_basic_insert(conn_params: ConnParams) -> None:
|
|
"""``executemany`` for batched INSERT — PREPARE once, BIND/EXECUTE per row."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_em_a (id INTEGER, name VARCHAR(50))")
|
|
rows = [(1, "alpha"), (2, "beta"), (3, "gamma"), (4, "delta")]
|
|
cur.executemany("INSERT INTO t_em_a VALUES (?, ?)", rows)
|
|
assert cur.rowcount == 4
|
|
|
|
cur.execute("SELECT id, name FROM t_em_a ORDER BY id")
|
|
assert cur.fetchall() == rows
|
|
|
|
|
|
def test_executemany_update(conn_params: ConnParams) -> None:
|
|
"""``executemany`` works for UPDATE too — per-row WHERE matches."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_em_u (id INTEGER, name VARCHAR(50))")
|
|
cur.executemany(
|
|
"INSERT INTO t_em_u VALUES (?, ?)",
|
|
[(1, "old"), (2, "old"), (3, "old")],
|
|
)
|
|
cur.executemany(
|
|
"UPDATE t_em_u SET name = ? WHERE id = ?",
|
|
[("A", 1), ("B", 2), ("C", 3)],
|
|
)
|
|
cur.execute("SELECT id, name FROM t_em_u ORDER BY id")
|
|
assert cur.fetchall() == [(1, "A"), (2, "B"), (3, "C")]
|
|
|
|
|
|
def test_executemany_empty_list(conn_params: ConnParams) -> None:
|
|
"""Empty parameter list is a no-op with rowcount=0."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_em_e (id INTEGER)")
|
|
cur.executemany("INSERT INTO t_em_e VALUES (?)", [])
|
|
assert cur.rowcount == 0
|
|
|
|
|
|
def test_executemany_inconsistent_param_lens_raises(conn_params: ConnParams) -> None:
|
|
"""Mismatched parameter-set lengths must raise ProgrammingError."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_em_bad (id INTEGER, name VARCHAR(50))")
|
|
with pytest.raises(informix_db.ProgrammingError, match="parameter set"):
|
|
cur.executemany(
|
|
"INSERT INTO t_em_bad VALUES (?, ?)",
|
|
[(1, "ok"), (2,)], # second has only 1 value
|
|
)
|
|
|
|
|
|
def test_executemany_select_unsupported(conn_params: ConnParams) -> None:
|
|
"""``executemany`` on SELECT doesn't make sense — must raise."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
with pytest.raises(informix_db.NotSupportedError, match="SELECT"):
|
|
cur.executemany(
|
|
"SELECT tabname FROM systables WHERE tabid = ?",
|
|
[(1,), (2,)],
|
|
)
|
|
|
|
|
|
def test_dict_params_unsupported(conn_params: ConnParams) -> None:
|
|
"""Named parameters aren't supported — paramstyle is ``numeric``."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_p_g (id INTEGER)")
|
|
with pytest.raises(informix_db.NotSupportedError, match="positional"):
|
|
cur.execute("INSERT INTO t_p_g VALUES (:id)", {"id": 1})
|