informix-db/src/informix_db/_messages.py
Ryan Malloy a42dc5c5de Phase 18: server-side scrollable cursors via SQ_SFETCH (v2026.05.04.2)
Opt-in via conn.cursor(scrollable=True). Opens the cursor with
SQ_SCROLL (24) before SQ_OPEN (6), keeps it open server-side, and
sends SQ_SFETCH (23) per scroll call instead of materializing the
result set up-front.

User-facing API is identical to Phase 17's in-memory scroll
(fetch_first/last/prior/absolute/relative, scroll, rownumber).
Only the internal mechanism differs:

  | feature           | default          | scrollable=True
  |-------------------|------------------|------------------
  | memory            | all rows         | one row at a time
  | round-trips/fetch | 0 (after NFETCH) | 1 per call
  | cursor lifetime   | closed after exec| open until close()
  | best for          | sequential iter  | random access on
                                         | huge result sets

Wire format (verified against JDBC ScrollProbe capture):
* SQ_SFETCH: [short SQ_ID=4][int 23][short scrolltype]
  [int target][int bufSize=4096][short SQ_EOT]
  scrolltype: 1=NEXT, 4=LAST, 6=ABSOLUTE
* SQ_SCROLL (24): emitted between CURNAME and SQ_OPEN
* SQ_TUPID (25): response tag with 1-indexed row position;
  authoritative source for client-side position tracking

Position tracking uses the server's SQ_TUPID rather than client-
computed indexes. Total row count discovered lazily via SFETCH(LAST)
when negative absolute indexing requires it; cached in
_scroll_total_rows.

Trap on the way: initial SFETCH used SHORT for bufSize → server
hung silently. Same SHORT-vs-INT diagnostic pattern as Phase 4.x's
CURNAME+NFETCH. Captured JDBC trace, byte-diffed against ours,
found the mismatch (bufSize is INT in modern Informix per
isXPSVER8_40 / is2GBFetchBufferSupported).

Tests: 14 integration tests in test_scroll_cursor_server.py
covering lifecycle, sequential fetch, fetch_first/last/prior/
absolute/relative, negative indexing, scroll, empty result sets,
past-end, and random-access on a 100-row result set.

Total: 69 unit + 191 integration = 260 tests.
2026-05-04 16:41:25 -06:00

221 lines
8.1 KiB
Python

"""SQLI wire-protocol message-type constants.
Two distinct namespaces live here:
* ``SQ_*`` — the post-login SQLI message-type tags from
``com.informix.jdbc.IfxMessageTypes``. Each one is a 16-bit big-endian
short on the wire. See ``docs/PROTOCOL_NOTES.md`` §5 for categorization
by purpose.
* ``ASF_*``, ``SL_*``, ``PF_*`` — the connection-establishment markers
used inside the binary login PDU body, defined locally in
``com.informix.asf.Connection`` (NOT in ``IfxMessageTypes``). These
also appear on the wire as 16-bit shorts but they're a separate
namespace whose values may overlap numerically with ``SQ_*`` constants.
"""
from __future__ import annotations
from enum import IntEnum
class MessageType(IntEnum):
"""SQLI post-login message-type tags. Wire format: 2 bytes, big-endian."""
# --- Statement execution ---
SQ_COMMAND = 1
SQ_PREPARE = 2
SQ_CURNAME = 3
SQ_ID = 4
SQ_BIND = 5
SQ_OPEN = 6
SQ_EXECUTE = 7
SQ_DESCRIBE = 8
SQ_NFETCH = 9
SQ_CLOSE = 10
SQ_RELEASE = 11
SQ_NDESCRIBE = 22 # numerical describe — request column metadata after a PREPARE/COMMAND
SQ_WANTDONE = 49 # request a SQ_DONE completion notification
# Phase 18: server-side scrollable cursor.
SQ_SFETCH = 23 # scroll-fetch: ``[short SFETCH][short scrolltype]
# [int target][short bufSize]``. scrolltype values
# per JDBC IfxSqli.getaRow: 1=NEXT, 4=LAST, 6=ABSOLUTE.
SQ_SCROLL = 24 # cursor-open modifier — emitted *before* SQ_OPEN
# to mark the cursor as scrollable. Server keeps
# the result set materialized for random access.
SQ_TUPID = 25 # server response tag carrying the row's 1-indexed
# position. Body: ``[int tupleId]``. Sent before
# SQ_TUPLE in scrollable-cursor responses.
# --- Per-PDU framing ---
SQ_EOT = 12 # end-of-transmission / flush marker; ends every PDU
SQ_ERR = 13 # error response from the server
SQ_TUPLE = 14 # one row of result data
SQ_DONE = 15 # statement / result-set completion
SQ_XACTSTAT = 99 # transaction-state event (logged DBs only). Body:
# ``[short xcEvent][short xcNewLevel][short xcOldLevel]``. See
# ``IfxSqli.receiveXactstat`` and the Phase 7 DECISION_LOG entry.
# --- Transactions ---
SQ_CMMTWORK = 19
SQ_RBWORK = 20
SQ_SVPOINT = 21
SQ_BEGIN = 35
SQ_DBOPEN = 36
SQ_DBCLOSE = 37
SQ_DBLIST = 26
# --- Connection lifecycle (post-login) ---
SQ_VERSION = 53
SQ_EXIT = 56 # client-initiated session terminate (also returned as ack)
SQ_INFO = 81 # info request, with sub-codes (see ``InfoSubtype`` below)
SQ_CONNECT = 112
SQ_SETCONN = 113
SQ_DISCONNECT = 114
SQ_PROTOCOLS = 126
# --- Auth (server-initiated challenge/response, e.g. PAM) ---
SQ_ACCEPT = 127
SQ_ACK = 128
SQ_CHALLENGE = 129
SQ_RESPONSE = 130
# --- Savepoints (newer) ---
SQ_SQLISETSVPT = 137
SQ_SQLIRELSVPT = 138
SQ_SQLIRBACKSVPT = 139
# --- XA (distributed transactions) — Phase 6+ ---
SQ_XROLLBACK = 65
SQ_XCLOSE = 66
SQ_XCOMMIT = 67
SQ_XEND = 68
SQ_XFORGET = 69
SQ_XPREPARE = 70
SQ_XRECOVER = 71
SQ_XSTART = 72
SQ_XERR = 73
SQ_XASTATE = 74
SQ_XOPEN = 82
# --- BLOB / LOB ---
# Phase 8 (BYTE/TEXT in-row blobs)
SQ_FETCHBLOB = 38
SQ_BLOB = 39
SQ_BBIND = 41
SQ_SBBIND = 52
SQ_FILE_READ = 106
SQ_FILE_WRITE = 107
# Phase 9+ (smart-LOB BLOB/CLOB)
SQ_LODATA = 97 # smart-LOB data transfer with sub-commands:
# 0=LO_READ, 1=LO_READWITHSEEK, 2=LO_WRITE.
# Body: [short subCom][short loFd][int length]
# [short bufSize=32000] (+ [int8 offset][short whence]
# for LO_READWITHSEEK). See IfxSqli.sendLoData line 4864.
SQ_GETROUTINE = 101 # request a routine handle by signature.
# Body: [byte isRoutineById][int sigLen]
# [sig bytes][pad if odd][short fparamFlag].
# Response is the same tag with body
# [short dbNameLen][dbName][int handle].
SQ_EXFPROUTINE = 102 # execute a fast-path routine with bound
# params. Body: [char dbName][int handle]
# [short paramCount][short fparamFlag]
# [SQ_BIND-format params].
SQ_FPROUTINE = 103 # response tag: fast-path return-value descriptor.
# Body: [short numReturns] then per return:
# [short type][maybe UDT info][short ind]
# [short prec][data].
SQ_FPARAM = 104 # parameter metadata for SQ_FPROUTINE
# --- RPC sub-protocol (range 200-205) — Phase 6+ ---
SQ_INVOKE = 200
SQ_REPLY = 201
SQ_EXCEPTION = 202
SQ_VERSION_REQ = 203
SQ_VERSION_REPLY = 204
SQ_AMFPARAM = 205
class InfoSubtype(IntEnum):
"""Sub-codes for the ``SQ_INFO`` message body."""
INFO_DONE = 0
INFO_REQUEST = 1
INFO_VERSION = 2
INFO_TYPE = 3
INFO_CAPABILITY = 4
INFO_DB = 5
INFO_ENV = 6
class LoginMarker(IntEnum):
"""Login-PDU section markers from ``com.informix.asf.Connection``.
These appear inside the binary login PDU as 16-bit shorts and tag
the structural sub-blocks (association header, capability section,
env vars, process info, etc.). Numerically distinct from ``MessageType``;
do not mix the two.
See ``docs/PROTOCOL_NOTES.md`` §3 for the full byte-by-byte layout.
"""
SQ_ASSOC = 100 # start of "association" record
SQ_ASCBINARY = 101 # binary-format indicator
SQ_ASCINITRESP = 102 # init response (error block follows in some paths)
SQ_ASCDBLIST = 103
SQ_ASCINITREQ = 104 # init request marker (paired with ASF_XCONNECT=11)
SQ_ASCENV = 106 # environment-vars sub-block
SQ_ASCPINFO = 107 # process-info sub-block
SQ_ASCBPARMS = 108 # binary parameters block
SQ_ASSOCBIND = 110
SQ_ASSOCRESP = 111
SQ_ASCMISC_60 = 116 # misc / AppName section
SQ_ASCEOT = 127 # end-of-PDU marker for the login
class SLHeader(IntEnum):
"""SL header constants for the 6-byte login envelope."""
# slType byte (offset 2 of SLheader)
SLTYPE_CONREQ = 1 # client → server: connection request
SLTYPE_CONACC = 2 # server → client: connection accepted
SLTYPE_CONREJ = 3 # server → client: connection rejected
SLTYPE_REDIRECT = 13 # server → client: redirect to another node
# slAttribute byte (offset 3 of SLheader): protocol version
PF_PROT_SQLI_0600 = 60 # SQLI protocol family 6.00 (current)
PF_PROT_SQLI_WITH_CSS = 61 # variant with column-storage support
class StmtOptions(IntEnum):
"""Bit flags packed into the login-PDU stmtoptions int."""
ASF_AMBIG_SEOL = 3 # bits 0+1 (always set)
ASF_GRPREF = 0x02000000 # bit 25 — sqlhosts group reference
ASF_TRUSTCTXT = 0x04000000 # bit 26 — trusted context
# Hardcoded constant strings emitted in the binary login PDU.
# These are NOT free parameters — they identify our wire client to the server
# and must match what IBM's JDBC driver sends, byte-for-byte.
APPL_TYPE = b"sqlexec\x00\x00\x00\x00\x00" # 12 bytes, fixed-width nul-padded
APPL_ID = b"sqli" # 4 bytes (length-prefix says 5 because of trailing nul)
PROT_SQLIOL = b"ol\x00\x00\x00\x00\x00\x00" # 8 bytes; SQLI/OL protocol identifier
NET_TLITCP = b"tlitcp\x00\x00" # 8 bytes; "TLI over TCP" network type
FLOAT_TYPE = b"IEEEM" # 5 bytes; declares we want IEEE 754 float encoding
CLIENT_VERSION = b"9.280" # 5 bytes; hardcoded by JDBC, NOT a real version
CLIENT_SERIAL = b"RDS#R000000" # 11 bytes; hardcoded marketing/license artifact
# Buffer / size constants from com.informix.asf.Connection
MAX_BUFF_SIZE = 32768
MIN_BUFF_SIZE = 140
STREAM_BUF_SIZE = 4096
PFCONREQ_BUF_SIZE = 2048
SL_HEADER_SIZE = 6
# UTYPE_INTERNET — declares we're connecting over IP (vs. shared-memory etc.)
UTYPE_INTERNET = 1
# ASF_XCONNECT — paired with SQ_ASCINITREQ in the login PDU init-request marker
ASF_XCONNECT = 11