diff --git a/docs-site/src/content/docs/explain/phase-log.md b/docs-site/src/content/docs/explain/phase-log.md index c67549f..ebb28c6 100644 --- a/docs-site/src/content/docs/explain/phase-log.md +++ b/docs-site/src/content/docs/explain/phase-log.md @@ -96,6 +96,40 @@ The cheapest fix was also the most valuable. Asserting that a row decoder lands Test count went from 241 to 457 across three server versions over this stretch. +### 2026.09.03 + +Two more, one of each kind. + +The `:N` placeholder rewriter was a bare regular expression, so it +rewrote the inside of string literals: `'http://host:8080/x'` was stored +as `'http://host?/x'`. Same family as the statement-classification bug +above, and the last place the driver inferred meaning from SQL text +instead of handling it. Replaced with a scanner using Informix's own +lexical rules, measured rather than assumed. One of those rules would +have been got wrong from habit: a backslash escapes nothing, so `'a\'b'` +is an unterminated string and draws `-282`. A scanner written to +Postgres reflexes desyncs there and corrupts everything after it. + +`row_factory=Row` adds column-name and attribute access, on request from +a user coming from `pyodbc`. Opt-in, because it costs about 9% on bulk +fetch and that is not a trade to make for someone who never reads a +column by name. + +Testing the second one found the pattern again, this time in code +written an hour earlier. The names a column could shadow were +hand-listed as `count` and `index`, the two `tuple` methods; then `keys`, +`_asdict` and `_fields` were added to the same class and the list was +never revisited. A column called `keys` returned a bound method, in +silence. The fix was to compute the set from the class rather than +lengthen the list, and to parametrize the test over that computed set so +it grows with the class. + +The interesting part is where testing found it. A differential test +across twenty columns covering every awkward type, populated and fully +NULL, found nothing. The bug was in six lines of name bookkeeping that +looked obviously correct, and had been correct when written. Anything +that must stay in sync with a class by hand eventually will not. + ## Notable architectural pivots The decision log calls out four moments where the obvious choice would have been wrong: diff --git a/docs-site/src/content/docs/how-to/migrate-from-ifxpy.md b/docs-site/src/content/docs/how-to/migrate-from-ifxpy.md index d8e688e..df027e9 100644 --- a/docs-site/src/content/docs/how-to/migrate-from-ifxpy.md +++ b/docs-site/src/content/docs/how-to/migrate-from-ifxpy.md @@ -48,6 +48,36 @@ The exception hierarchy is identical: `Error`, `Warning`, `InterfaceError`, `Dat - **Type-safe annotations**: `informix-driver` ships with `py.typed` - **Python 3.12+ support** - **Pipelined `executemany`**: 1.6× faster than IfxPy's per-row implementation +- **Rows addressable by name**: `row["col"]` and `row.col`, not just `row[0]` + +## Reading rows by name + +IfxPy gives you positional access and nothing else, which is why most +IfxPy codebases grow a helper like this: + +```python +cols = [c[0].lower() for c in cur.description] +row_dict = dict(zip(cols, cur.fetchone())) +``` + +That gets you a dict and still no attribute access. Ask for `Row` +instead and all three work at once: + +```python +conn = informix_db.connect(..., row_factory=informix_db.Row) +cur.execute("SELECT config_key, config_value FROM settings") +row = cur.fetchone() + +row[0], row["config_key"], row.config_key +``` + +Set on the connection, so it applies to every cursor from it. The +`.lower()` in the hand-written version is already a no-op, incidentally: +Informix folds unquoted identifiers to lower case. + +It is opt-in because it costs about 9% on bulk fetch. See +[the API reference](/reference/api/#rows) for the numbers and the +edge cases. ## Migrating incrementally diff --git a/docs-site/src/content/docs/start/quickstart.mdx b/docs-site/src/content/docs/start/quickstart.mdx index ee65e4f..e969875 100644 --- a/docs-site/src/content/docs/start/quickstart.mdx +++ b/docs-site/src/content/docs/start/quickstart.mdx @@ -121,6 +121,11 @@ with informix_db.connect(host="127.0.0.1", port=9088, user="informix", `?` and `:1` both work. Informix's native paramstyle is `numeric`, but `?` is supported as a synonym. +Rows come back as plain tuples. If you would rather read them by column +name, pass `row_factory=informix_db.Row` to `connect()` and you get +`row["name"]` and `row.name` alongside `row[0]`. See +[the API reference](/reference/api/#rows). + ## 5. Use the connection pool For real applications, prefer the pool: