informix-db/CHANGELOG.md
Ryan Malloy 461c62c8d3 Phase 17: scroll cursor API (v2026.05.04.1)
Adds scroll/random-access methods on Cursor:
* scroll(value, mode='relative'|'absolute') — PEP 249 compatible
* fetch_first() / fetch_last() — jump to result-set ends
* fetch_prior() — step backward (SQL-standard: from past-end yields
  the last row, matching JDBC ResultSet.previous() semantics)
* fetch_absolute(n) — 0-indexed jump; negative n indexes from end
* fetch_relative(n) — n-step from current position
* rownumber property — current 0-indexed position

Implementation: replaced _row_iter (single-pass iterator) with
_row_index (random-access index) on the cursor. The result set
is already materialized in _rows during execute(); scroll just
repositions the index. No new wire protocol needed.

For server-side scroll over genuinely huge result sets, SQ_SFETCH
(tag 23) would be needed — JDBC has executeScrollFetch (line 3908)
but we only need it if someone hits the in-memory materialization
ceiling. Phase 18 if so.

Out-of-range scroll raises IndexError per PEP 249. Invalid mode
strings raise ProgrammingError. fetchall() now correctly returns
only the rows from the current position to end (not all rows).

14 new integration tests in test_scroll_cursor.py covering:
* fetchone advancing rownumber sequentially
* fetch_first reset
* fetch_last
* fetch_prior including the past-end-to-last-row semantics
* fetch_absolute with positive and negative indexes
* fetch_relative
* PEP 249 scroll(value, mode='relative'/'absolute')
* IndexError on out-of-range
* ProgrammingError on bad mode
* Empty-result-set edge cases
* fetchall after partial iteration

Total: 69 unit + 177 integration = 246 tests.
2026-05-04 15:51:24 -06:00

4.8 KiB

Changelog

All notable changes to informix-db. Versioning is CalVerYYYY.MM.DD for date-based releases, YYYY.MM.DD.N for same-day post-releases per PEP 440.

2026.05.04.1 — Scroll cursors

Added

  • Scroll cursor API on Cursor (Phase 17):

    • cur.scroll(value, mode='relative'|'absolute') — PEP 249 compatible
    • cur.fetch_first() / cur.fetch_last() — jump to ends
    • cur.fetch_prior() — backward step (SQL-standard semantics: from past-end yields the last row)
    • cur.fetch_absolute(n) — 0-indexed jump; negative n indexes from the end
    • cur.fetch_relative(n) — n-step from current position
    • cur.rownumber — current 0-indexed position (None if before-first or no result set)

    In-memory implementation — no new wire-protocol; the existing materialized result set in cur._rows is now indexed rather than iterated. For server-side scroll over huge result sets, SQ_SFETCH (tag 23) would be needed — Phase 18 if anyone hits the in-memory ceiling.

Tests

14 new integration tests in test_scroll_cursor.py. Total: 69 unit + 177 integration = 246 tests.

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.
  • TLStls=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 recognitionROW, SET, MULTISET, LIST columns return typed RowValue / CollectionValue wrappers exposing schema and raw bytes.
  • Type codecsINTERVAL (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

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).