Docs: put named row access where someone leaving IfxPy will find it
The API reference gained the Row section with the release, which is the right home for the details and the wrong place to discover the feature exists. Three gaps closed. The IfxPy migration guide is the page for people in exactly the position that prompted this: IfxPy is positional-only, so those codebases grow a description/zip helper that yields a dict and still no attribute access. That page now shows the helper it replaces alongside the one-liner, and notes the .lower() in the hand-written version is already a no-op since Informix folds unquoted identifiers. Quickstart gains one sentence, next to the paramstyle note where someone is already thinking about how rows come back. The phase log records 2026.09.03, including where the testing actually found the shadowed-names bug: not in the differential test across twenty awkward types, which found nothing, but in six lines of name bookkeeping that were correct when written and stopped being correct an hour later. Verified live by content, and the #rows anchor the new cross-links point at resolves.
This commit is contained in:
parent
acc21b8b81
commit
c85aaf2ecb
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user