Closes the last open item from the Informix 12 field report. The driver
hardcodes several wire-framing choices that SQLI actually negotiates.
Those choices are correct on every server we've measured, but "correct
as far as we know" and "checked" are different things, and a framing
mismatch corrupts rows silently.
We were already sending SQ_PROTOCOLS with the same 8-byte client offer
IBM's JDBC driver uses, and discarding the reply. Now we decode it.
New informix_db.ServerCapabilities, reachable from any connection:
conn.server_capabilities.four_byte_offset
conn.server_capabilities.varchar_var_len
conn.server_capabilities.violated_assumptions() # [] when we agree
conn.server_version
violated_assumptions() names each place we emit or parse a fixed wire
shape that is actually capability-gated. Non-empty at connect time logs
a warning naming the bit, so an untested server produces a diagnosable
complaint instead of quiet corruption.
Nothing branches on these bits yet — this is observation and validation
only. The hardcoded framing is correct on all three supported servers,
and rewriting working parse paths to be conditional without a server
that needs it trades certainty for risk.
The measurement, and why 12/14/15 are interchangeable:
15.0.1.0.3 bdbe9ffe7fb7ffef ff
14.10.FC7W1 bdbe9ffe7fb7ffef f8
12.10.FC12W1DE bdbe9ffe7fb7ffef f0
^^^^^^^^^^^^^^^^ identical
The first 64 bits are byte-identical. Those releases don't merely behave
alike, they negotiate exactly the same capability set.
Two details worth recording. The reply is NINE bytes; JDBC's
enhancedProtocolMechanism switches on case 0..7 and drops the ninth, so
its BitSet(64) never sees it — yet that dropped byte is the only part
that differs between releases. And Cap_1 in the login response is not a
server version, it's the client's declared protocol level echoed back,
which is why JDBC tests == 316 rather than >=. The version string there
is the internal one: 12.10 reports 9.56, 14.10 reports 9.59. At the
protocol level both really are 9.x servers.
This also retires isUSVER as a red herring: it's one of six bits JDBC
pre-sets for any non-zero Cap_1, and Java's BitSet.set never clears, so
it is true on every modern server regardless of the mask.
Separately, fixed __version__. The distribution was renamed informix-db
-> informix-driver on 2026-05-08 but __init__ kept looking up the old
name. importlib.metadata needs the distribution name, not the module
name, and the miss fails silently — so every install of the renamed
package has reported "0.0.0+local". It escaped notice because a stale
informix-db distribution lingered in the dev venv and answered the
query. tests/test_package_metadata.py now pins the name against
[project].name; verified it catches the bug by reintroducing it.
247/247 integration on 15, 14.10, and 12.10. 120 unit tests.
99 lines
3.4 KiB
Python
99 lines
3.4 KiB
Python
"""Integration tests for live SQ_PROTOCOLS negotiation.
|
|
|
|
These run against whatever server ``IFX_PORT`` points at, so `make
|
|
test-matrix` exercises them on 12.10, 14.10, and 15 in turn.
|
|
|
|
The important one is ``test_no_violated_assumptions``: this driver
|
|
hardcodes several wire-framing choices that SQLI actually negotiates, and
|
|
a mismatch corrupts rows silently. That test turns "we assume this" into
|
|
"we check this on every supported server".
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
import informix_db
|
|
from informix_db._capabilities import ENHANCED_PROTOCOL_CAP, ServerCapabilities
|
|
from tests.conftest import ConnParams
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
def _connect(conn_params: ConnParams) -> informix_db.Connection:
|
|
return informix_db.connect(
|
|
host=conn_params.host,
|
|
port=conn_params.port,
|
|
user=conn_params.user,
|
|
password=conn_params.password,
|
|
database=conn_params.database,
|
|
server=conn_params.server,
|
|
connect_timeout=10.0,
|
|
read_timeout=10.0,
|
|
)
|
|
|
|
|
|
def test_capabilities_are_decoded(conn_params: ConnParams) -> None:
|
|
with _connect(conn_params) as conn:
|
|
caps = conn.server_capabilities
|
|
assert isinstance(caps, ServerCapabilities)
|
|
assert caps.raw_mask, "server sent an empty protocols reply"
|
|
assert caps.bits
|
|
|
|
|
|
def test_no_violated_assumptions(conn_params: ConnParams) -> None:
|
|
"""Every wire-framing shape we hardcode is one the server negotiated.
|
|
|
|
If this fails, rows are being decoded against the wrong framing and
|
|
the fix is to branch on the capability rather than assume it.
|
|
"""
|
|
with _connect(conn_params) as conn:
|
|
caps = conn.server_capabilities
|
|
assert caps is not None
|
|
assert caps.violated_assumptions() == []
|
|
|
|
|
|
def test_enhanced_protocol_negotiated(conn_params: ConnParams) -> None:
|
|
"""Cap_1 is the client's declared protocol level echoed back. Every
|
|
supported server accepts 316."""
|
|
with _connect(conn_params) as conn:
|
|
caps = conn.server_capabilities
|
|
assert caps is not None
|
|
assert caps.cap_1 == ENHANCED_PROTOCOL_CAP
|
|
assert caps.enhanced_protocol
|
|
|
|
|
|
def test_framing_capabilities_present(conn_params: ConnParams) -> None:
|
|
with _connect(conn_params) as conn:
|
|
caps = conn.server_capabilities
|
|
assert caps is not None
|
|
assert caps.four_byte_offset
|
|
assert caps.varchar_var_len
|
|
assert caps.remove_64k_limit
|
|
assert caps.usver
|
|
|
|
|
|
def test_server_version_is_exposed(conn_params: ConnParams) -> None:
|
|
"""The version here is the *internal* protocol version, not the
|
|
marketing one: 12.10 reports 9.56 and 14.10 reports 9.59. Assert the
|
|
shape rather than a value so this holds across the matrix."""
|
|
with _connect(conn_params) as conn:
|
|
version = conn.server_version
|
|
assert "Informix" in version
|
|
assert "Version" in version
|
|
|
|
|
|
def test_capabilities_survive_multiple_connections(
|
|
conn_params: ConnParams,
|
|
) -> None:
|
|
"""Negotiation happens per-connection; two connections to the same
|
|
server must agree."""
|
|
with _connect(conn_params) as first, _connect(conn_params) as second:
|
|
assert first.server_capabilities is not None
|
|
assert second.server_capabilities is not None
|
|
assert (
|
|
first.server_capabilities.raw_mask
|
|
== second.server_capabilities.raw_mask
|
|
)
|
|
assert first.server_capabilities.bits == second.server_capabilities.bits
|