Skip to content

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.

PhaseTitleOutcome
1Socket + minimal SQ_INFOFirst handshake against the dev container
2–4Login, DBOPEN, error decodingCan connect and select a database
5Statement executionSELECT 1 works
6Parameter binding?-placeholders, basic types
7Logged-DB transactionsDiscovered Informix needs explicit SQ_BEGIN per tx in non-ANSI mode
8BYTE / TEXT (legacy in-row blobs)Needs blobspace
9Scrollable cursorsSQ_SCROLL PDU, position semantics
10/11Smart-LOB read & writeArchitectural pivot to SQ_FILE intercept; ~3× smaller than projected
PhaseTitleOutcome
12Type system overhaulPer-column readers (predecessor to Phase 37)
13DECIMAL / MONEY exact precisiondecimal.Decimal round-trip
14DATETIME range typingReturns date / datetime / time per field range
15INTERVAL typesCustom IntervalYM, timedelta for D-to-F
16Async APIPivot to thread-pool wrapping (~250 lines) instead of full async refactor (~2000 lines)
17Connection pool (sync)min/max sizing, acquire timeout, max idle
18Connection pool (async)Mirror of sync API on aio.Pool
19TLS supportBring-your-own-context, tls=True for dev
20Locale / Unicodeclient_locale, full mapping in Connection.encoding
PhaseTitleOutcome
21Type-checking passpy.typed, full mypy/pyright coverage
22Error code mappingSQLCODE → exception per reference
23Health checksPool validates idle connections before return
24Statement cachingPer-connection prepared-statement cache
25Fast-path call (SQ_FPROUTINE)Direct UDF/SPL invocation, bypassing PREPARE
26CRITICALPool returned connections with open transactions — fixed
27CRITICALPer-connection wire lock + async cancellation safety
28HIGH_raise_sq_err bare-except masking wire desync — fixed
29Cursor finalizersServer-side resource leak on mid-fetch raise — fixed
30Hardening pass5 medium-severity audit findings — all closed

After Phase 30: 0 critical, 0 high, 0 medium audit findings remain. Driver is production-ready.

PhaseTitleResult
31Statement cache LRU tuningBetter hit rate on repeated queries
32Cursor lifecycle optimizationFewer round-trips on small queries
33Pipelined executemany1.6× faster than IfxPy on bulk inserts
34LRU caches for type lookupRemoved dispatch overhead on hot paths
35Memory profile passIdentified 100k-row baseline
36IfxPy comparison harnessEstablished the 2.4× bulk-fetch gap
37Per-column reader strategy−10% on bulk SELECT, ratio → 2.10×
38exec()-based row-decoder codegenFurther −12%, ratio → 2.04×
39Connection-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.

The decision log calls out four moments where the obvious choice would have been wrong:

  1. Phase 10/11 — abandoning SQ_FPROUTINE + SQ_LODATA for SQ_FILE intercept. Smaller, simpler, same correctness.
  2. Phase 16 — thread-pool async instead of full async refactor. ~88% less code, same FastAPI surface.
  3. 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.
  4. 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.

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 callproc with 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.