diff --git a/src/ops/query.c b/src/ops/query.c index cab91802..b9ef5ad7 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 @@ -15436,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); @@ -15546,6 +15573,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..8ec45a0d --- /dev/null +++ b/test/rfl/join/str_key_nyi.rfl @@ -0,0 +1,33 @@ +;; 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; 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 +(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 +(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] +(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]