From 0c856372a66845bc8405902f425adc11ba5bc2be Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Mon, 4 May 2026 15:38:09 -0600 Subject: [PATCH] v2026.05.04: bump CalVer + polish docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 43 ++++++ README.md | 181 +++++++++++++++++++++++--- docs/USAGE.md | 345 +++++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 7 +- uv.lock | 2 +- 5 files changed, 553 insertions(+), 25 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/USAGE.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..8fff2ae --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,43 @@ +# Changelog + +All notable changes to `informix-db`. Versioning is [CalVer](https://calver.org/) — `YYYY.MM.DD` for date-based releases, `YYYY.MM.DD.N` for same-day post-releases per PEP 440. + +## 2026.05.04 — Library completion + +The Phase 0 ambition — first pure-Python Informix SQLI driver — reaches feature completeness. Adds async, TLS, connection pool, smart-LOBs, fast-path RPC, composite UDTs. + +### Added + +- **Async API** (`informix_db.aio`) — `AsyncConnection`, `AsyncCursor`, `AsyncConnectionPool` for FastAPI / aiohttp / asyncio. Each blocking I/O call is offloaded to a worker thread via `asyncio.to_thread`; event loop never blocks. +- **Connection pool** (`informix_db.create_pool`) — thread-safe with min/max sizing, lazy growth, health-check on acquire, error-aware eviction. +- **TLS** — `tls=True` for self-signed dev servers, `tls=ssl.SSLContext` for production. Wrapping happens in `IfxSocket` so the rest of the protocol layer is unaware. +- **Smart-LOBs** (BLOB / CLOB) — full read/write end-to-end via `cursor.read_blob_column()` / `cursor.write_blob_column()` using the server's `lotofile` / `filetoblob` SQL functions intercepted at the `SQ_FILE` (98) protocol level. +- **Legacy in-row blobs** (BYTE / TEXT) — bind + read via the `SQ_BBIND` / `SQ_BLOB` / `SQ_FETCHBLOB` protocol family. +- **Fast-path RPC** (`Connection.fast_path_call`) — direct stored-procedure invocation bypassing PREPARE/EXECUTE; routine handles cached per-connection. +- **Composite UDT recognition** — `ROW`, `SET`, `MULTISET`, `LIST` columns return typed `RowValue` / `CollectionValue` wrappers exposing schema and raw bytes. +- **Type codecs** — `INTERVAL` (both DAY-TO-FRACTION and YEAR-TO-MONTH families), `DATETIME` (all qualifier ranges), `DECIMAL` / `MONEY` (BCD with sign+exp head byte and asymmetric base-100 complement for negatives), `DATE`, `BOOL`, all integer / float widths, `CHAR` / `VARCHAR` / `LVARCHAR`. +- **Transactions** — implicit `SQ_BEGIN` before each transaction in non-ANSI logged DBs; transparent no-ops on unlogged DBs. +- **PEP 249 exception hierarchy** — server `SQLCODE` mapped to the right exception class (`IntegrityError` for duplicate-key violations, `ProgrammingError` for syntax errors, etc.). + +### Documentation + +- [`README.md`](README.md) — overview and quick-start +- [`docs/USAGE.md`](docs/USAGE.md) — practical recipes and migration guide +- [`docs/PROTOCOL_NOTES.md`](docs/PROTOCOL_NOTES.md) — byte-level wire-format reference +- [`docs/DECISION_LOG.md`](docs/DECISION_LOG.md) — phase-by-phase architectural decisions, with the *why* preserved +- [`docs/JDBC_NOTES.md`](docs/JDBC_NOTES.md) — index into the decompiled IBM JDBC reference +- [`docs/CAPTURES/`](docs/CAPTURES/) — annotated socat hex-dump captures + +### Test coverage + +232 tests total: **69 unit + 163 integration**. Unit tests run with no external dependencies; integration tests run against the IBM Informix Developer Edition Docker image. + +### Known gaps (deferred) + +- **Full ROW/COLLECTION recursive parsing**: Phase 12 ships type recognition + raw-bytes wrapper. Parsing the textual representation into typed Python tuples/sets/lists is deferred — most workloads can use SQL projections (`SELECT row_col.fieldname FROM tbl`) instead. +- **UDT parameter encoding for fast-path**: scalar params/returns work; passing a 72-byte BLOB locator as a UDT param requires extending the SQ_BIND encoder with the extended_owner/extended_name preamble for type > 18. +- **Native async I/O**: Phase 16 ships a thread-pool wrapper that's functionally equivalent for typical FastAPI workloads. Native async (asyncpg-style transport abstraction) would be Phase 17 if a real workload needs it. + +## 2026.05.02 — Phase 1: connection lifecycle + +Initial release. `connect()` / `close()` works end-to-end. Cursor / execute / fetch arrived in Phase 2 (subsequent commits within the same session). diff --git a/README.md b/README.md index 01d013a..bd10b9d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # 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.** +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. -## Status +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. -🟢 **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. +```bash +pip install informix-db +``` ## Quick start @@ -14,30 +14,169 @@ To our knowledge this is the **first pure-socket Informix driver in any language import informix_db with informix_db.connect( - host="127.0.0.1", port=9088, - user="informix", password="in4mix", - database="sysmaster", server="informix", + host="db.example.com", port=9088, + user="informix", password="...", + database="mydb", server="informix", ) as conn: - # cursor() / execute() / fetchone() arrive in Phase 2 - pass + cur = conn.cursor() + cur.execute("SELECT id, name FROM users WHERE id = ?", (42,)) + user_id, name = cur.fetchone() ``` -## Test against the official Informix dev container +## Async (FastAPI / aiohttp / asyncio) + +```python +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) + +```python +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 + +```python +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 + +```python +# 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`](docs/DECISION_LOG.md) §10–11 for the architecture pivot that made this possible. + +## Direct stored-procedure invocation (fast-path) + +```python +# 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`](docs/DECISION_LOG.md): +- Phase 7 — logged-DB transactions +- Phase 8 — BYTE/TEXT (needs blobspace) +- Phase 10/11 — BLOB/CLOB (needs sbspace + `SBSPACENAME` config + 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 `:1` both work) +* **Threadsafety = 1**: threads may share the module but not connections; the pool gives per-thread connection access +* **CalVer versioning**: `YYYY.MM.DD` releases. PEP 440 post-releases (`.1`, `.2`) for same-day fixes. + +## Development ```bash -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) +# 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/ ``` -## Phase 0 artifacts (still useful — they ARE the public reference) +The integration suite expects an Informix Developer Edition container on `localhost:9088`: -- [`docs/PROTOCOL_NOTES.md`](docs/PROTOCOL_NOTES.md) — byte-level wire-format reference, derived from packet captures + JDBC decompilation, validated against a real server -- [`docs/JDBC_NOTES.md`](docs/JDBC_NOTES.md) — index into the decompiled IBM JDBC driver's wire-protocol classes -- [`docs/DECISION_LOG.md`](docs/DECISION_LOG.md) — running rationale for protocol / auth / type decisions -- [`docs/CAPTURES/`](docs/CAPTURES) — socat hex-dump captures of three reference scenarios (connect, SELECT, full DML cycle) -- [`tests/reference/RefClient.java`](tests/reference/RefClient.java) — re-runnable JDBC ground-truth client for capturing fresh traces +```bash +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`](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`](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 +- [`docs/DECISION_LOG.md`](docs/DECISION_LOG.md) — phase-by-phase architectural decisions, with the *why* preserved +- [`docs/CAPTURES/`](docs/CAPTURES/) — annotated socat hex-dump captures + +Notable architectural pivots documented in the decision log: +- **Phase 10/11** (smart-LOB read/write): used `lotofile`/`filetoblob` SQL functions + `SQ_FILE` protocol intercept instead of the heavier `SQ_FPROUTINE` + `SQ_LODATA` stack — ~3x smaller than originally projected +- **Phase 7** (logged-DB transactions): discovered Informix requires explicit `SQ_BEGIN` before each transaction in non-ANSI mode, plus `SQ_RBWORK` needs 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 diff --git a/docs/USAGE.md b/docs/USAGE.md new file mode 100644 index 0000000..bba7385 --- /dev/null +++ b/docs/USAGE.md @@ -0,0 +1,345 @@ +# Usage Guide + +Practical recipes for common Informix patterns with `informix-db`. For installation and a quick overview, see the [README](../README.md). For protocol-level / architectural decisions, see the [DECISION_LOG](DECISION_LOG.md). + +## Connecting + +```python +import informix_db + +conn = informix_db.connect( + host="db.example.com", + port=9088, + user="informix", + password="...", + database="mydb", + server="informix", # the DBSERVERNAME from sqlhosts + autocommit=False, # default; opt-in with True + connect_timeout=10.0, # seconds; None = OS default + read_timeout=30.0, # seconds for each read; None = no timeout + keepalive=False, # SO_KEEPALIVE on the socket + client_locale="en_US.8859-1", +) +``` + +`server` is **not** the hostname — it's the Informix DBSERVERNAME the listener identifies itself as (configured server-side in `$ONCONFIG`'s `DBSERVERNAME`). For the official IBM Developer Edition Docker image, the default `"informix"` is correct. + +`database` may be `None` to log in without selecting a database; the server still completes a successful login. Useful for cross-database queries that fully qualify table names. + +## Cursor lifecycle + +```python +cur = conn.cursor() +cur.execute("SELECT id, name FROM users WHERE active = ?", (True,)) + +# Single row +row = cur.fetchone() # tuple or None + +# All rows +rows = cur.fetchall() # list[tuple] + +# Bounded batch +batch = cur.fetchmany(100) # honors cur.arraysize default + +# Iteration +for row in cur: + print(row) + +cur.close() +``` + +The connection's `with` block automatically closes both the connection and any open cursors: + +```python +with informix_db.connect(...) as conn: + cur = conn.cursor() + cur.execute("SELECT 1 FROM systables WHERE tabid = 1") + print(cur.fetchone()) +# socket closed, cursor torn down +``` + +## Parameter binding + +Informix uses `paramstyle = "numeric"` (ESQL/C convention). Both `?` and `:1` / `:2` work: + +```python +cur.execute("SELECT id FROM users WHERE name = ? AND age > ?", ("alice", 30)) + +cur.execute( + "UPDATE users SET email = :2 WHERE id = :1", + (42, "alice@example.com"), +) +``` + +Type mapping: `int`, `float`, `str`, `bool`, `None`, `datetime.date`, `datetime.datetime`, `datetime.timedelta`, `decimal.Decimal`, `informix_db.IntervalYM`, `bytes` (BYTE/TEXT params). + +## Transactions + +Logged-DB transactions are managed implicitly. The driver sends `SQ_BEGIN` before each transaction in non-autocommit mode; `commit()` and `rollback()` close it. + +```python +conn = informix_db.connect(..., autocommit=False) # default +cur = conn.cursor() + +cur.execute("INSERT INTO orders VALUES (?, ?)", (1, "...")) +cur.execute("UPDATE inventory SET qty = qty - 1 WHERE sku = ?", ("ABC",)) +conn.commit() + +cur.execute("INSERT INTO orders VALUES (?, ?)", (2, "...")) +conn.rollback() # discards the second insert +``` + +For **unlogged databases**, both `commit()` and `rollback()` are silent no-ops — the connection knows it can't open a transaction (the server returns sqlcode -201 to `SQ_BEGIN`) and caches that state. Same client code works with both DB modes. + +For **autocommit mode**, each statement commits independently: + +```python +conn = informix_db.connect(..., autocommit=True) +cur = conn.cursor() +cur.execute("INSERT ...") # already committed +``` + +## executemany + +Batched DML — PREPARE once, BIND/EXECUTE per row, RELEASE at the end: + +```python +cur.executemany( + "INSERT INTO log VALUES (?, ?, ?)", + [ + (1, "info", "started"), + (2, "info", "loaded config"), + (3, "warn", "missing optional setting"), + ], +) +conn.commit() +``` + +## Smart-LOBs (BLOB / CLOB) + +### Read + +```python +# Fetch a single row's BLOB content as bytes +data = cur.read_blob_column( + "SELECT data FROM photos WHERE id = ?", (42,) +) +# data is bytes (or None if NULL or no rows match) +``` + +For multi-row reads or full control, drop down to the lower-level +`lotofile()` SQL form: + +```python +cur.execute( + "SELECT id, lotofile(data, '/tmp/x', 'client') FROM photos LIMIT 100" +) +for row in cur: + photo_id, returned_filename = row + raw_bytes = cur.blob_files[returned_filename] + process(photo_id, raw_bytes) +``` + +The server returns a unique filename suffix for each row; `cur.blob_files` is a dict keyed by those names. Phase 10 in the [decision log](DECISION_LOG.md) explains the protocol. + +### Write + +```python +cur.write_blob_column( + "INSERT INTO photos VALUES (?, BLOB_PLACEHOLDER)", + blob_data=jpeg_bytes, + params=(42,), +) +# CLOB column? Pass clob=True so it routes through filetoclob: +cur.write_blob_column( + "INSERT INTO docs VALUES (?, BLOB_PLACEHOLDER)", + blob_data=text.encode("iso-8859-1"), + params=(1,), + clob=True, +) +``` + +### Why `BLOB_PLACEHOLDER` instead of `?`? + +Plain `bytes` already maps to BYTE (legacy in-row blobs, type 11) when used as a `?`-parameter. The token approach makes it unambiguous which column receives the smart-LOB. The driver substitutes `BLOB_PLACEHOLDER` with `filetoblob('', 'client')` and registers the bytes for upload via the `SQ_FILE` protocol. + +## Connection pool + +```python +pool = informix_db.create_pool( + host="...", user="...", password="...", + database="mydb", + min_size=1, # pre-opened on construction + max_size=10, # hard ceiling + acquire_timeout=30.0, # seconds to wait for a free connection +) + +# Acquire / release via context manager (preferred) +with pool.connection() as conn: + cur = conn.cursor() + cur.execute(...) +# automatically returned to the pool + +# Or manually +conn = pool.acquire(timeout=5.0) +try: + cur = conn.cursor() + cur.execute(...) +finally: + pool.release(conn) + +pool.close() # drains idle connections; in-use connections close on their next release +``` + +The pool sends a trivial `SELECT 1` round-trip before yielding each connection (cheap health check; ~1ms on local network). Dead connections are silently replaced. Connection-related errors (`OperationalError`, `InterfaceError`) raised inside `with pool.connection() as conn:` evict the connection rather than returning it to the pool. + +## Async (asyncio) + +```python +import asyncio +from informix_db import aio + +async def main(): + async with await aio.connect( + host="...", user="...", password="...", database="mydb", + ) as conn: + cur = await conn.cursor() + await cur.execute( + "SELECT id, name FROM users WHERE active = ?", (True,) + ) + async for row in cur: + print(row) + +asyncio.run(main()) +``` + +Async pool: + +```python +pool = await aio.create_pool( + host="...", user="...", password="...", database="mydb", + min_size=1, max_size=10, +) +async with pool.connection() as conn: + cur = await conn.cursor() + await cur.execute(...) + rows = await cur.fetchall() +await pool.close() +``` + +The async API mirrors the sync API one-to-one. Each blocking I/O call is offloaded to a worker thread via `asyncio.to_thread` — the event loop never blocks; concurrent queries across an `asyncio.gather` actually run in parallel up to `max_size`. + +## TLS + +```python +import ssl + +# Production: caller-supplied SSLContext with full verification +ctx = ssl.create_default_context(cafile="/path/to/ca.pem") +informix_db.connect(host="db.example.com", port=9089, ..., tls=ctx) + +# Dev / self-signed certs: tls=True (verification DISABLED) +informix_db.connect(host="127.0.0.1", port=9089, ..., tls=True) +``` + +Informix uses dedicated TLS-enabled listener ports (configured server-side in `sqlhosts`) — point `port` at the TLS listener (often `9089`) when `tls` is enabled. + +## Error handling + +The exception hierarchy follows PEP 249: + +```text +Warning +Error +├── InterfaceError +└── DatabaseError + ├── DataError + ├── OperationalError + │ ├── PoolClosedError + │ └── PoolTimeoutError + ├── IntegrityError + ├── InternalError + ├── ProgrammingError + └── NotSupportedError +``` + +Server-side SQL errors carry the Informix `sqlcode`, `isamcode`, byte offset, and "near token" attributes: + +```python +try: + cur.execute("INSERT INTO users VALUES (1, 'duplicate-name')") +except informix_db.IntegrityError as e: + print(e.sqlcode) # e.g., -239 (duplicate key) + print(e.isamcode) # e.g., -100 + print(e.near) # e.g., "u_users_name" +``` + +The exception class is chosen based on the sqlcode (per the catalog in `informix_db/_errcodes.py`): + +| sqlcode | Exception class | +|---------|-----------------| +| -239, -268, -391, etc. | `IntegrityError` | +| -201, -202, -206, etc. | `ProgrammingError` | +| -255, -256, -267, etc. | `OperationalError` | +| -329, -413, -879, etc. | `NotSupportedError` | + +## Direct stored-procedure invocation (fast-path RPC) + +For UDFs that aren't callable via plain SQL (`ifx_lo_close`, etc.) or where you want to skip PREPARE → DESCRIBE → EXECUTE overhead: + +```python +result = conn.fast_path_call( + "function informix.ifx_lo_close(integer)", lofd +) +# result is a list of return values; here, [0] on success +``` + +Routine handles are cached per-connection by signature — first call resolves via `SQ_GETROUTINE`, subsequent calls skip that round-trip. UDT parameters (e.g., the 72-byte BLOB locator type) aren't yet supported on the bind side; only scalar params/returns work in the current MVP. + +## Server-side requirements + +Informix dev-image setup once-per-instance for the LOB feature set: + +```bash +# Inside the container, as the informix user with INFORMIXDIR/INFORMIXSERVER set: +onspaces -c -b blobspace1 -p /opt/ibm/data/spaces/blobspace.000 -o 0 -s 50000 +onspaces -c -S sbspace1 -p /opt/ibm/data/spaces/sbspace.000 -o 0 -s 50000 -Df "AVG_LO_SIZE=100" +onmode -wm SBSPACENAME=sbspace1 +onmode -wm LTAPEDEV=/dev/null +onmode -wm TAPEDEV=/dev/null +onmode -l +ontape -s -L 0 -t /dev/null +``` + +Then create a logged database (required for BYTE/TEXT/BLOB/CLOB): + +```sql +CREATE DATABASE mydb WITH LOG; +``` + +These steps are detailed in the [DECISION_LOG](DECISION_LOG.md) §6.f and §10. + +## Migration from `IfxPy` / legacy `informixdb` + +The PEP 249 surface is identical — most code Just Works after switching the import: + +```python +# Before +import IfxPyDbi as ifx + +# After +import informix_db as ifx +``` + +Differences worth knowing: + +| | `IfxPy` / legacy `informixdb` | `informix-db` | +|---|---|---| +| **Native deps** | IBM CSDK (`libifsql.so`) | None | +| **Wheel size** | ~50MB+ (CSDK bundled) | ~50KB | +| **Connection string** | DSN format | Per-keyword args (`host=`, `user=`, `password=`, `database=`, `server=`) | +| **paramstyle** | `qmark` | `numeric` (both `?` and `:N` work) | +| **TLS** | CSDK-managed | Native Python `ssl.SSLContext` | +| **Async** | Not supported | `informix_db.aio` | +| **Pool** | External (e.g., SQLAlchemy) | Built-in (`informix_db.create_pool`) | +| **BLOB API** | `setBytes`/`getBytes` | `cursor.read_blob_column` / `cursor.write_blob_column` with `BLOB_PLACEHOLDER` | diff --git a/pyproject.toml b/pyproject.toml index 3f3cf99..bdd1ea4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,14 +1,15 @@ [project] name = "informix-db" -version = "2026.05.02" +version = "2026.05.04" description = "Pure-Python driver for IBM Informix IDS — speaks the SQLI wire protocol over raw sockets. No CSDK, no JVM, no native libraries." readme = "README.md" license = { text = "MIT" } authors = [{ name = "Ryan Malloy", email = "ryan@supported.systems" }] requires-python = ">=3.10" -keywords = ["informix", "database", "sqli", "db-api", "pep-249"] +keywords = ["informix", "database", "sqli", "db-api", "pep-249", "asyncio", "async"] classifiers = [ - "Development Status :: 2 - Pre-Alpha", + "Development Status :: 4 - Beta", + "Framework :: AsyncIO", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", diff --git a/uv.lock b/uv.lock index 582148a..6d5aada 100644 --- a/uv.lock +++ b/uv.lock @@ -34,7 +34,7 @@ wheels = [ [[package]] name = "informix-db" -version = "2026.5.2" +version = "2026.5.4" source = { editable = "." } [package.optional-dependencies]