informix-db/tests/test_fastpath.py
Ryan Malloy fa3ab751f9 Phase 13: SQ_FPROUTINE / SQ_EXFPROUTINE fast-path RPC
Implements direct stored-procedure invocation via the parallel
fast-path protocol family. Three new wire messages:
* SQ_GETROUTINE (101) — handle resolution by signature
* SQ_EXFPROUTINE (102) — execute by handle with bound params
* SQ_FPROUTINE (103) — response with return values

API: Connection.fast_path_call(signature, *params) -> list

Routine handles cached per-connection in a dict[signature -> (db_name,
handle)] — first call resolves and caches, subsequent calls skip
GETROUTINE.

Why this matters even though Phase 10/11 already do most smart-LOB
work via SQL: ifx_lo_close(int) can't be invoked via "EXECUTE FUNCTION"
(returns -674). Without the fast-path, opened locators leak server-side
until the session ends. The fast-path also enables tighter UDF-in-loop
workloads — no PREPARE→DESCRIBE→EXECUTE overhead, just GETROUTINE+
EXFPROUTINE (one round-trip after caching).

Wire format examples (verified against JDBC):
* GETROUTINE request:
    [short 101][byte 0][int sigLen][sig bytes][pad if odd]
    [short 0][short SQ_EOT]
* EXFPROUTINE request:
    [short 102][short dbNameLen][dbName][pad if odd][int handle]
    [short paramCount][short fparamFlag][SQ_BIND data][short SQ_EOT]
* FPROUTINE response:
    [short numReturns][per-return: type/UDT-info/ind/prec/data]
    + drain SQ_DONE/SQ_COST/SQ_XACTSTAT until SQ_EOT

MVP scope:
* Scalar params/returns only (int/float/str/bool/None/etc.)
* UDT params (e.g., 72-byte BLOB locator) deferred to Phase 13.x
* SQ_LODATA chunked I/O deferred — Phase 10/11 already cover read/write

Tests: 5 integration tests covering error paths, success paths,
handle caching, and multiple cycles.

Total: 64 unit + 139 integration = 203 tests.

Architectural milestone: with Phase 13 complete, the project now
covers every wire-message family JDBC uses for ordinary database
work. Only TLS handshake and cluster-redirect (replication failover)
remain unimplemented — neither is needed for a single-instance
driver.
2026-05-04 14:36:21 -06:00

154 lines
5.3 KiB
Python

"""Phase 13 integration tests — fast-path RPC via SQ_FPROUTINE / SQ_EXFPROUTINE.
The fast-path family is a parallel protocol for invoking server-side
stored functions without going through PREPARE → EXECUTE → FETCH.
Three messages:
* ``SQ_GETROUTINE`` (101) — handle resolution by signature
* ``SQ_EXFPROUTINE`` (102) — execute by handle with bound params
* ``SQ_FPROUTINE`` (103) — response with return values
Used by JDBC's ``IfxSmartBlob`` to call ``ifx_lo_open`` / ``ifx_lo_close``
/ ``ifx_lo_create`` etc. We expose it as
``Connection.fast_path_call(signature, *params)`` for direct UDF/SPL
invocation.
Routine handles are cached per-connection by signature so repeated
calls skip the lookup round-trip.
"""
from __future__ import annotations
import contextlib
import pytest
import informix_db
from tests.conftest import ConnParams
pytestmark = pytest.mark.integration
def _connect(params: ConnParams) -> informix_db.Connection:
return informix_db.connect(
host=params.host,
port=params.port,
user=params.user,
password=params.password,
database=params.database,
server=params.server,
connect_timeout=10.0,
read_timeout=10.0,
autocommit=True,
)
def test_fast_path_close_invalid_lofd_raises(
logged_db_params: ConnParams,
) -> None:
"""Calling ``ifx_lo_close(-1)`` raises with sqlcode -9810."""
with _connect(logged_db_params) as conn:
with pytest.raises(informix_db.Error) as excinfo:
conn.fast_path_call(
"function informix.ifx_lo_close(integer)", -1
)
# The message should mention -9810 (smart-large-object error)
assert "-9810" in str(excinfo.value)
def test_fast_path_close_real_lofd_returns_zero(
logged_db_params: ConnParams,
) -> None:
"""End-to-end: open a real smart-LOB, close it via fast-path, expect 0."""
with _connect(logged_db_params) as conn:
cur = conn.cursor()
with contextlib.suppress(Exception):
cur.execute("DROP TABLE p13_t1")
try:
cur.execute("CREATE TABLE p13_t1 (id INT, data BLOB)")
except informix_db.Error as e:
pytest.skip(f"sbspace unavailable ({e!r})")
cur.write_blob_column(
"INSERT INTO p13_t1 VALUES (?, BLOB_PLACEHOLDER)",
b"fast-path test", (1,),
)
# Open via SQL (returns the lofd as an int)
cur.execute("SELECT ifx_lo_open(data, 4) FROM p13_t1")
(lofd,) = cur.fetchone()
assert lofd > 0 # valid file descriptor
# Close via fast-path RPC
result = conn.fast_path_call(
"function informix.ifx_lo_close(integer)", lofd
)
assert result == [0]
cur.execute("DROP TABLE p13_t1")
def test_fast_path_handle_caching(
logged_db_params: ConnParams,
) -> None:
"""Second call with same signature reuses the cached handle (no GETROUTINE)."""
with _connect(logged_db_params) as conn:
sig = "function informix.ifx_lo_close(integer)"
# First call — populates cache
with contextlib.suppress(informix_db.Error):
conn.fast_path_call(sig, -1)
assert sig in conn._fp_handle_cache
cached_handle = conn._fp_handle_cache[sig]
assert cached_handle[0] == "testdb" # db name
assert isinstance(cached_handle[1], int)
first_handle = cached_handle[1]
# Second call — should use the cache (no second GETROUTINE)
with contextlib.suppress(informix_db.Error):
conn.fast_path_call(sig, -2)
# Handle in cache unchanged — same int = same cache entry hit
assert conn._fp_handle_cache[sig][1] == first_handle
def test_fast_path_unknown_function_raises(
logged_db_params: ConnParams,
) -> None:
"""Bad signature → server error from SQ_GETROUTINE."""
with _connect(logged_db_params) as conn, pytest.raises(informix_db.Error):
conn.fast_path_call(
"function informix.no_such_function_at_all_xyz(integer)", 1
)
def test_fast_path_open_and_close_cycle(
logged_db_params: ConnParams,
) -> None:
"""Demonstrate a full open → close cycle through fast-path + SQL.
``ifx_lo_open`` accepts a UDT (BLOB locator) parameter which our
fast-path encoder doesn't yet support — but the *server* can supply
the locator from the column itself, so we open via plain SQL
(``SELECT ifx_lo_open(col, mode)``) and close via fast-path. This
is the same pattern Phase 10 used for read.
"""
with _connect(logged_db_params) as conn:
cur = conn.cursor()
with contextlib.suppress(Exception):
cur.execute("DROP TABLE p13_t2")
try:
cur.execute("CREATE TABLE p13_t2 (id INT, data BLOB)")
except informix_db.Error as e:
pytest.skip(f"sbspace unavailable ({e!r})")
cur.write_blob_column(
"INSERT INTO p13_t2 VALUES (?, BLOB_PLACEHOLDER)",
b"open close cycle data", (1,),
)
# Multiple open/close cycles to verify cleanup
for _ in range(3):
cur.execute("SELECT ifx_lo_open(data, 4) FROM p13_t2")
(lofd,) = cur.fetchone()
result = conn.fast_path_call(
"function informix.ifx_lo_close(integer)", lofd
)
assert result == [0]
cur.execute("DROP TABLE p13_t2")