"""Codec micro-benchmarks — no server required. These measure the tight inner loops the driver hits on every row: ``decode()`` per cell, ``parse_tuple_payload()`` per row, ``encode_param()`` per parameter. A 1M-row fetch hits ``decode()`` 5-10M times; a 1% slowdown there is *visible*. The fixtures synthesize realistic byte payloads — no need for the Docker container. This makes the benchmarks usable in CI's pre-merge job (which doesn't run integration tests). """ from __future__ import annotations import datetime import struct from io import BytesIO import pytest from informix_db._protocol import IfxStreamReader from informix_db._resultset import ColumnInfo, parse_tuple_payload from informix_db._types import IfxType from informix_db.converters import decode, encode_param pytestmark = pytest.mark.benchmark # --------------------------------------------------------------------------- # decode() — per-value dispatch # --------------------------------------------------------------------------- def test_decode_int(benchmark) -> None: """Hot path: per-cell INT decode. ~5M calls/sec is the kind of speed a 1M-row fetch with 5 INT columns needs.""" raw = struct.pack("!i", 42) benchmark(decode, int(IfxType.INT), raw) def test_decode_smallint(benchmark) -> None: raw = struct.pack("!h", 100) benchmark(decode, int(IfxType.SMALLINT), raw) def test_decode_bigint(benchmark) -> None: raw = struct.pack("!q", 1234567890123) benchmark(decode, int(IfxType.BIGINT), raw) def test_decode_float(benchmark) -> None: raw = struct.pack("!d", 3.14159) benchmark(decode, int(IfxType.FLOAT), raw) def test_decode_date(benchmark) -> None: raw = struct.pack("!i", 45678) benchmark(decode, int(IfxType.DATE), raw) def test_decode_varchar_short(benchmark) -> None: """20-byte ASCII string — typical name column.""" raw = b"hello world example " benchmark(decode, int(IfxType.VARCHAR), raw) def test_decode_varchar_long(benchmark) -> None: """255-byte VARCHAR — max non-LVARCHAR length.""" raw = b"x" * 255 benchmark(decode, int(IfxType.VARCHAR), raw) def test_decode_varchar_utf8(benchmark) -> None: """Multi-byte UTF-8 decode — exercise Phase 20 path.""" raw = "café résumé naïve Zürich".encode() benchmark(decode, int(IfxType.VARCHAR), raw, "utf-8") # --------------------------------------------------------------------------- # encode_param() — parameter-binding hot path # --------------------------------------------------------------------------- def test_encode_int(benchmark) -> None: benchmark(encode_param, 42) def test_encode_str_ascii(benchmark) -> None: benchmark(encode_param, "hello world example", "iso-8859-1") def test_encode_str_utf8(benchmark) -> None: benchmark(encode_param, "café résumé naïve", "utf-8") def test_encode_float(benchmark) -> None: benchmark(encode_param, 3.14159) def test_encode_date(benchmark) -> None: benchmark(encode_param, datetime.date(2026, 5, 4)) def test_encode_datetime(benchmark) -> None: benchmark(encode_param, datetime.datetime(2026, 5, 4, 12, 30, 45)) # --------------------------------------------------------------------------- # parse_tuple_payload() — per-row decode # --------------------------------------------------------------------------- def _build_systables_row_payload() -> bytes: """Synthesize the SQ_TUPLE bytes a typical systables row produces. Layout: [short warn=0][int size][payload][optional pad] Payload has columns: tabname VARCHAR(128), owner VARCHAR(32), tabid INT, partnum INT, ncols INT. """ payload = bytearray() # tabname VARCHAR: [byte len][bytes] — single-byte length prefix per # the discovered tuple format name = b"systables" payload.append(len(name)) payload.extend(name) # owner VARCHAR owner = b"informix" payload.append(len(owner)) payload.extend(owner) # tabid INT payload.extend(struct.pack("!i", 1)) # partnum INT payload.extend(struct.pack("!i", 1048578)) # ncols INT payload.extend(struct.pack("!i", 32)) out = bytearray() out.extend(struct.pack("!h", 0)) # warn out.extend(struct.pack("!i", len(payload))) out.extend(payload) if len(payload) & 1: out.append(0) # even-byte pad return bytes(out) _SYSTABLES_COLUMNS = [ ColumnInfo( name="tabname", type_code=int(IfxType.VARCHAR), raw_type_code=int(IfxType.VARCHAR), encoded_length=128, ), ColumnInfo( name="owner", type_code=int(IfxType.VARCHAR), raw_type_code=int(IfxType.VARCHAR), encoded_length=32, ), ColumnInfo( name="tabid", type_code=int(IfxType.INT), raw_type_code=int(IfxType.INT), encoded_length=4, ), ColumnInfo( name="partnum", type_code=int(IfxType.INT), raw_type_code=int(IfxType.INT), encoded_length=4, ), ColumnInfo( name="ncols", type_code=int(IfxType.INT), raw_type_code=int(IfxType.INT), encoded_length=4, ), ] def test_parse_tuple_5cols_iso8859(benchmark) -> None: """Decode a 5-column row (2 VARCHAR + 3 INT) — typical `systables` shape.""" payload = _build_systables_row_payload() def run() -> tuple: reader = IfxStreamReader(BytesIO(payload)) return parse_tuple_payload(reader, _SYSTABLES_COLUMNS) benchmark(run) def test_parse_tuple_5cols_utf8(benchmark) -> None: """Same shape, UTF-8 codec path — verify Phase 20 isn't a bottleneck.""" payload = _build_systables_row_payload() def run() -> tuple: reader = IfxStreamReader(BytesIO(payload)) return parse_tuple_payload(reader, _SYSTABLES_COLUMNS, encoding="utf-8") benchmark(run)