Implements row-decoding for IDS INTERVAL, the last common temporal type. The qualifier short bisects the type at the wire level: start_TU >= DAY maps to datetime.timedelta (day-fraction), start_TU <= MONTH maps to a new informix_db.IntervalYM (year-month). Wire format mirrors DATETIME exactly — `[head byte][digit pairs in base-100]`, with the qualifier dictating field interpretation. The fraction-to-nanoseconds scaling (`scale_exp = 18 - end_TU`, forced odd) is the JDBC pattern from `Decimal.fromIfxToArray`. IntervalYM is a frozen dataclass holding signed total months, with `years` and `remainder_months` as derived properties. Matches JDBC's `IntervalYM.months` shape rather than a (years, months) tuple — avoids ambiguity around what "negative" means for a multi-field tuple. Tests: 13 unit (synthetic byte streams covering all decoder branches) + 9 integration (real Informix queries spanning DAY TO SECOND, HOUR TO SECOND, YEAR TO MONTH, negatives, NULL, and mixed-family rows). Total test count: 53 unit + 82 integration = 135. Encoder for INTERVAL parameter binding is deferred to a later phase (same arc as DECIMAL/DATETIME — decode lands first).
162 lines
5.4 KiB
Python
162 lines
5.4 KiB
Python
"""Phase 6.d integration tests — INTERVAL decoding for both qualifier families.
|
|
|
|
INTERVAL splits into two families that decode to different Python types:
|
|
- DAY-FRACTION (start TU >= DAY=4) → ``datetime.timedelta``
|
|
- YEAR-MONTH (start TU <= MONTH=2) → :class:`informix_db.IntervalYM`
|
|
|
|
The wire format is identical to DECIMAL/DATETIME (BCD with sign+exp head
|
|
byte); the qualifier short tells us how to interpret the digits as
|
|
time fields.
|
|
"""
|
|
|
|
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_interval_day_to_second(conn_params: ConnParams) -> None:
|
|
"""INTERVAL DAY TO SECOND: 3 days, 4 hours, 5 minutes, 6 seconds."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT INTERVAL(3 04:05:06) DAY TO SECOND "
|
|
"FROM systables WHERE tabid = 1"
|
|
)
|
|
(val,) = cur.fetchone()
|
|
assert val == datetime.timedelta(
|
|
days=3, hours=4, minutes=5, seconds=6
|
|
)
|
|
|
|
|
|
def test_interval_hour_to_second(conn_params: ConnParams) -> None:
|
|
"""INTERVAL HOUR TO SECOND: 12:34:56."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT INTERVAL(12:34:56) HOUR TO SECOND "
|
|
"FROM systables WHERE tabid = 1"
|
|
)
|
|
(val,) = cur.fetchone()
|
|
assert val == datetime.timedelta(hours=12, minutes=34, seconds=56)
|
|
|
|
|
|
def test_interval_minute_to_second(conn_params: ConnParams) -> None:
|
|
"""INTERVAL MINUTE TO SECOND: 23:45."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT INTERVAL(23:45) MINUTE TO SECOND "
|
|
"FROM systables WHERE tabid = 1"
|
|
)
|
|
(val,) = cur.fetchone()
|
|
assert val == datetime.timedelta(minutes=23, seconds=45)
|
|
|
|
|
|
def test_interval_year_to_month(conn_params: ConnParams) -> None:
|
|
"""INTERVAL YEAR TO MONTH: 5 years, 3 months."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT INTERVAL(5-3) YEAR TO MONTH "
|
|
"FROM systables WHERE tabid = 1"
|
|
)
|
|
(val,) = cur.fetchone()
|
|
assert val == IntervalYM(months=5 * 12 + 3)
|
|
|
|
|
|
def test_interval_year_only(conn_params: ConnParams) -> None:
|
|
"""INTERVAL YEAR — exercises start==end, year-only qualifier."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT INTERVAL(2026) YEAR(4) TO YEAR "
|
|
"FROM systables WHERE tabid = 1"
|
|
)
|
|
(val,) = cur.fetchone()
|
|
assert val == IntervalYM(months=2026 * 12)
|
|
|
|
|
|
def test_interval_negative(conn_params: ConnParams) -> None:
|
|
"""Negative INTERVAL — exercises 9's-complement decode for intervals."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT INTERVAL(-3 04:05:06) DAY TO SECOND "
|
|
"FROM systables WHERE tabid = 1"
|
|
)
|
|
(val,) = cur.fetchone()
|
|
# Note: Informix encodes the whole interval as negative — all
|
|
# fields share the sign.
|
|
assert val == datetime.timedelta(
|
|
days=-3, hours=-4, minutes=-5, seconds=-6
|
|
)
|
|
|
|
|
|
def test_interval_in_table_column(conn_params: ConnParams) -> None:
|
|
"""INTERVAL stored in a table column round-trips correctly."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"CREATE TEMP TABLE t_iv "
|
|
"(id INTEGER, "
|
|
" span INTERVAL DAY(2) TO SECOND, "
|
|
" age INTERVAL YEAR(4) TO MONTH)"
|
|
)
|
|
cur.execute(
|
|
"INSERT INTO t_iv VALUES "
|
|
"(1, INTERVAL(7 12:00:00) DAY TO SECOND, "
|
|
"INTERVAL(2-6) YEAR TO MONTH)"
|
|
)
|
|
cur.execute("SELECT span, age FROM t_iv")
|
|
span, age = cur.fetchone()
|
|
assert span == datetime.timedelta(days=7, hours=12)
|
|
assert age == IntervalYM(months=2 * 12 + 6)
|
|
|
|
|
|
def test_interval_null(conn_params: ConnParams) -> None:
|
|
"""NULL INTERVAL decodes to Python None."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute("CREATE TEMP TABLE t_iv2 (span INTERVAL DAY TO SECOND)")
|
|
cur.execute("INSERT INTO t_iv2 VALUES (NULL)")
|
|
cur.execute("SELECT span FROM t_iv2")
|
|
assert cur.fetchone() == (None,)
|
|
|
|
|
|
def test_interval_multiple_columns(conn_params: ConnParams) -> None:
|
|
"""Multiple INTERVAL qualifiers in one row — proves per-column slicing."""
|
|
with _connect(conn_params) as conn:
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"SELECT "
|
|
" INTERVAL(3 04:05:06) DAY TO SECOND, "
|
|
" INTERVAL(1-6) YEAR TO MONTH, "
|
|
" INTERVAL(99:30) HOUR(2) TO MINUTE "
|
|
"FROM systables WHERE tabid = 1"
|
|
)
|
|
df, ym, hm = cur.fetchone()
|
|
assert df == datetime.timedelta(days=3, hours=4, minutes=5, seconds=6)
|
|
assert ym == IntervalYM(months=18)
|
|
assert hm == datetime.timedelta(hours=99, minutes=30)
|