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.
77 lines
2.8 KiB
Python
77 lines
2.8 KiB
Python
"""Guards on the package's own metadata.
|
|
|
|
The distribution is ``informix-driver``; the import module is
|
|
``informix_db``. That mismatch is a live trap: ``importlib.metadata``
|
|
needs the *distribution* name, so the string can't be derived from
|
|
``__name__``, and getting it wrong fails silently — ``__version__``
|
|
degrades to the ``0.0.0+local`` sentinel instead of raising.
|
|
|
|
That is exactly what shipped between the 2026-05-08 rename and
|
|
2026.08.27: ``__init__`` still asked for ``informix-db``, so anyone who
|
|
installed the renamed package saw ``0.0.0+local``. It went unnoticed
|
|
because a stale ``informix-db`` distribution lingered in the dev
|
|
environment and answered the query.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import tomllib
|
|
|
|
import informix_db
|
|
|
|
PYPROJECT = Path(__file__).resolve().parent.parent / "pyproject.toml"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def pyproject() -> dict:
|
|
if not PYPROJECT.is_file():
|
|
pytest.skip("pyproject.toml not present (installed-package test run)")
|
|
with PYPROJECT.open("rb") as handle:
|
|
return tomllib.load(handle)
|
|
|
|
|
|
def test_version_is_not_the_unresolved_sentinel() -> None:
|
|
"""If this fails, the distribution name in ``__init__`` no longer
|
|
matches ``[project].name`` and every user sees a bogus version."""
|
|
assert informix_db.__version__ != "0.0.0+local", (
|
|
"__version__ fell back to its sentinel — the distribution name "
|
|
"looked up in informix_db/__init__.py does not match the installed "
|
|
"package. Check [project].name in pyproject.toml."
|
|
)
|
|
|
|
|
|
def test_version_matches_pyproject(pyproject: dict) -> None:
|
|
declared = pyproject["project"]["version"]
|
|
# PEP 440 normalisation drops leading zeros: 2026.08.27 -> 2026.8.27.
|
|
normalised = ".".join(
|
|
str(int(part)) if part.isdigit() else part
|
|
for part in declared.split(".")
|
|
)
|
|
assert informix_db.__version__ in {declared, normalised}
|
|
|
|
|
|
def test_init_looks_up_the_declared_distribution_name(pyproject: dict) -> None:
|
|
"""Catch the rename trap directly: the name passed to
|
|
``importlib.metadata.version()`` must equal ``[project].name``."""
|
|
source = (
|
|
Path(informix_db.__file__).resolve().parent / "__init__.py"
|
|
).read_text(encoding="utf-8")
|
|
declared_name = pyproject["project"]["name"]
|
|
assert f'version("{declared_name}")' in source, (
|
|
f"informix_db/__init__.py should call version({declared_name!r}) to "
|
|
"match [project].name in pyproject.toml"
|
|
)
|
|
|
|
|
|
def test_version_is_importable_and_stringy() -> None:
|
|
assert isinstance(informix_db.__version__, str)
|
|
assert informix_db.__version__
|
|
|
|
|
|
def test_public_surface_is_exported() -> None:
|
|
for name in informix_db.__all__:
|
|
assert hasattr(informix_db, name), f"{name} in __all__ but not defined"
|