Skip to content

fix(q): convert minute/second/month units on decode; null out-of-range datetime - #7

Open
belowzeroff wants to merge 1 commit into
RayforceDB:masterfrom
belowzeroff:fix/q-temporal-units
Open

belowzeroff wants to merge 1 commit into
RayforceDB:masterfrom
belowzeroff:fix/q-temporal-units

Conversation

@belowzeroff

Copy link
Copy Markdown
Contributor

How it looks from the user's side

Pull anything with a q minute, second or month column through .q.send — a select by 5 xbar time.minute bucket, a second-typed timestamp, a monthly key — and the values come back as the right type with the wrong value, silently:

q sends got should be
09:30 (minute) 00:00:00.570 09:30:00.000
09:30:15 (second) 00:00:34.215 09:30:15.000
2024.02m (month) 2000.10.16 2024.02.01

Nothing errors; every result carries the correct TIME/DATE type code, so joins, bucketing and filters downstream just work on garbage.

A datetime (KZ) past the TIMESTAMP nanosecond range, or the q infinities 0Wz/-0Wz, had the mirror problem at the edges: an overflowed but plausible-looking timestamp instead of a null.

Root cause

The decoder copied every 4-byte q temporal raw and re-tagged it as the rayforce type of the same width. That is right for date (KD → DATE, days) and time (KT → TIME, ms), and wrong for the other three sharing the width: minute (KU) counts minutes and second (KV) counts seconds since midnight, but landed in TIME's i32 milliseconds unscaled; month (KM) counts months since 2000.01 but landed in DATE's i32 days.

q_kz_days_to_nanos fed any double through llround and an i64 multiply with no range check.

Fix

  • KU → TIME scaled ×60000, KV → TIME scaled ×1000, atoms and vectors.
  • KM → DATE of the month's first day, what q's `date$ does with a month (2024.02m2024.02.01).
  • A value the target cannot hold — the q infinities 0W/-0W, a minute/second count past the i32 ms range, a datetime past the TIMESTAMP range — decodes to the typed null rather than wrapping; the null sentinels (0Nu/0Nv/0Nm/0Nz) map to the null as before, and the converted vectors carry the HAS_NULLS gate so aggregates skip them.

timespan (KN) is deliberately unchanged: rayforce has no duration type, and its current mapping to a TIMESTAMP anchored at 2000.01.01 is what kdb-tick style time columns rely on today. Worth a separate decision.

Tests

  • test/rfl/client/05_temporal.rfl (real q, both the blocking and --poll runs): minute/second/month atoms and vectors, negative minutes, nulls inside vectors, infinities → null, `date$2024.02m agreement, and min/max over vectors with nulls (the null gate).
  • test/driver.c codec selftest: the raw wire bytes for a minute atom, a second atom, 0Wu, a month atom, a negative month (1999.12m) and an out-of-range datetime — so the conversion is verified in CI, where no q is available. All six fail on the previous decoder.

make test against current dev core, with a local q 4.x: codec + exchange selftests ok, server 33/33, real-q interop 30/30, client 91 assertions × 2 runs + push 27, 0 failures.

…e datetime

The decoder copied every 4-byte q temporal raw and re-tagged it as the
rayforce type of the same width, which is only correct for date (KD) and
time (KT). minute (KU) counts minutes and second (KV) counts seconds since
midnight, but both landed in TIME (i32 milliseconds) unscaled — 09:30 came
back as 00:00:00.570 and 09:30:15 as 00:00:34.215. month (KM) counts months
since 2000.01 but landed in DATE (i32 days) — 2024.02m came back as
2000.10.16. Every result carried the correct type code, so nothing
downstream could tell the values were wrong.

Decode KU and KV by scaling to ms, and KM to the DATE of the month's first
day (what q's `date$ does with a month). Values the target type cannot hold
— the q infinities 0W/-0W, a minute/second count past the i32 ms range —
decode to the typed null, and the null sentinels map to the null as before.

The datetime (KZ) conversion had the mirror problem at the edges: it fed
any double through llround and an i64 multiply, so a datetime past the
TIMESTAMP nanosecond range (or 0Wz) overflowed into a plausible-looking
timestamp. Range-check the day count and decode those to the null
timestamp.

timespan (KN) is unchanged and still decodes to a TIMESTAMP anchored at
2000.01.01: rayforce has no duration type, and that mapping is what
kdb-tick style `time` columns rely on today.

Tests: 05_temporal.rfl covers atoms, vectors, nulls, infinities and the
null gate on aggregates against a real q; the codec selftest checks the
raw wire bytes so the conversion is also verified where no q is available.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant