From 85d3a8f7d7a0834da1de9bb111ad83436ac5b596 Mon Sep 17 00:00:00 2001 From: Ryan Malloy Date: Thu, 27 Aug 2026 09:29:02 -0600 Subject: [PATCH] Docs site: correct APIs that don't exist, document what does MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../content/docs/explain/async-strategy.mdx | 2 +- .../src/content/docs/how-to/async-fastapi.mdx | 16 +++-- .../src/content/docs/how-to/executemany.mdx | 12 +++- .../src/content/docs/how-to/smart-lobs.mdx | 20 ++++-- docs-site/src/content/docs/reference/api.md | 62 +++++++++++++++--- docs-site/src/content/docs/reference/types.md | 64 ++++++++++++++++--- docs-site/src/content/docs/start/wtf.md | 4 +- 7 files changed, 148 insertions(+), 32 deletions(-) diff --git a/docs-site/src/content/docs/explain/async-strategy.mdx b/docs-site/src/content/docs/explain/async-strategy.mdx index 7fc816b..cb6a75d 100644 --- a/docs-site/src/content/docs/explain/async-strategy.mdx +++ b/docs-site/src/content/docs/explain/async-strategy.mdx @@ -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. diff --git a/docs-site/src/content/docs/how-to/async-fastapi.mdx b/docs-site/src/content/docs/how-to/async-fastapi.mdx index 9b8fc5f..e9d0f1a 100644 --- a/docs-site/src/content/docs/how-to/async-fastapi.mdx +++ b/docs-site/src/content/docs/how-to/async-fastapi.mdx @@ -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. + + diff --git a/docs-site/src/content/docs/how-to/executemany.mdx b/docs-site/src/content/docs/how-to/executemany.mdx index 0b7ad6c..e780f5d 100644 --- a/docs-site/src/content/docs/how-to/executemany.mdx +++ b/docs-site/src/content/docs/how-to/executemany.mdx @@ -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: diff --git a/docs-site/src/content/docs/how-to/smart-lobs.mdx b/docs-site/src/content/docs/how-to/smart-lobs.mdx index c583957..95a0256 100644 --- a/docs-site/src/content/docs/how-to/smart-lobs.mdx +++ b/docs-site/src/content/docs/how-to/smart-lobs.mdx @@ -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