SELECT on BLOB or CLOB columns no longer requires raw byte interpretation. The 72-byte server-side locator is wrapped in a typed BlobLocator or ClobLocator (frozen dataclass) so the column is recognizable as "server-side reference, not actual bytes". Wire-protocol findings: * Smart-LOB columns DON'T appear with their nominal type codes (102/101) in SQ_DESCRIBE. They surface as UDTFIXED (41) with extended_id 10 (BLOB) or 11 (CLOB) and encoded_length=72 (locator size). * Retrieving the actual bytes requires SQ_FPROUTINE (103) RPC to invoke ifx_lo_open, plus SQ_LODATA (97) for chunked transfer, plus another SQ_FPROUTINE for ifx_lo_close. That's a Phase 10 lift — roughly 2x the protocol surface of Phase 8. Server config needed (added to Phase 7 setup): * sbspace: onspaces -c -S sbspace1 ... * default sbspace: onmode -wm SBSPACENAME=sbspace1 What ships in Phase 9: * informix_db.BlobLocator(raw: bytes) — 72-byte frozen wrapper * informix_db.ClobLocator(raw: bytes) — distinct type, same shape * Row decoder branch in _resultset.parse_tuple_payload * Wire constants SQ_LODATA=97, SQ_FPROUTINE=103, SQ_FPARAM=104 Tests: * 11 unit tests in test_blob_locator_unit.py (no Informix needed) — construction, immutability, equality, hash, repr safety, size validation. * 4 integration tests in test_smart_lob.py — fixture seeds via JDBC reference client (smart-LOB writes also need deferred protocols). * RefBlob.java helper in tests/reference/ for seeding via JDBC. Total: 64 unit + 111 integration = 175 tests. Locator design note: __repr__ omits the raw bytes (they're opaque to the client). Same-bytes locators of different families compare unequal — BlobLocator(x) != ClobLocator(x) — to avoid silent type confusion.
72 lines
2.1 KiB
Python
72 lines
2.1 KiB
Python
"""Phase 9 unit tests — BlobLocator/ClobLocator value semantics.
|
|
|
|
These don't require Informix; they exercise the typed locator wrappers
|
|
directly to verify shape, immutability, equality, and repr safety.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
|
|
import pytest
|
|
|
|
from informix_db import BlobLocator, ClobLocator
|
|
|
|
|
|
def test_blob_locator_holds_72_bytes() -> None:
|
|
raw = bytes(range(72))
|
|
loc = BlobLocator(raw=raw)
|
|
assert loc.raw == raw
|
|
assert len(loc.raw) == 72
|
|
|
|
|
|
def test_clob_locator_holds_72_bytes() -> None:
|
|
raw = bytes(reversed(range(72)))
|
|
loc = ClobLocator(raw=raw)
|
|
assert loc.raw == raw
|
|
|
|
|
|
@pytest.mark.parametrize("size", [0, 1, 71, 73, 144])
|
|
def test_locator_rejects_wrong_size(size: int) -> None:
|
|
"""The constructor enforces exactly 72 bytes."""
|
|
with pytest.raises(ValueError, match="72 bytes"):
|
|
BlobLocator(raw=bytes(size))
|
|
with pytest.raises(ValueError, match="72 bytes"):
|
|
ClobLocator(raw=bytes(size))
|
|
|
|
|
|
def test_locator_is_frozen() -> None:
|
|
"""Instances are immutable per ``frozen=True`` dataclass decorator."""
|
|
loc = BlobLocator(raw=bytes(72))
|
|
with pytest.raises(dataclasses.FrozenInstanceError):
|
|
loc.raw = b"x" * 72 # type: ignore[misc]
|
|
|
|
|
|
def test_blob_and_clob_locator_are_distinct_types() -> None:
|
|
"""Same-bytes locators of different families compare unequal."""
|
|
raw = bytes(72)
|
|
blob = BlobLocator(raw=raw)
|
|
clob = ClobLocator(raw=raw)
|
|
assert blob != clob
|
|
assert not isinstance(blob, ClobLocator)
|
|
assert not isinstance(clob, BlobLocator)
|
|
|
|
|
|
def test_locator_equality() -> None:
|
|
"""Same bytes + same family → equal."""
|
|
raw = b"\x01\x02\x03" + bytes(69)
|
|
a = BlobLocator(raw=raw)
|
|
b = BlobLocator(raw=raw)
|
|
assert a == b
|
|
assert hash(a) == hash(b)
|
|
|
|
|
|
def test_locator_repr_omits_raw_bytes() -> None:
|
|
"""``repr`` doesn't leak the opaque locator bytes (no use to user)."""
|
|
raw = b"\xde\xad\xbe\xef" + bytes(68)
|
|
loc = BlobLocator(raw=raw)
|
|
r = repr(loc)
|
|
assert "BlobLocator" in r
|
|
assert "deadbeef" not in r.lower()
|
|
assert raw.hex() not in r
|