Implements encoders for datetime.timedelta → INTERVAL DAY(9) TO FRACTION(5) and IntervalYM → INTERVAL YEAR(9) TO MONTH. Both follow the 2-byte-length- prefixed BCD wire format established in Phase 6.c (DECIMAL/DATETIME). The default qualifier choice is generous: DAY(9) covers any timedelta, YEAR(9) handles ±1B years. JDBC defaults to smaller widths (DAY(2)/YEAR(4)) trading safety for compactness — we make the opposite trade. FRACTION(5) is the Informix precision ceiling — sub-10us intervals can't round-trip cleanly. Same limitation JDBC has. Six integration tests, all green on first run against live Informix — the synthetic round-trip in the test framework caught every framing bug locally, before integration tests even started. This is the dividend from owning both decoder and encoder. Total: 53 unit + 88 integration = 141 tests. Type matrix update: INTERVAL now has both decode + encode. Only BLOB/CLOB and BYTE/TEXT remain among the common types.
176 lines
6.0 KiB
Python
176 lines
6.0 KiB
Python
"""Phase 6.e integration tests — INTERVAL parameter encoding round-trip.
|
|
|
|
The encoder produces a 2-byte-length-prefixed BCD payload identical in
|
|
shape to DATETIME/DECIMAL (per the Phase 6.c discovery). These tests
|
|
verify the round-trip: Python value → SQ_BIND wire bytes → Informix
|
|
storage → SELECT → Python value.
|
|
|
|
If the server silently drops a parametrized INSERT with no error, the
|
|
encoder envelope is wrong (same diagnostic pattern as DECIMAL/DATETIME).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import datetime
|
|
|
|
import pytest
|
|
|
|
import informix_db
|
|
from informix_db import IntervalYM
|
|
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_timedelta_param_round_trip(conn_params: ConnParams) -> None:
|
|
"""``datetime.timedelta`` as a bind parameter round-trips."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"CREATE TEMP TABLE t_iv_e "
|
|
"(id INTEGER, span INTERVAL DAY(9) TO FRACTION(5))"
|
|
)
|
|
cur.executemany(
|
|
"INSERT INTO t_iv_e VALUES (?, ?)",
|
|
[
|
|
(1, datetime.timedelta(days=3, hours=4, minutes=5, seconds=6)),
|
|
(2, datetime.timedelta(days=0, hours=12)),
|
|
(3, datetime.timedelta(seconds=45)),
|
|
(4, datetime.timedelta(days=999_999_999)),
|
|
],
|
|
)
|
|
cur.execute("SELECT id, span FROM t_iv_e ORDER BY id")
|
|
rows = cur.fetchall()
|
|
assert rows == [
|
|
(1, datetime.timedelta(days=3, hours=4, minutes=5, seconds=6)),
|
|
(2, datetime.timedelta(days=0, hours=12)),
|
|
(3, datetime.timedelta(seconds=45)),
|
|
(4, datetime.timedelta(days=999_999_999)),
|
|
]
|
|
|
|
|
|
def test_negative_timedelta_round_trip(conn_params: ConnParams) -> None:
|
|
"""Negative ``timedelta`` exercises the asymmetric base-100 complement encode."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"CREATE TEMP TABLE t_iv_e2 "
|
|
"(id INTEGER, span INTERVAL DAY(9) TO FRACTION(5))"
|
|
)
|
|
cur.executemany(
|
|
"INSERT INTO t_iv_e2 VALUES (?, ?)",
|
|
[
|
|
(1, datetime.timedelta(days=-3, hours=-4, minutes=-5, seconds=-6)),
|
|
(2, datetime.timedelta(seconds=-45)),
|
|
],
|
|
)
|
|
cur.execute("SELECT id, span FROM t_iv_e2 ORDER BY id")
|
|
rows = cur.fetchall()
|
|
assert rows == [
|
|
(1, datetime.timedelta(days=-3, hours=-4, minutes=-5, seconds=-6)),
|
|
(2, datetime.timedelta(seconds=-45)),
|
|
]
|
|
|
|
|
|
def test_timedelta_with_microseconds(conn_params: ConnParams) -> None:
|
|
"""timedelta down to 10us precision — Informix FRACTION(5) limit."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"CREATE TEMP TABLE t_iv_e3 "
|
|
"(id INTEGER, span INTERVAL DAY(9) TO FRACTION(5))"
|
|
)
|
|
cur.execute(
|
|
"INSERT INTO t_iv_e3 VALUES (?, ?)",
|
|
(1, datetime.timedelta(seconds=10, microseconds=500_000)),
|
|
)
|
|
cur.execute("SELECT span FROM t_iv_e3")
|
|
(val,) = cur.fetchone()
|
|
assert val == datetime.timedelta(seconds=10, microseconds=500_000)
|
|
|
|
|
|
def test_intervalym_param_round_trip(conn_params: ConnParams) -> None:
|
|
"""``IntervalYM`` as a bind parameter round-trips."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"CREATE TEMP TABLE t_iv_ym "
|
|
"(id INTEGER, age INTERVAL YEAR(9) TO MONTH)"
|
|
)
|
|
cur.executemany(
|
|
"INSERT INTO t_iv_ym VALUES (?, ?)",
|
|
[
|
|
(1, IntervalYM(months=5 * 12 + 3)),
|
|
(2, IntervalYM(months=0)),
|
|
(3, IntervalYM(months=12)),
|
|
(4, IntervalYM(months=2026 * 12 + 7)),
|
|
],
|
|
)
|
|
cur.execute("SELECT id, age FROM t_iv_ym ORDER BY id")
|
|
rows = cur.fetchall()
|
|
assert rows == [
|
|
(1, IntervalYM(months=5 * 12 + 3)),
|
|
(2, IntervalYM(months=0)),
|
|
(3, IntervalYM(months=12)),
|
|
(4, IntervalYM(months=2026 * 12 + 7)),
|
|
]
|
|
|
|
|
|
def test_intervalym_negative_round_trip(conn_params: ConnParams) -> None:
|
|
"""Negative ``IntervalYM`` round-trips."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"CREATE TEMP TABLE t_iv_ym2 "
|
|
"(id INTEGER, age INTERVAL YEAR(9) TO MONTH)"
|
|
)
|
|
cur.executemany(
|
|
"INSERT INTO t_iv_ym2 VALUES (?, ?)",
|
|
[
|
|
(1, IntervalYM(months=-(5 * 12 + 3))),
|
|
(2, IntervalYM(months=-1)),
|
|
],
|
|
)
|
|
cur.execute("SELECT id, age FROM t_iv_ym2 ORDER BY id")
|
|
rows = cur.fetchall()
|
|
assert rows == [
|
|
(1, IntervalYM(months=-(5 * 12 + 3))),
|
|
(2, IntervalYM(months=-1)),
|
|
]
|
|
|
|
|
|
def test_interval_in_where_clause(conn_params: ConnParams) -> None:
|
|
"""timedelta as a parameter in a WHERE clause."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"CREATE TEMP TABLE t_iv_w "
|
|
"(id INTEGER, span INTERVAL DAY(9) TO SECOND)"
|
|
)
|
|
cur.executemany(
|
|
"INSERT INTO t_iv_w VALUES (?, ?)",
|
|
[
|
|
(1, datetime.timedelta(days=1)),
|
|
(2, datetime.timedelta(days=10)),
|
|
(3, datetime.timedelta(days=100)),
|
|
],
|
|
)
|
|
cur.execute(
|
|
"SELECT id FROM t_iv_w WHERE span > ? ORDER BY id",
|
|
(datetime.timedelta(days=5),),
|
|
)
|
|
assert cur.fetchall() == [(2,), (3,)]
|