Three findings, each caught by a different debugging technique,
documented in DECISION_LOG.md:
1. CURNAME+NFETCH PDU: trailing reserved field is SHORT not INT.
Caught by byte-diffing our 44-byte PDU against JDBC's 42-byte
reference under socat. The server tolerated the longer version
for INT-only SELECTs (silently consuming extra zeros) but
rejected it for VARCHAR queries. Lesson: server tolerance varies
by query type — always match JDBC byte-for-byte.
2. SQ_TUPLE payload pads to even byte alignment. An 11-byte
"syscolumns" VARCHAR payload had a trailing 0x00 between it and
the next SQ_TUPLE tag. JDBC's IfxRowColumn.readTuple consumes
this pad silently; we weren't, so any odd-length variable-width
row desynced the parser.
3. VARCHAR/NCHAR/NVCHAR in tuple data use a SINGLE-byte length
prefix (max 255 chars — IDS VARCHAR's hard limit). NOT a 2-byte
short as I'd initially assumed. CHAR is fixed-width per
encoded_length. LVARCHAR uses a 4-byte int prefix for >255 byte
values.
Module changes:
src/informix_db/_resultset.py — _LENGTH_PREFIXED_SHORT_TYPES set,
branched VARCHAR/NCHAR/NVCHAR (1-byte prefix) vs CHAR (fixed)
vs LVARCHAR (4-byte prefix); even-byte alignment pad consumed
after each SQ_TUPLE payload.
src/informix_db/cursors.py — CURNAME+NFETCH and standalone NFETCH
PDUs now write_short(0) for the reserved trailing field.
Tests: 40 unit + 18 integration (3 new VARCHAR tests) = 58 total,
all green, ruff clean. New tests cover:
- VARCHAR single-column SELECT
- Odd-length VARCHAR row (regression for the pad-byte bug)
- Mixed INT + VARCHAR + FLOAT three-column SELECT
Sample output:
SELECT FIRST 5 tabname FROM systables → ('systables',),
('syscolumns',), ('sysindices',), ('systabauth',), ('syscolauth',)
SELECT FIRST 3 tabname, tabid, nrows → ('systables', 1, 276.0), ...
VARCHAR was the last known gap from the Phase 2 commit. Phase 2
now reads INT, BIGINT, REAL, FLOAT, CHAR, VARCHAR end-to-end. Phase
6+ types (DATETIME, INTERVAL, DECIMAL, BLOBs) remain.
informix-db
Pure-Python driver for IBM Informix IDS, speaking the SQLI wire protocol over raw sockets. No IBM Client SDK. No JVM. No native libraries.
Status
🟢 Phase 1 complete. connect() / close() work end-to-end against a real Informix server. Cursor / execute / fetch land in Phase 2.
To our knowledge this is the first pure-socket Informix driver in any language — every other Informix driver (IfxPy, the legacy informixdb, ODBC bridges, Perl DBD::Informix) wraps either IBM's CSDK or the JDBC JAR.
Quick start
import informix_db
with informix_db.connect(
host="127.0.0.1", port=9088,
user="informix", password="in4mix",
database="sysmaster", server="informix",
) as conn:
# cursor() / execute() / fetchone() arrive in Phase 2
pass
Test against the official Informix dev container
docker compose -f tests/docker-compose.yml up -d # IBM Developer Edition, pinned by digest
uv sync --extra dev
uv run pytest # 34 unit tests (no Docker needed)
uv run pytest -m integration # 6 integration tests (needs the container)
Phase 0 artifacts (still useful — they ARE the public reference)
docs/PROTOCOL_NOTES.md— byte-level wire-format reference, derived from packet captures + JDBC decompilation, validated against a real serverdocs/JDBC_NOTES.md— index into the decompiled IBM JDBC driver's wire-protocol classesdocs/DECISION_LOG.md— running rationale for protocol / auth / type decisionsdocs/CAPTURES/— socat hex-dump captures of three reference scenarios (connect, SELECT, full DML cycle)tests/reference/RefClient.java— re-runnable JDBC ground-truth client for capturing fresh traces
License
MIT.
Description
Pure-Python driver for IBM Informix IDS — speaks the SQLI wire protocol over a raw socket. No CSDK, no JVM, no native libraries.
https://informix-db.warehack.ing
Languages
Python
85.6%
MDX
8.1%
CSS
2.1%
Java
1.7%
Astro
1%
Other
1.4%