"""Phase 2 integration tests — SELECT execution end-to-end. Marked ``integration`` so the default ``pytest`` invocation skips them. Run with ``pytest -m integration`` after ``docker compose up``. """ 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_select_1_returns_one_tuple(conn_params: ConnParams) -> None: """The canonical Phase 2 milestone: ``SELECT 1`` → ``(1,)``.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT 1 FROM systables WHERE tabid = 1") assert cur.fetchone() == (1,) assert cur.fetchone() is None # no more rows def test_select_1_description_shape(conn_params: ConnParams) -> None: """``cursor.description`` is a 7-tuple per PEP 249.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT 1 FROM systables WHERE tabid = 1") assert cur.description is not None assert len(cur.description) == 1 col = cur.description[0] assert len(col) == 7 # (name, type_code, display_size, internal_size, precision, scale, null_ok) name, type_code, display_size, internal_size, _precision, _scale, _null_ok = col assert name == "(constant)" assert type_code == 2 # IfxType.INT assert display_size == internal_size == 4 def test_select_multi_row_int(conn_params: ConnParams) -> None: """Multi-row INT SELECT — fetchall returns a list of tuples.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT FIRST 5 tabid FROM systables ORDER BY tabid") rows = cur.fetchall() assert len(rows) == 5 assert rows == [(1,), (2,), (3,), (4,), (5,)] assert cur.rowcount == 5 def test_select_multi_column_mixed_types(conn_params: ConnParams) -> None: """Multi-column with mixed types (INT + FLOAT).""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT FIRST 3 tabid, nrows FROM systables ORDER BY tabid") rows = cur.fetchall() assert len(rows) == 3 for tabid, nrows in rows: assert isinstance(tabid, int) assert isinstance(nrows, float) names = [c[0] for c in cur.description] assert names == ["tabid", "nrows"] def test_iterator_protocol(conn_params: ConnParams) -> None: """Cursor supports the iterator protocol — ``for row in cursor``.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT FIRST 3 tabid FROM systables ORDER BY tabid") rows = list(cur) assert rows == [(1,), (2,), (3,)] def test_fetchmany(conn_params: ConnParams) -> None: """``fetchmany(n)`` returns up to n rows.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT FIRST 5 tabid FROM systables ORDER BY tabid") first_two = cur.fetchmany(2) assert first_two == [(1,), (2,)] rest = cur.fetchall() assert rest == [(3,), (4,), (5,)] def test_two_executes_on_same_cursor(conn_params: ConnParams) -> None: """Re-executing on the same cursor resets state cleanly.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT 1 FROM systables WHERE tabid = 1") assert cur.fetchone() == (1,) cur.execute("SELECT 2 FROM systables WHERE tabid = 1") assert cur.fetchone() == (2,) def test_varchar_single_column(conn_params: ConnParams) -> None: """VARCHAR column decoding — single-byte length prefix in tuple payload.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT FIRST 5 tabname FROM systables ORDER BY tabid") rows = cur.fetchall() assert len(rows) == 5 # All rows should have a single VARCHAR string element for (name,) in rows: assert isinstance(name, str) assert len(name) > 0 # Specifically the system-catalog tables we expect names = [r[0] for r in rows] assert names[0] == "systables" assert "syscolumns" in names def test_varchar_with_odd_length_padding(conn_params: ConnParams) -> None: """Odd-length VARCHAR row — payload padding to even alignment must be consumed. 'syscolumns' is 10 chars but the payload is 11 bytes (1-byte length + 10 bytes). 11 is odd, so the SQ_TUPLE format inserts a pad byte before the next message. Regression for the bug where parse_tuple_payload didn't consume that pad. """ with _connect(conn_params) as conn: cur = conn.cursor() # systables row 2 is "syscolumns" (10 chars → 11-byte payload → odd → pad) cur.execute("SELECT FIRST 2 tabname FROM systables ORDER BY tabid") rows = cur.fetchall() assert rows == [("systables",), ("syscolumns",)] def test_mixed_types_int_varchar_float(conn_params: ConnParams) -> None: """Three-column SELECT mixing INT + VARCHAR + FLOAT.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute("SELECT FIRST 3 tabname, tabid, nrows FROM systables ORDER BY tabid") rows = cur.fetchall() assert len(rows) == 3 for name, tabid, nrows in rows: assert isinstance(name, str) assert isinstance(tabid, int) assert isinstance(nrows, float) def test_two_cursors_on_same_connection(conn_params: ConnParams) -> None: """Two cursors on one connection — used sequentially (Phase 4 may parallel-ize).""" with _connect(conn_params) as conn: cur1 = conn.cursor() cur1.execute("SELECT 1 FROM systables WHERE tabid = 1") assert cur1.fetchone() == (1,) cur1.close() cur2 = conn.cursor() cur2.execute("SELECT FIRST 2 tabid FROM systables ORDER BY tabid") assert cur2.fetchall() == [(1,), (2,)] cur2.close()