Ryan Malloy 9048335462 Phase 12: ROW / COLLECTION type recognition
Composite UDTs (ROW=22, COLLECTION=23, SET=19, MULTISET=20, LIST=21)
now decode into typed wrapper objects (informix_db.RowValue,
informix_db.CollectionValue) that expose schema + raw payload bytes.

The wire format is the now-familiar [byte ind][int length][bytes]
pattern (same as UDTVAR(lvarchar) from Phase 10). The bytes are a
TEXTUAL representation of the value when selected without the
extended-binary opt-in JDBC uses:
  ROW value:  b"ROW('Alice',30         )"
  SET value:  b"SET{'red','green','blue'}"
  LIST value: b"LIST{10         ,20         ,30         }"

JDBC's binary-with-schema format runs ~30x larger (1420 bytes for a
2-field ROW vs. our 24). We don't request it — the textual form is
what the server returns by default and is sufficient for type
recognition.

Phase 12 ships type recognition only. Full recursive parsing into
Python tuples/lists/sets is deferred to Phase 13 (would require a
SQL-literal lexer + recursive type-driven decoding). Production
workloads that need typed field access today can project via SQL:

  cur.execute("SELECT id, r.name, r.age FROM tbl")

Tests: 8 integration tests in test_composite_types.py covering ROW
recognition, NULL, sub-field projection workaround, long values
(>255 bytes — verifies 4-byte length prefix), SET/MULTISET/LIST
recognition, and null collections.

Total: 64 unit + 134 integration = 198 tests.

Lesson reinforced: once one UDT-shaped type is implemented (UDTVAR
in Phase 10, smart-LOB in Phase 9), every subsequent UDT-shaped type
is mostly a copy of the existing decoder branch. The hard part is
payload semantics, not framing.
2026-05-04 14:30:44 -06:00

122 lines
3.0 KiB
Python

"""informix-db — pure-Python driver for IBM Informix IDS over the SQLI wire protocol.
PEP 249 (DB-API 2.0) module surface. Phase 1 implements the connection
lifecycle (open + login + close) only; cursor/execute/fetch land in Phase 2.
Quick start::
import informix_db
conn = informix_db.connect(
host="127.0.0.1", port=9088,
user="informix", password="in4mix",
database="sysmaster", server="informix",
)
conn.close()
For the full design, see ``docs/PROTOCOL_NOTES.md`` and
``docs/DECISION_LOG.md``.
"""
from __future__ import annotations
from importlib.metadata import PackageNotFoundError, version
from .connections import Connection
from .converters import (
BlobLocator,
ClobLocator,
CollectionValue,
IntervalYM,
RowValue,
)
from .exceptions import (
DatabaseError,
DataError,
Error,
IntegrityError,
InterfaceError,
InternalError,
NotSupportedError,
OperationalError,
ProgrammingError,
Warning,
)
# PEP 249 module-level globals
apilevel = "2.0"
threadsafety = 1 # threads may share the module but not connections
paramstyle = "numeric" # locked in DECISION_LOG.md — matches Informix ESQL/C
try:
__version__ = version("informix-db")
except PackageNotFoundError:
# Editable install or running uninstalled; fall back to a sentinel.
__version__ = "0.0.0+local"
__all__ = [
"BlobLocator",
"ClobLocator",
"CollectionValue",
"Connection",
"DataError",
"DatabaseError",
"Error",
"IntegrityError",
"InterfaceError",
"InternalError",
"IntervalYM",
"NotSupportedError",
"OperationalError",
"ProgrammingError",
"RowValue",
"Warning",
"__version__",
"apilevel",
"connect",
"paramstyle",
"threadsafety",
]
def connect(
host: str,
port: int = 9088,
*,
user: str,
password: str | None,
database: str | None = None,
server: str = "informix",
connect_timeout: float | None = None,
read_timeout: float | None = None,
keepalive: bool = False,
client_locale: str = "en_US.8859-1",
env: dict[str, str] | None = None,
autocommit: bool = False,
) -> Connection:
"""Open a connection to an Informix server.
The ``server`` argument is the Informix DBSERVERNAME the listener
identifies itself as (default ``"informix"`` matches the IBM
Developer Edition Docker image). It's distinct from ``host`` (which
is the network address) and from ``database`` (which is the
database name to open after login).
``database`` may be ``None`` to log in without selecting a database;
the server still requires a successful login for this to work.
"""
return Connection(
host=host,
port=port,
user=user,
password=password,
database=database,
server=server,
connect_timeout=connect_timeout,
read_timeout=read_timeout,
keepalive=keepalive,
client_locale=client_locale,
env=env,
autocommit=autocommit,
)