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.
154 lines
5.3 KiB
Python
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")
|