diff --git a/README.md b/README.md index bbef5ff..a1548fc 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,25 @@ To our knowledge this is the **first pure-socket Informix driver in any language pip install informix-db ``` +Requires Python ≥ 3.10. + +## Status + +**Production ready.** Every finding from a system-wide failure-mode audit (data correctness, wire safety, resource leaks, concurrency, async cancellation) has been addressed: + +| Severity | Finding | Status | +|---|---|---| +| Critical | Pool returns connections with open transactions | Fixed (Phase 26) | +| Critical | Unsynchronized wire path → PDU interleaving | Fixed (Phase 27) — per-connection wire lock | +| High | Async cancellation leaks running workers onto recycled connections | Fixed (Phase 27) | +| High | `_raise_sq_err` bare-except masks wire desync | Fixed (Phase 28) | +| High | Cursor finalizers — server-side resources leak on mid-fetch raise | Fixed (Phase 28+29) | +| Medium | 5 hardening items | Fixed (Phase 28+30) | + +**0 critical, 0 high, 0 medium audit findings remain.** Every architectural change went through a Margaret Hamilton-style review focused on silent-failure modes, recovery paths, and documented invariants. Each documented invariant is paired with either a runtime guard or a CI tripwire test. + +**Test coverage:** 300+ tests across unit / integration / benchmark suites. Integration tests run against the official IBM Informix Developer Edition Docker image (15.0.1.0.3DE). + ## Quick start ```python @@ -112,7 +131,7 @@ cur.write_blob_column( ) ``` -Both work end-to-end in pure Python via the `lotofile` / `filetoblob` server functions intercepted at the `SQ_FILE` (98) wire-protocol level — no thread of native machinery. See [`docs/DECISION_LOG.md`](docs/DECISION_LOG.md) §10–11 for the architecture pivot that made this possible. +Both work end-to-end in pure Python via the `lotofile` / `filetoblob` server functions intercepted at the `SQ_FILE` (98) wire-protocol level — no native machinery anywhere in the thread of execution. See [`docs/DECISION_LOG.md`](docs/DECISION_LOG.md) §10–11 for the architecture pivot that made this possible. ## Direct stored-procedure invocation (fast-path) @@ -135,34 +154,41 @@ For features that need server-side configuration (smart-LOBs, logged transaction - Phase 8 — BYTE/TEXT (needs blobspace) - Phase 10/11 — BLOB/CLOB (needs sbspace + `SBSPACENAME` config + level-0 archive) +## Performance + +Single-connection benchmarks against the dev container on loopback: + +| Operation | Mean | Throughput | +|---|---:|---:| +| `decode(int)` per cell | 139 ns | 7.2M ops/sec | +| `parse_tuple_payload` per row (5 cols) | 1.4 µs | 715K rows/sec | +| `SELECT 1` round-trip | ~140 µs | ~7K queries/sec | +| 1000-row SELECT | ~1.0 ms | ~990K rows/sec sustained | +| `executemany(1000)` in transaction | 32 ms | **~31,000 rows/sec** | +| Pool acquire + query + release | 295 µs | ~3.4K queries/sec | +| Cold connect (login handshake) | 11 ms | ~90 connections/sec | + +**Performance gotcha**: `executemany(...)` under `autocommit=True` is **53× slower** than the same call inside a single transaction (server flushes the transaction log per row). For bulk loads, `autocommit=False` (default) + `conn.commit()` at the end. See [`docs/USAGE.md`](docs/USAGE.md) for the full performance tips section. + ## Standards & guarantees * **PEP 249** (DB-API 2.0): `connect()`, `Connection`, `Cursor`, `description`, `rowcount`, exception hierarchy * **`paramstyle = "numeric"`** (Informix's native ESQL/C convention; `?` and `:1` both work) -* **Threadsafety = 1**: threads may share the module but not connections; the pool gives per-thread connection access +* **Threadsafety = 1**: threads may share the module but not connections; the pool gives per-thread connection access. Phase 27 added a per-connection wire lock that makes accidental sharing safe (interleaved PDUs serialize correctly), but PEP 249 advice still holds — give each thread its own connection. * **CalVer versioning**: `YYYY.MM.DD` releases. PEP 440 post-releases (`.1`, `.2`) for same-day fixes. ## Development -```bash -# Set up the dev environment -uv sync --dev - -# Run the test suite (unit-only by default; no Docker needed) -uv run pytest # 69 unit tests -uv run pytest -m integration # 163 integration tests (needs Docker) - -# Lint -uv run ruff check src/ tests/ -``` - -The integration suite expects an Informix Developer Edition container on `localhost:9088`: +The full test + lint workflow is in the [Makefile](Makefile). Quick summary: ```bash -docker compose -f tests/docker-compose.yml up -d +make test # 77 unit tests (no Docker) +make ifx-up && make test-integration # 231 integration tests +make bench # benchmark suite +make lint # ruff ``` -For the smart-LOB tests specifically, the dev container needs additional one-time setup (blobspace + sbspace + level-0 archive). See [`docs/DECISION_LOG.md`](docs/DECISION_LOG.md) §10 for the exact `onspaces` / `onmode` / `ontape` commands. +For the smart-LOB tests specifically, the dev container needs additional one-time setup (blobspace + sbspace + level-0 archive). See [`docs/DECISION_LOG.md`](docs/DECISION_LOG.md) §10 for the `onspaces` / `onmode` / `ontape` commands. ## Documentation @@ -172,7 +198,7 @@ For the smart-LOB tests specifically, the dev container needs additional one-tim ## Project history & design rationale -This driver was built incrementally across 22+ phases, each with a focused scope and decision log. The reasoning trail lives in: +This driver was built incrementally across 30 phases, each with a focused scope and decision log. The reasoning trail lives in: - [`docs/PROTOCOL_NOTES.md`](docs/PROTOCOL_NOTES.md) — byte-level SQLI wire-format reference - [`docs/JDBC_NOTES.md`](docs/JDBC_NOTES.md) — index into the decompiled IBM JDBC driver, used as a clean-room reference diff --git a/pyproject.toml b/pyproject.toml index 978d769..340a044 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{ name = "Ryan Malloy", email = "ryan@supported.systems" }] requires-python = ">=3.10" keywords = ["informix", "database", "sqli", "db-api", "pep-249", "asyncio", "async"] classifiers = [ - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License",