"""Regression tests for DATETIME sub-second precision on bind. ``_encode_datetime`` used to emit YEAR TO SECOND unconditionally, so binding a ``datetime`` carrying microseconds into a ``DATETIME YEAR TO FRACTION(n)`` column stored zeros — silently, with no error and no warning. Reads were always fine, which is what made it hard to notice: the value only went missing on the way in. It now emits YEAR TO FRACTION(5) when ``microsecond`` is non-zero and keeps the original YEAR TO SECOND encoding otherwise. FRACTION(5) is Informix's widest and resolves to 10 µs, so Python's sixth microsecond digit is dropped. That's a real limit of the type, not a driver choice, and the tests below pin the truncation so it can't drift into something worse. """ from __future__ import annotations import datetime import pytest import informix_db from informix_db.converters import _encode_datetime from tests.conftest import ConnParams 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, ) # -------------------------------------------------------------------------- # Encoder unit tests — no server needed # -------------------------------------------------------------------------- def test_encoder_uses_year_to_second_without_microseconds() -> None: """The long-exercised path must stay byte-identical.""" type_code, prec, raw = _encode_datetime( datetime.datetime(2026, 8, 31, 12, 30, 15) ) assert type_code == 10 assert prec == (14 << 8) | 10 # digit_count 14, YEAR..SECOND assert raw == b"\x00\x08\xc7\x14\x1a\x08\x1f\x0c\x1e\x0f" assert len(raw) == 10 # 2 len + 1 exp + 7 BCD pairs def test_encoder_widens_to_fraction_when_microseconds_present() -> None: type_code, prec, raw = _encode_datetime( datetime.datetime(2026, 8, 31, 12, 30, 15, 120000) ) assert type_code == 10 assert prec == (19 << 8) | 15 # digit_count 19, YEAR..FRACTION(5) assert len(raw) == 13 # 2 len + 1 exp + 10 BCD pairs # Exponent byte is unchanged: the integer part is still 7 base-100 # pairs, the fraction just adds three more after the point. assert raw[2] == 0xC7 # Trailing pairs carry 120000 as BCD 12/00/00. assert raw[-3:] == b"\x0c\x00\x00" def test_encoder_pads_fraction_to_six_digits() -> None: """One microsecond must not shift the BCD pairs.""" _, prec, raw = _encode_datetime( datetime.datetime(2026, 8, 31, 12, 30, 15, 1) ) assert prec == (19 << 8) | 15 assert len(raw) == 13 assert raw[-3:] == b"\x00\x00\x01" # 000001 # -------------------------------------------------------------------------- # Round-trip against a real server # -------------------------------------------------------------------------- @pytest.mark.integration @pytest.mark.parametrize( ("microsecond", "expected"), [ (0, 0), (120000, 120000), (500000, 500000), (1, 0), # below FRACTION(5) resolution (999999, 999990), # truncated to 5 significant digits ], ) def test_fraction_round_trip( conn_params: ConnParams, microsecond: int, expected: int ) -> None: value = datetime.datetime(2026, 8, 31, 12, 30, 15, microsecond) with _connect(conn_params) as conn: cur = conn.cursor() cur.execute( "CREATE TEMP TABLE t_dt_frac " "(k INT, t DATETIME YEAR TO FRACTION(5))" ) cur.execute("INSERT INTO t_dt_frac VALUES (?, ?)", (1, value)) cur.execute("SELECT t FROM t_dt_frac") (got,) = cur.fetchone() assert got == value.replace(microsecond=expected) @pytest.mark.integration def test_fraction_bind_into_year_to_second_column( conn_params: ConnParams, ) -> None: """Widening the bind must not break narrower columns — Informix converts between qualifiers on assignment, truncating server-side.""" with _connect(conn_params) as conn: cur = conn.cursor() cur.execute( "CREATE TEMP TABLE t_dt_sec (k INT, t DATETIME YEAR TO SECOND)" ) cur.execute( "INSERT INTO t_dt_sec VALUES (?, ?)", (1, datetime.datetime(2026, 8, 31, 12, 30, 15, 987654)), ) cur.execute("SELECT t FROM t_dt_sec") assert cur.fetchone() == ( datetime.datetime(2026, 8, 31, 12, 30, 15), ) @pytest.mark.integration def test_fraction_survives_alongside_lvarchar( conn_params: ConnParams, ) -> None: """The reported schema pairs FRACTION(5) columns with LVARCHARs.""" ts = datetime.datetime(2026, 8, 31, 12, 30, 15, 120000) with _connect(conn_params) as conn: cur = conn.cursor() cur.execute( "CREATE TEMP TABLE t_dt_lv " "(s LVARCHAR(512), t DATETIME YEAR TO FRACTION(5), n INT8 NOT NULL)" ) cur.execute( "INSERT INTO t_dt_lv VALUES (?, ?, ?)", ("PackageRoot", ts, 10) ) cur.execute("SELECT s, t, n FROM t_dt_lv") assert cur.fetchone() == ("PackageRoot", ts, 10)