Version bump (2026.05.02 → 2026.05.04) reflects the library reaching
feature completeness across Phases 1-16.
Documentation:
* README.md — full rewrite. The previous README was from Phase 1
("cursor() / execute() / fetchone() arrive in Phase 2"). New
README covers: sync + async APIs, connection pool, TLS, full type
matrix, smart-LOBs, fast-path RPC, server-compatibility,
development workflow, and pointers to the protocol research docs.
* docs/USAGE.md — new practical recipe guide. Connecting, cursor
lifecycle, parameter binding, transactions (logged + unlogged),
executemany, smart-LOB read/write, connection pool, async,
TLS, error handling, fast-path RPC, server-side setup steps,
and a migration table from IfxPy / legacy informixdb.
* CHANGELOG.md — new file. Captures the v2026.05.04 release as the
Phase 1-16 completion milestone with a full feature inventory
and known-gap list. Future point-releases append here.
Classifiers updated:
* Development Status: 2 → 4 (Pre-Alpha → Beta)
* Added Framework :: AsyncIO
Keywords: added asyncio, async.
No code changes; tests still pass (69 unit + 163 integration = 232).
Ruff clean.
6.9 KiB
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. PEP 249 compliant; sync + async APIs; built-in connection pool; TLS support.
To our knowledge this is the first pure-socket Informix driver in any language — every other Informix driver (IfxPy, the legacy informixdb, ODBC bridges, JPype/JDBC, Perl DBD::Informix) wraps either IBM's CSDK or the JDBC JAR.
pip install informix-db
Quick start
import informix_db
with informix_db.connect(
host="db.example.com", port=9088,
user="informix", password="...",
database="mydb", server="informix",
) as conn:
cur = conn.cursor()
cur.execute("SELECT id, name FROM users WHERE id = ?", (42,))
user_id, name = cur.fetchone()
Async (FastAPI / aiohttp / asyncio)
import asyncio
from informix_db import aio
async def main():
pool = await aio.create_pool(
host="db.example.com", user="informix", password="...",
database="mydb",
min_size=1, max_size=10,
)
async with pool.connection() as conn:
cur = await conn.cursor()
await cur.execute("SELECT id, name FROM users WHERE id = ?", (42,))
row = await cur.fetchone()
await pool.close()
asyncio.run(main())
Connection pool (sync)
import informix_db
pool = informix_db.create_pool(
host="db.example.com", user="informix", password="...",
database="mydb",
min_size=1, max_size=10, acquire_timeout=5.0,
)
with pool.connection() as conn:
cur = conn.cursor()
cur.execute("...")
pool.close()
TLS
import ssl
# Production: bring your own context
ctx = ssl.create_default_context(cafile="/path/to/ca.pem")
informix_db.connect(host="...", port=9089, ..., tls=ctx)
# Dev / self-signed: tls=True disables verification
informix_db.connect(host="127.0.0.1", port=9089, ..., tls=True)
Informix uses dedicated TLS-enabled listener ports (configured server-side in sqlhosts) rather than STARTTLS upgrade — point port at the TLS listener (often 9089) when tls is enabled.
Type support
| SQL type | Python type |
|---|---|
SMALLINT / INT / BIGINT / SERIAL |
int |
FLOAT / SMALLFLOAT |
float |
DECIMAL(p,s) / MONEY |
decimal.Decimal |
CHAR / VARCHAR / NCHAR / NVCHAR / LVARCHAR |
str |
BOOLEAN |
bool |
DATE |
datetime.date |
DATETIME YEAR TO ... |
datetime.datetime / datetime.time / datetime.date |
INTERVAL DAY TO FRACTION |
datetime.timedelta |
INTERVAL YEAR TO MONTH |
informix_db.IntervalYM |
BYTE / TEXT (legacy in-row blobs) |
bytes / str |
BLOB / CLOB (smart-LOBs) |
informix_db.BlobLocator / informix_db.ClobLocator (read via cursor.read_blob_column, write via cursor.write_blob_column) |
ROW(...) |
informix_db.RowValue |
SET(...) / MULTISET(...) / LIST(...) |
informix_db.CollectionValue |
NULL |
None |
Smart-LOB (BLOB / CLOB) read & write
# Read: returns the actual bytes
data = cur.read_blob_column(
"SELECT data FROM photos WHERE id = ?", (42,)
)
# Write: BLOB_PLACEHOLDER token marks where the BLOB goes
cur.write_blob_column(
"INSERT INTO photos VALUES (?, BLOB_PLACEHOLDER)",
blob_data=jpeg_bytes,
params=(42,),
)
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 §10–11 for the architecture pivot that made this possible.
Direct stored-procedure invocation (fast-path)
# Cleanly close a smart-LOB descriptor opened via SQL
result = conn.fast_path_call(
"function informix.ifx_lo_close(integer)", lofd
)
# result == [0] on success
The fast-path RPC (SQ_FPROUTINE / SQ_EXFPROUTINE) bypasses PREPARE → EXECUTE → FETCH for direct UDF/SPL calls. Routine handles are cached per-connection, so repeated calls to the same function take a single round-trip.
Server compatibility
Tested against IBM Informix Dynamic Server 15.0.1.0.3DE (the official icr.io/informix/informix-developer-database Docker image). The wire protocol is stable across modern Informix versions; should work against 12.10+ unmodified.
For features that need server-side configuration (smart-LOBs, logged transactions), see docs/DECISION_LOG.md:
- Phase 7 — logged-DB transactions
- Phase 8 — BYTE/TEXT (needs blobspace)
- Phase 10/11 — BLOB/CLOB (needs sbspace +
SBSPACENAMEconfig + level-0 archive)
Standards & guarantees
- PEP 249 (DB-API 2.0):
connect(),Connection,Cursor,description,rowcount, exception hierarchy paramstyle = "numeric"(Informix's native ESQL/C convention;?and:1both work)- Threadsafety = 1: threads may share the module but not connections; the pool gives per-thread connection access
- CalVer versioning:
YYYY.MM.DDreleases. PEP 440 post-releases (.1,.2) for same-day fixes.
Development
# 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:
docker compose -f tests/docker-compose.yml up -d
For the smart-LOB tests specifically, the dev container needs additional one-time setup (blobspace + sbspace + level-0 archive). See docs/DECISION_LOG.md §10 for the exact onspaces / onmode / ontape commands.
Project history & design rationale
This driver was built incrementally over 16 phases, each with a focused scope and decision log. The full reasoning trail lives in:
docs/PROTOCOL_NOTES.md— byte-level SQLI wire-format referencedocs/JDBC_NOTES.md— index into the decompiled IBM JDBC driver, used as a clean-room referencedocs/DECISION_LOG.md— phase-by-phase architectural decisions, with the why preserveddocs/CAPTURES/— annotated socat hex-dump captures
Notable architectural pivots documented in the decision log:
- Phase 10/11 (smart-LOB read/write): used
lotofile/filetoblobSQL functions +SQ_FILEprotocol intercept instead of the heavierSQ_FPROUTINE+SQ_LODATAstack — ~3x smaller than originally projected - Phase 7 (logged-DB transactions): discovered Informix requires explicit
SQ_BEGINbefore each transaction in non-ANSI mode, plusSQ_RBWORKneeds a savepoint short payload - Phase 16 (async): shipped thread-pool wrapping (~250 lines) instead of full I/O abstraction refactor (~2000 lines); functionally equivalent for typical FastAPI workloads
License
MIT.