The phase log
The driver was built across 39+ phases, each with a focused scope and a decision log. This page is a high-level index; the gory details (with rationale, alternatives considered, and rollback notes) live in docs/DECISION_LOG.md.
Foundation (Phases 1–10)
Section titled “Foundation (Phases 1–10)”| Phase | Title | Outcome |
|---|---|---|
| 1 | Socket + minimal SQ_INFO | First handshake against the dev container |
| 2–4 | Login, DBOPEN, error decoding | Can connect and select a database |
| 5 | Statement execution | SELECT 1 works |
| 6 | Parameter binding | ?-placeholders, basic types |
| 7 | Logged-DB transactions | Discovered Informix needs explicit SQ_BEGIN per tx in non-ANSI mode |
| 8 | BYTE / TEXT (legacy in-row blobs) | Needs blobspace |
| 9 | Scrollable cursors | SQ_SCROLL PDU, position semantics |
| 10/11 | Smart-LOB read & write | Architectural pivot to SQ_FILE intercept; ~3× smaller than projected |
Hardening (Phases 12–20)
Section titled “Hardening (Phases 12–20)”| Phase | Title | Outcome |
|---|---|---|
| 12 | Type system overhaul | Per-column readers (predecessor to Phase 37) |
| 13 | DECIMAL / MONEY exact precision | decimal.Decimal round-trip |
| 14 | DATETIME range typing | Returns date / datetime / time per field range |
| 15 | INTERVAL types | Custom IntervalYM, timedelta for D-to-F |
| 16 | Async API | Pivot to thread-pool wrapping (~250 lines) instead of full async refactor (~2000 lines) |
| 17 | Connection pool (sync) | min/max sizing, acquire timeout, max idle |
| 18 | Connection pool (async) | Mirror of sync API on aio.Pool |
| 19 | TLS support | Bring-your-own-context, tls=True for dev |
| 20 | Locale / Unicode | client_locale, full mapping in Connection.encoding |
Production review (Phases 21–30)
Section titled “Production review (Phases 21–30)”| Phase | Title | Outcome |
|---|---|---|
| 21 | Type-checking pass | py.typed, full mypy/pyright coverage |
| 22 | Error code mapping | SQLCODE → exception per reference |
| 23 | Health checks | Pool validates idle connections before return |
| 24 | Statement caching | Per-connection prepared-statement cache |
| 25 | Fast-path call (SQ_FPROUTINE) | Direct UDF/SPL invocation, bypassing PREPARE |
| 26 | CRITICAL | Pool returned connections with open transactions — fixed |
| 27 | CRITICAL | Per-connection wire lock + async cancellation safety |
| 28 | HIGH | _raise_sq_err bare-except masking wire desync — fixed |
| 29 | Cursor finalizers | Server-side resource leak on mid-fetch raise — fixed |
| 30 | Hardening pass | 5 medium-severity audit findings — all closed |
After Phase 30: 0 critical, 0 high, 0 medium audit findings remain. Driver is production-ready.
Performance (Phases 31–39)
Section titled “Performance (Phases 31–39)”| Phase | Title | Result |
|---|---|---|
| 31 | Statement cache LRU tuning | Better hit rate on repeated queries |
| 32 | Cursor lifecycle optimization | Fewer round-trips on small queries |
| 33 | Pipelined executemany | 1.6× faster than IfxPy on bulk inserts |
| 34 | LRU caches for type lookup | Removed dispatch overhead on hot paths |
| 35 | Memory profile pass | Identified 100k-row baseline |
| 36 | IfxPy comparison harness | Established the 2.4× bulk-fetch gap |
| 37 | Per-column reader strategy | −10% on bulk SELECT, ratio → 2.10× |
| 38 | exec()-based row-decoder codegen | Further −12%, ratio → 2.04× |
| 39 | Connection-scoped buffered reader | −32% on bulk SELECT, ratio → 1.05–1.15× |
The Phase 37–39 trajectory is documented in detail at The buffered reader →, including the architectural mistake the first pass got wrong.
Notable architectural pivots
Section titled “Notable architectural pivots”The decision log calls out four moments where the obvious choice would have been wrong:
- Phase 10/11 — abandoning
SQ_FPROUTINE+SQ_LODATAforSQ_FILEintercept. Smaller, simpler, same correctness. - Phase 16 — thread-pool async instead of full async refactor. ~88% less code, same FastAPI surface.
- Phase 27 — adding a per-connection wire lock instead of relying on PEP 249’s “don’t share connections” advice. Made accidental sharing safe rather than catastrophic.
- Phase 39 — buffer on the connection, not on the reader. Got it wrong on the first pass; the bug surfaced as a hang on pipelined
executemany. Fixed in ten minutes once the architectural mistake was named.
What’s next
Section titled “What’s next”The roadmap (loose, not committed):
- Phase 40+ (codec): Numpy-backed bulk decode for homogeneous columns. ~5× speedup target on analytical workloads.
- Phase 4x (protocol): Optional Cython acceleration for the codec hot loop. Would compromise “pure Python” — gated behind a build flag.
- Phase 5x (API): Native
callprocwith named parameters, IBM-specific scrollable cursor extensions for full IfxPy parity.
The phase log is updated as work lands. The repo’s CHANGELOG.md is the source of truth for shipped changes.