From da97945a4fdffe3b554a8d24b566cbadd781153b Mon Sep 17 00:00:00 2001 From: Evgen Byelozorov Date: Fri, 14 Aug 2026 07:47:53 +0200 Subject: [PATCH 1/2] fix(query): reject STR equality keys in asof-join and window-join MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both kernels read equality-key cells through read_col_i64, which has no RAY_STR case: a STR column falls into the byte-wide default. asof-join (via asof_eq_lread) mismatches outright — a key that should match nulls out; window-join / window-join1 read one byte per row when they sort and probe the right side, so once there are more distinct keys than fit in a byte the collisions cross-contaminate groups and the aggregates are wrong. Neither surfaces an error, so callers get silently corrupt data. The base inner/left/anti joins use a separate STR-aware kernel and stay correct, so scope the fix to the two window/asof kernels: decline a STR equality key on either side with nyi (parted columns unwrapped to their base type first). One guard in ray_asof_join_core covers both asof call paths; one in window_join_impl covers window-join and window-join1. Adds test/rfl/join/str_key_nyi.rfl covering the nyi rejection, the SYM keys that must keep working (order-independent), and the base joins that must NOT be over-rejected on STR keys. --- src/ops/query.c | 32 ++++++++++++++++++++++++++++++++ test/rfl/join/str_key_nyi.rfl | 31 +++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 test/rfl/join/str_key_nyi.rfl diff --git a/src/ops/query.c b/src/ops/query.c index cab91802..98aadc7d 100644 --- a/src/ops/query.c +++ b/src/ops/query.c @@ -14876,6 +14876,21 @@ static ray_t* window_join_impl(ray_t** args, int64_t n, int mode) { for (int i = 0; i < 4; i++) ray_release(eargs[i]); return ray_error("domain", "window-join: equality key column not found in both tables"); } + /* STR equality-key columns are silently mismatched: window-join + * sorts and probes eq cells through read_col_i64, which has no + * RAY_STR case (it reads one raw byte per row), so once there are + * enough distinct keys the byte collisions cross-contaminate + * groups and the aggregates are WRONG. Decline both sides — the + * base equi-join has a STR-aware kernel, these window kernels do + * not. */ + int8_t lct = left_eq[e]->type, rct = right_eq[e]->type; + if (RAY_IS_PARTED(lct)) lct = (int8_t)RAY_PARTED_BASETYPE(lct); + if (RAY_IS_PARTED(rct)) rct = (int8_t)RAY_PARTED_BASETYPE(rct); + if (lct == RAY_STR || rct == RAY_STR) { + scratch_free(eq_hdr); + for (int i = 0; i < 4; i++) ray_release(eargs[i]); + return ray_error("nyi", "window-join: string equality key columns are not supported"); + } } /* Parse every (name, (op src)) pair from the agg dict. The dict's @@ -15546,6 +15561,23 @@ static ray_t* ray_asof_join_core(ray_t* keys_vec, ray_t* left_tbl, ray_t* right_ if (!nm) { scratch_free(eqops_hdr); ray_graph_free(g); if (_bxk) ray_release(_bxk); return ray_error("domain", "asof-join: unknown equality key symbol"); } eq_ops[i] = ray_scan(g, ray_str_ptr(nm)); if (!eq_ops[i]) { scratch_free(eqops_hdr); ray_graph_free(g); if (_bxk) ray_release(_bxk); return ray_error("domain", "asof-join: equality key column not found"); } + /* STR equality-key columns are silently mismatched: the asof kernel + * reads eq cells through read_col_i64 (asof_eq_lread), which has no + * RAY_STR case, so string keys compare as raw bytes and yield WRONG + * results (a key that should match can null out). The base equi-join + * has a separate STR-aware kernel; asof does not, so decline both + * sides rather than return corrupt data. */ + ray_t* lcol = ray_table_get_col(left_tbl, eq_syms[i]->i64); + ray_t* rcol = ray_table_get_col(right_tbl, eq_syms[i]->i64); + int8_t lct = lcol ? lcol->type : (int8_t)0; + int8_t rct = rcol ? rcol->type : (int8_t)0; + if (RAY_IS_PARTED(lct)) lct = (int8_t)RAY_PARTED_BASETYPE(lct); + if (RAY_IS_PARTED(rct)) rct = (int8_t)RAY_PARTED_BASETYPE(rct); + if (lct == RAY_STR || rct == RAY_STR) { + scratch_free(eqops_hdr); + ray_graph_free(g); if (_bxk) ray_release(_bxk); + return ray_error("nyi", "asof-join: string equality key columns are not supported"); + } } if (_bxk) ray_release(_bxk); diff --git a/test/rfl/join/str_key_nyi.rfl b/test/rfl/join/str_key_nyi.rfl new file mode 100644 index 00000000..3aeaf484 --- /dev/null +++ b/test/rfl/join/str_key_nyi.rfl @@ -0,0 +1,31 @@ +;; str_key_nyi.rfl — STR equality-key columns are unsupported in the asof and +;; window join kernels and must be DECLINED, not silently mis-answered. +;; +;; Root cause: both kernels read equality-key cells through read_col_i64 +;; (asof via asof_eq_lread; window when it sorts/probes the right side), and +;; read_col_i64 has no RAY_STR case — a STR column falls into the byte-wide +;; default. asof then mismatches outright (a key that should match nulls +;; out); window only reads one byte per row, so small key sets coincide by +;; luck but enough distinct keys collide and cross-contaminate groups, so the +;; aggregates are WRONG. The base equi-join (inner/left/anti) uses a separate +;; STR-aware kernel and stays correct, so the fix is scoped to asof + window: +;; reject a STR eq-key on either side with `nyi` instead of returning corrupt +;; data. window-join1 shares the window kernel; both asof call paths share +;; ray_asof_join_core, so one guard each covers every entry point. + +;; ── asof-join: STR equality key → nyi (both operand orders) ────────── +(set tr (table [Sym Time Price] (list ["A" "A"] [12:00:01 12:00:04] [100 101])))(set qs (table [Sym Time Bid] (list ["A" "A"] [12:00:00 12:00:05] [99 100])))(asof-join [Sym Time] tr qs) !- nyi +(set tr (table [Sym Time Price] (list ["A" "A"] [12:00:01 12:00:04] [100 101])))(set qs (table [Sym Time Bid] (list ["A" "A"] [12:00:00 12:00:05] [99 100])))(asof-join [Sym Time] qs tr) !- nyi + +;; ── window-join / window-join1: STR equality key → nyi ─────────────── +(set wt (table [Sym Time Price] (list ["a" "a"] [10:00:01.000 10:00:05.000] [100 200])))(set wq (table [Sym Time Bid] (list ["a" "a" "a"] [10:00:00.000 10:00:02.000 10:00:04.000] [99 100 101])))(set wi (map-left + [-2000 2000] (at wt 'Time)))(window-join [Sym Time] wi wt wq {minBid: (min Bid)}) !- nyi +(set wt (table [Sym Time Price] (list ["a" "a"] [10:00:01.000 10:00:05.000] [100 200])))(set wq (table [Sym Time Bid] (list ["a" "a" "a"] [10:00:00.000 10:00:02.000 10:00:04.000] [99 100 101])))(set wi (map-left + [-2000 2000] (at wt 'Time)))(window-join1 [Sym Time] wi wt wq {minBid: (min Bid)}) !- nyi + +;; ── SYM equality keys keep working and stay order-independent ──────── +(set KS (as 'sym ["AAPL" "AAPL"]))(set trS (table [Sym Time Price] (list KS [12:00:01 12:00:04] [100 101])))(set qsS (table [Sym Time Bid] (list KS [12:00:00 12:00:05] [99 100])))(at (asof-join [Sym Time] trS qsS) 'Bid) -- [99 99] +(set KS (as 'sym ["AAPL" "AAPL"]))(set trS (table [Sym Time Price] (list KS [12:00:01 12:00:04] [100 101])))(set qsU (table [Sym Time Bid] (list KS [12:00:05 12:00:00] [100 99])))(at (asof-join [Sym Time] trS qsU) 'Bid) -- [99 99] +(set KW (as 'sym ["a" "a"]))(set wtS (table [Sym Time Price] (list KW [10:00:01.000 10:00:05.000] [100 200])))(set wqS (table [Sym Time Bid] (list (as 'sym ["a" "a" "a"]) [10:00:00.000 10:00:02.000 10:00:04.000] [99 100 101])))(set wiS (map-left + [-2000 2000] (at wtS 'Time)))(at (window-join [Sym Time] wiS wtS wqS {minBid: (min Bid)}) 'minBid) -- [99 100] + +;; ── base equi-join must NOT over-reject: STR keys stay correct ─────── +(set A (table [K V] (list ["x" "y"] [1 2])))(set B (table [K W] (list ["x" "z"] [10 20])))(at (inner-join [K] A B) 'V) -- [1] +(set A (table [K V] (list ["x" "y"] [1 2])))(set B (table [K W] (list ["x" "z"] [10 20])))(at (anti-join [K] A B) 'V) -- [2] From 20b46e30dbfc439b4df8ba939529305e0bfb7538 Mon Sep 17 00:00:00 2001 From: Evgen Byelozorov Date: Fri, 14 Aug 2026 11:32:24 +0200 Subject: [PATCH 2/2] fix(query): reject STR keys in legacy window joins --- src/ops/query.c | 12 ++++++++++++ test/rfl/join/str_key_nyi.rfl | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/ops/query.c b/src/ops/query.c index 98aadc7d..b9ef5ad7 100644 --- a/src/ops/query.c +++ b/src/ops/query.c @@ -15451,6 +15451,18 @@ static ray_t* window_join_impl(ray_t** args, int64_t n, int mode) { if (!nm) { scratch_free(eqops_hdr); ray_graph_free(g); if (_bxeq) ray_release(_bxeq); return ray_error("domain", "window-join: unknown equality key symbol"); } eq_ops[i] = ray_scan(g, ray_str_ptr(nm)); if (!eq_ops[i]) { scratch_free(eqops_hdr); ray_graph_free(g); if (_bxeq) ray_release(_bxeq); return ray_error("domain", "window-join: equality key column not found"); } + ray_t* lcol = ray_table_get_col(left_tbl, eq_elems[i]->i64); + ray_t* rcol = ray_table_get_col(right_tbl, eq_elems[i]->i64); + int8_t lct = lcol ? lcol->type : (int8_t)0; + int8_t rct = rcol ? rcol->type : (int8_t)0; + if (RAY_IS_PARTED(lct)) lct = (int8_t)RAY_PARTED_BASETYPE(lct); + if (RAY_IS_PARTED(rct)) rct = (int8_t)RAY_PARTED_BASETYPE(rct); + if (lct == RAY_STR || rct == RAY_STR) { + scratch_free(eqops_hdr); + ray_graph_free(g); + if (_bxeq) ray_release(_bxeq); + return ray_error("nyi", "window-join: string equality key columns are not supported"); + } } if (_bxeq) ray_release(_bxeq); diff --git a/test/rfl/join/str_key_nyi.rfl b/test/rfl/join/str_key_nyi.rfl index 3aeaf484..8ec45a0d 100644 --- a/test/rfl/join/str_key_nyi.rfl +++ b/test/rfl/join/str_key_nyi.rfl @@ -10,8 +10,8 @@ ;; aggregates are WRONG. The base equi-join (inner/left/anti) uses a separate ;; STR-aware kernel and stays correct, so the fix is scoped to asof + window: ;; reject a STR eq-key on either side with `nyi` instead of returning corrupt -;; data. window-join1 shares the window kernel; both asof call paths share -;; ray_asof_join_core, so one guard each covers every entry point. +;; data. window-join1 shares the window kernel; the 4-argument compatibility +;; window forms fall through to the asof executor, so they need the same guard. ;; ── asof-join: STR equality key → nyi (both operand orders) ────────── (set tr (table [Sym Time Price] (list ["A" "A"] [12:00:01 12:00:04] [100 101])))(set qs (table [Sym Time Bid] (list ["A" "A"] [12:00:00 12:00:05] [99 100])))(asof-join [Sym Time] tr qs) !- nyi @@ -20,6 +20,8 @@ ;; ── window-join / window-join1: STR equality key → nyi ─────────────── (set wt (table [Sym Time Price] (list ["a" "a"] [10:00:01.000 10:00:05.000] [100 200])))(set wq (table [Sym Time Bid] (list ["a" "a" "a"] [10:00:00.000 10:00:02.000 10:00:04.000] [99 100 101])))(set wi (map-left + [-2000 2000] (at wt 'Time)))(window-join [Sym Time] wi wt wq {minBid: (min Bid)}) !- nyi (set wt (table [Sym Time Price] (list ["a" "a"] [10:00:01.000 10:00:05.000] [100 200])))(set wq (table [Sym Time Bid] (list ["a" "a" "a"] [10:00:00.000 10:00:02.000 10:00:04.000] [99 100 101])))(set wi (map-left + [-2000 2000] (at wt 'Time)))(window-join1 [Sym Time] wi wt wq {minBid: (min Bid)}) !- nyi +(set wt (table [Sym Time Price] (list ["a" "a"] [10:00:01.000 10:00:05.000] [100 200])))(set wq (table [Sym Time Bid] (list ["a" "a" "a"] [10:00:00.000 10:00:02.000 10:00:04.000] [99 100 101])))(at (window-join wt wq [Sym] 'Time) 'Bid) !- nyi +(set wt (table [Sym Time Price] (list ["a" "a"] [10:00:01.000 10:00:05.000] [100 200])))(set wq (table [Sym Time Bid] (list ["a" "a" "a"] [10:00:00.000 10:00:02.000 10:00:04.000] [99 100 101])))(at (window-join1 wt wq [Sym] 'Time) 'Bid) !- nyi ;; ── SYM equality keys keep working and stay order-independent ──────── (set KS (as 'sym ["AAPL" "AAPL"]))(set trS (table [Sym Time Price] (list KS [12:00:01 12:00:04] [100 101])))(set qsS (table [Sym Time Bid] (list KS [12:00:00 12:00:05] [99 100])))(at (asof-join [Sym Time] trS qsS) 'Bid) -- [99 99]