"""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})