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