Docs site: correct APIs that don't exist, document what does
Auditing the docs site against the actual module surface — prompted by
the same field report as 2026.05.08.2 — turned up a partly fictional API.
Every claim below was checked by calling it, not by reading the source.
Documented but nonexistent, now removed:
conn.transaction() AttributeError (neither sync nor async)
conn.autocommit never existed; set via connect(autocommit=)
cursor.lastrowid never existed
cursor.read_clob_column never existed
cursor.write_clob_column never existed
A copy-pasteable example in reference/types.md raised TypeError:
IntervalYM takes a single total month count, not (years=, months=).
False descriptions corrected: RowValue was described as "named-tuple-like
with .name-accessible fields" and CollectionValue as "iterable" with
"indexed access". Neither is true — both are opaque wrappers over raw
bytes plus a schema string. The docs now say so and point at SQL
projection as the way to get fields today.
Replacements are the real idioms, taken from passing tests:
CLOBs -> write_blob_column(..., clob=True); read returns bytes
SERIAL value -> SELECT DBINFO('sqlca.sqlerrd1')
transactions -> commit()/rollback() in try/except
Also added: INT8/SERIAL8/BIGSERIAL to the type table (with why INT8 is
not BIGINT), server_capabilities + server_version, the scrollable-cursor
methods, and the note that server_version reports the internal protocol
version (12.10 says 9.56, 14.10 says 9.59).
Updated the test-count claim to 400+ across three server versions,
replacing "integration tests run against 15.0.1.0.3DE".
Added a checker pass over every conn.*/cursor.* reference in the docs;
the only remaining unresolved names are deliberate mentions of IfxPy
APIs we don't implement. Site builds clean.
This commit is contained in:
parent
f5a539d4a8
commit
85d3a8f7d7
@ -7,7 +7,7 @@ sidebar:
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
`informix-driver`'s async API (`from informix_db import aio`) is implemented by wrapping the sync core in a thread pool. Every `await conn.execute(...)` schedules the underlying sync `execute()` on the pool's executor.
|
||||
`informix-driver`'s async API (`from informix_db import aio`) is implemented by wrapping the sync core in a thread pool. Every `await cur.execute(...)` schedules the underlying sync `execute()` on the pool's executor.
|
||||
|
||||
This is a deliberate architectural choice from Phase 16. Here's the reasoning.
|
||||
|
||||
|
||||
@ -64,13 +64,13 @@ This is a Phase 27 invariant: async cancellation cannot leak running workers ont
|
||||
|
||||
## Connection-level transactions
|
||||
|
||||
For request-scoped transactions, use a context manager around the connection:
|
||||
For request-scoped transactions, commit on the way out and roll back on any exception:
|
||||
|
||||
```python
|
||||
@app.post("/orders")
|
||||
async def create_order(order: OrderIn, conn = Depends(get_conn)):
|
||||
async with conn.transaction():
|
||||
cur = await conn.cursor()
|
||||
cur = await conn.cursor()
|
||||
try:
|
||||
await cur.execute(
|
||||
"INSERT INTO orders VALUES (?, ?, ?)",
|
||||
(order.id, order.customer_id, order.total),
|
||||
@ -79,7 +79,15 @@ async def create_order(order: OrderIn, conn = Depends(get_conn)):
|
||||
"UPDATE inventory SET qty = qty - ? WHERE sku = ?",
|
||||
(order.qty, order.sku),
|
||||
)
|
||||
await conn.commit()
|
||||
except Exception:
|
||||
await conn.rollback()
|
||||
raise
|
||||
return {"ok": True}
|
||||
```
|
||||
|
||||
The transaction commits on normal exit and rolls back on any exception, including `HTTPException`.
|
||||
`HTTPException` is an `Exception`, so raising one inside the block rolls back before FastAPI turns it into a response.
|
||||
|
||||
<Aside type="note">
|
||||
There is no `conn.transaction()` context manager on either the sync or async connection — an earlier version of this page showed one. Wrapping this in your own `@asynccontextmanager` helper is a few lines if you want the shorthand.
|
||||
</Aside>
|
||||
|
||||
@ -85,7 +85,17 @@ with conn:
|
||||
|
||||
## Returning generated keys
|
||||
|
||||
Informix's `SERIAL` columns are server-assigned. To get the IDs back, use a single-row insert per row and read `cur.lastrowid` — `executemany` doesn't return per-row IDs.
|
||||
Informix's `SERIAL` columns are server-assigned, and `executemany` doesn't return per-row IDs. To get an ID back, insert one row at a time and ask the server for the value it assigned:
|
||||
|
||||
```python
|
||||
cur.execute("INSERT INTO orders (customer_id, total) VALUES (?, ?)", (7, 42.0))
|
||||
cur.execute("SELECT DBINFO('sqlca.sqlerrd1') FROM systables WHERE tabid = 1")
|
||||
new_id = cur.fetchone()[0]
|
||||
```
|
||||
|
||||
`sqlca.sqlerrd1` is where Informix records the serial value from the most recent INSERT on this connection, so read it before running any other statement.
|
||||
|
||||
There is no `cursor.lastrowid` — an earlier version of this page said there was.
|
||||
|
||||
For batch inserts that need the IDs, the idiomatic pattern is:
|
||||
|
||||
|
||||
@ -34,25 +34,33 @@ The `BLOB_PLACEHOLDER` token in the SQL marks where the BLOB data goes. Other pa
|
||||
|
||||
## Reading a CLOB
|
||||
|
||||
CLOBs use the same methods as BLOBs — there is no separate `read_clob_column`. `read_blob_column` returns `bytes` for both, so decode it with the encoding the column was written in:
|
||||
|
||||
```python
|
||||
text: str = cur.read_clob_column(
|
||||
raw: bytes = cur.read_blob_column(
|
||||
"SELECT body FROM articles WHERE id = ?",
|
||||
(42,),
|
||||
)
|
||||
text: str = raw.decode("iso-8859-1") # or your DB_LOCALE's codec
|
||||
```
|
||||
|
||||
Returns a decoded `str` using the connection's `client_locale`.
|
||||
Returning `bytes` rather than `str` is deliberate: the driver transports the LOB over the `SQ_FILE` channel and never sees the column's declared character set, so guessing an encoding here would be exactly the kind of silent assumption that produces mojibake in one deployment and works fine in another.
|
||||
|
||||
## Writing a CLOB
|
||||
|
||||
Pass `clob=True` so the write routes through `filetoclob` rather than `filetoblob`, and encode the text yourself:
|
||||
|
||||
```python
|
||||
cur.write_clob_column(
|
||||
"INSERT INTO articles VALUES (?, CLOB_PLACEHOLDER)",
|
||||
clob_data="long article text...",
|
||||
params=(42,),
|
||||
cur.write_blob_column(
|
||||
"INSERT INTO articles VALUES (?, BLOB_PLACEHOLDER)",
|
||||
"long article text… café résumé".encode("iso-8859-1"),
|
||||
(42,),
|
||||
clob=True,
|
||||
)
|
||||
```
|
||||
|
||||
The placeholder token is `BLOB_PLACEHOLDER` for both BLOB and CLOB writes.
|
||||
|
||||
## Server-side prerequisites
|
||||
|
||||
<Aside type="caution">
|
||||
|
||||
@ -51,15 +51,35 @@ informix_db.connect(
|
||||
|
||||
| Method / property | Description |
|
||||
|---|---|
|
||||
| `cursor()` | Returns a new `Cursor`. |
|
||||
| `cursor(scrollable=False)` | Returns a new `Cursor`. |
|
||||
| `commit()` | Commits the current transaction. |
|
||||
| `rollback()` | Rolls back the current transaction. |
|
||||
| `close()` | Closes the connection. Idempotent. |
|
||||
| `transaction()` | Context manager — commits on success, rolls back on exception. |
|
||||
| `fast_path_call(routine, *args)` | Direct UDF/SPL invocation, bypassing PREPARE/EXECUTE/FETCH. |
|
||||
| `encoding` | Resolved Python codec for `client_locale`. |
|
||||
| `autocommit` | Read-only after connect; set via `connect(autocommit=...)`. |
|
||||
| `closed` | `True` after `close()`. |
|
||||
| `server_version` | Server version string from the login response. See note below. |
|
||||
| `server_capabilities` | Negotiated `ServerCapabilities`, or `None` if the reply couldn't be decoded. |
|
||||
|
||||
`Connection` is also a context manager (`with informix_db.connect(...) as conn:`), which closes on exit.
|
||||
|
||||
There is no `conn.transaction()` helper and no `conn.autocommit` attribute — earlier versions of this page listed both, and neither has ever existed. Set autocommit at connect time with `connect(autocommit=True)`, and manage transactions with `commit()` / `rollback()`:
|
||||
|
||||
```python
|
||||
conn = informix_db.connect(..., autocommit=False)
|
||||
try:
|
||||
cur = conn.cursor()
|
||||
cur.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?", (100, 1))
|
||||
cur.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?", (100, 2))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
```
|
||||
|
||||
:::note[server_version reports the *internal* version]
|
||||
The login response carries Informix's internal protocol version, not its marketing version. Informix 12.10 reports `9.56`, 14.10 reports `9.59`, and 15 reports `15.0.1.0.3`. At the protocol level the two older releases really are 9.x servers — which is why all three speak an identical SQLI dialect.
|
||||
:::
|
||||
|
||||
## Cursor
|
||||
|
||||
@ -70,16 +90,40 @@ informix_db.connect(
|
||||
| `fetchone()` | One row tuple, or `None`. |
|
||||
| `fetchmany(size=arraysize)` | List of row tuples. |
|
||||
| `fetchall()` | All remaining rows. |
|
||||
| `scroll(value, mode="relative")` | Scrollable cursor positioning. |
|
||||
| `read_blob_column(sql, params)` | Read a BLOB column → `bytes`. |
|
||||
| `write_blob_column(sql, blob_data, params)` | Write a BLOB column. |
|
||||
| `read_clob_column(sql, params)` | Read a CLOB column → `str`. |
|
||||
| `write_clob_column(sql, clob_data, params)` | Write a CLOB column. |
|
||||
| `scroll(value, mode="relative")` | Scrollable cursor positioning. Needs `cursor(scrollable=True)`. |
|
||||
| `fetch_first()` / `fetch_last()` | Jump to the first / last row. Scrollable cursors only. |
|
||||
| `fetch_prior()` / `fetch_relative(n)` / `fetch_absolute(n)` | Relative and absolute positioning. Scrollable cursors only. |
|
||||
| `read_blob_column(sql, params)` | Read a BLOB **or** CLOB column → `bytes`. |
|
||||
| `write_blob_column(sql, data, params, clob=False)` | Write a BLOB column; pass `clob=True` for CLOB. |
|
||||
| `close()` | Closes the cursor + releases server resources. |
|
||||
| `closed` | `True` after `close()`. |
|
||||
| `description` | Sequence of column descriptors per PEP 249. |
|
||||
| `rowcount` | Affected row count for DML; `-1` for SELECT. |
|
||||
| `rownumber` | Current 0-indexed position, or `None` before the first row. |
|
||||
| `arraysize` | Default `fetchmany()` size. |
|
||||
| `lastrowid` | Server-assigned key for the last single-row INSERT. |
|
||||
|
||||
There are no `read_clob_column` / `write_clob_column` methods and no `lastrowid` attribute — earlier versions of this page listed all three and none have existed.
|
||||
|
||||
CLOBs go through the BLOB methods. `read_blob_column` returns `bytes` either way, so decode it yourself with the column's encoding:
|
||||
|
||||
```python
|
||||
cur.write_blob_column(
|
||||
"INSERT INTO docs VALUES (?, BLOB_PLACEHOLDER)",
|
||||
"café résumé".encode("iso-8859-1"),
|
||||
(1,),
|
||||
clob=True,
|
||||
)
|
||||
raw = cur.read_blob_column("SELECT txt FROM docs WHERE id = ?", (1,))
|
||||
text = raw.decode("iso-8859-1")
|
||||
```
|
||||
|
||||
For a server-assigned `SERIAL` after an INSERT, ask the server — Informix exposes it through `DBINFO`:
|
||||
|
||||
```python
|
||||
cur.execute("INSERT INTO people (name) VALUES (?)", ("ada",))
|
||||
cur.execute("SELECT DBINFO('sqlca.sqlerrd1') FROM systables WHERE tabid = 1")
|
||||
new_id = cur.fetchone()[0]
|
||||
```
|
||||
|
||||
## Pool
|
||||
|
||||
|
||||
@ -7,21 +7,41 @@ sidebar:
|
||||
|
||||
| SQL type | Python type | Notes |
|
||||
|---|---|---|
|
||||
| `SMALLINT` / `INT` / `BIGINT` / `SERIAL` | `int` | Arbitrary precision on the Python side. |
|
||||
| `SMALLINT` / `INT` / `SERIAL` | `int` | Arbitrary precision on the Python side. |
|
||||
| `BIGINT` / `BIGSERIAL` | `int` | 8-byte two's complement on the wire. |
|
||||
| `INT8` / `SERIAL8` | `int` | The *legacy* 64-bit integer — a different wire format from `BIGINT`, not an alias. See below. |
|
||||
| `FLOAT` / `SMALLFLOAT` | `float` | IEEE 754 double / single. |
|
||||
| `DECIMAL(p,s)` / `MONEY` | `decimal.Decimal` | Exact precision preserved. |
|
||||
| `CHAR` / `VARCHAR` / `NCHAR` / `NVCHAR` / `LVARCHAR` | `str` | Decoded using `client_locale`. |
|
||||
| `CHAR` / `NCHAR` | `str` | Fixed width, space-padded; trailing spaces stripped on decode. |
|
||||
| `VARCHAR` / `NVCHAR` / `LVARCHAR` | `str` | Variable length. Decoded using `client_locale`. |
|
||||
| `BOOLEAN` | `bool` | |
|
||||
| `DATE` | `datetime.date` | |
|
||||
| `DATETIME YEAR TO …` | `datetime.datetime` / `datetime.time` / `datetime.date` | The Python type depends on the field range. |
|
||||
| `INTERVAL DAY TO FRACTION` | `datetime.timedelta` | |
|
||||
| `INTERVAL YEAR TO MONTH` | `informix_db.IntervalYM` | Custom type — `datetime.timedelta` can't represent year-month intervals. |
|
||||
| `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` | Named-tuple-like with `.name`-accessible fields. |
|
||||
| `SET(…)` / `MULTISET(…)` / `LIST(…)` | `informix_db.CollectionValue` | Iterable; preserves duplicates only for `MULTISET` / `LIST`. |
|
||||
| `BLOB` / `CLOB` (smart-LOBs) | `informix_db.BlobLocator` / `informix_db.ClobLocator` | Opaque server-side locators. Read via `cursor.read_blob_column`, write via `cursor.write_blob_column`. |
|
||||
| `ROW(…)` | `informix_db.RowValue` | Raw payload plus schema string — **not** decomposed into fields. See below. |
|
||||
| `SET(…)` / `MULTISET(…)` / `LIST(…)` | `informix_db.CollectionValue` | Raw payload plus element schema — **not** iterable. See below. |
|
||||
| `NULL` | `None` | |
|
||||
|
||||
## INT8 and SERIAL8 are not BIGINT
|
||||
|
||||
Informix has two unrelated 64-bit integer types and they share nothing on the wire:
|
||||
|
||||
| | Wire format |
|
||||
|---|---|
|
||||
| `BIGINT` / `BIGSERIAL` (type 52 / 53) | 8 bytes, two's complement, big-endian |
|
||||
| `INT8` / `SERIAL8` (type 17 / 18) | 10 bytes, sign-magnitude, halves stored high-last |
|
||||
|
||||
Both decode to plain Python `int`, so this only matters if you're reading the wire yourself. It's worth knowing that `INT8` stores `+n` and `−n` with *identical* magnitude bytes and the sign in a leading word — decoding it as a signed 64-bit integer looks correct for every positive value and is wrong for every negative one.
|
||||
|
||||
`INT8`/`SERIAL8` are common in schemas predating Informix 11.50, which is when `BIGINT` arrived.
|
||||
|
||||
:::caution[Fixed in 2026.08.27]
|
||||
Releases before `2026.08.27` did not decode `INT8`/`SERIAL8` at all and returned raw `bytes`. The same release fixed `NCHAR` (which lost its first character and could desync the row) and `BOOLEAN` (which corrupted every column after it). If you use any of those three types, upgrade.
|
||||
:::
|
||||
|
||||
## DATETIME field ranges
|
||||
|
||||
Informix's `DATETIME YEAR TO X` is field-range typed. The Python type returned depends on which fields are present:
|
||||
@ -44,16 +64,40 @@ For binding `Decimal` values into INSERT/UPDATE, the driver uses the column's de
|
||||
|
||||
## Type extensions
|
||||
|
||||
`informix_db.IntervalYM(years, months)` represents `INTERVAL YEAR TO MONTH`:
|
||||
`informix_db.IntervalYM` represents `INTERVAL YEAR TO MONTH`. It takes a **single total month count** — mirroring the server's own representation — with `years` and `remainder_months` available as derived properties:
|
||||
|
||||
```python
|
||||
from informix_db import IntervalYM
|
||||
|
||||
ym = IntervalYM(years=2, months=3)
|
||||
cur.execute("INSERT INTO contracts (term) VALUES (?)", (ym,))
|
||||
ym = IntervalYM(27) # 2 years, 3 months
|
||||
ym.years # 2
|
||||
ym.remainder_months # 3
|
||||
str(ym) # '2-03'
|
||||
|
||||
cur.execute("INSERT INTO contracts (term) VALUES (?)", (ym,))
|
||||
cur.execute("SELECT term FROM contracts WHERE id = ?", (1,))
|
||||
result = cur.fetchone()[0] # IntervalYM(years=2, months=3)
|
||||
result = cur.fetchone()[0] # IntervalYM(months=27)
|
||||
```
|
||||
|
||||
`informix_db.RowValue` and `informix_db.CollectionValue` are read-only types returned for `ROW`, `SET`, `MULTISET`, `LIST` columns. Both expose Python iteration and indexed access.
|
||||
Negative intervals are supported; the sign lives on `months` and propagates to both derived properties.
|
||||
|
||||
## ROW and collection columns
|
||||
|
||||
`informix_db.RowValue` and `informix_db.CollectionValue` are returned for `ROW`, `SET`, `MULTISET`, and `LIST` columns. Both are **opaque wrappers, not decomposed values**:
|
||||
|
||||
```python
|
||||
row_val.raw # bytes — the server's textual representation, e.g. b"ROW('Alice',30)"
|
||||
row_val.schema # str — the column's declared schema
|
||||
|
||||
coll_val.raw # bytes, e.g. b'LIST{10,20,30}'
|
||||
coll_val.kind # 'set' | 'multiset' | 'list' | 'collection'
|
||||
coll_val.element_schema # str
|
||||
```
|
||||
|
||||
They are not iterable and do not expose fields by name. Fully parsing a composite type means reimplementing JDBC's `IfxComplexInput`, which we haven't done.
|
||||
|
||||
If you need individual fields today, project them in SQL — the server does the decomposition for you and you get ordinary typed columns back:
|
||||
|
||||
```python
|
||||
cur.execute("SELECT person.name, person.age FROM staff") # str, int
|
||||
```
|
||||
|
||||
@ -76,7 +76,9 @@ Every finding from a system-wide failure-mode audit (data correctness, wire safe
|
||||
|
||||
**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.
|
||||
|
||||
300+ tests across unit / integration / benchmark suites. Integration tests run against the official IBM Informix Developer Edition Docker image (15.0.1.0.3DE).
|
||||
400+ tests across unit / integration / benchmark suites. The integration suite runs against the official IBM Informix Developer Edition Docker images and passes 247/247 on **all three** of 12.10.FC12W1DE, 14.10.FC7W1DE, and 15.0.1.0.3DE — `make test-matrix` runs the lot.
|
||||
|
||||
That matrix exists because it turned out to be needed. A user reported corrupted result sets on Informix 12; the cause was three framing bugs that affected every version including the one we tested against, and they'd survived because no fixture used the affected types. Testing one server and inferring the rest is how that happens.
|
||||
|
||||
## Read next
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user