Skip to content

fix: silent zero-match predicates — guid equality and like over list columns - #385

Open
protocolstardust wants to merge 2 commits into
RayforceDB:devfrom
protocolstardust:fix-guid-like-predicates
Open

fix: silent zero-match predicates — guid equality and like over list columns#385
protocolstardust wants to merge 2 commits into
RayforceDB:devfrom
protocolstardust:fix-guid-like-predicates

Conversation

@protocolstardust

Copy link
Copy Markdown
Collaborator

Two select-where predicate shapes returned zero rows without any error, discovered while benchmarking filters over a 13.9M-row splayed table.

GUID equality predicates matched nothing

(select {from: t where: (== guid_col g)}) returned an empty result at any table size:

  • the fused expression program has no 16-byte loads, so a compiled guid predicate read cells at the wrong width;
  • the elementwise fallback routed guid cells through the numeric loops with the same outcome.

Fix: the expression compiler bails GUID columns (EXPR_BAIL_GUID, mirroring the existing RAY_STR bail), and exec_elementwise_binary gets a memcmp branch for ==/!= over guid operands. Ordering operators over guids raise a type error instead of comparing garbage.

like over splayed string columns: type error direct, silent all-false in where

Splayed string columns load as a list of string atoms (col_load_str_list). ray_like_fn rejected that shape (str-find accepts it), and exec_like's fallback memset the result to all-false — so a like predicate over such a column, or over any unsupported column type, silently matched nothing.

Fix: ray_like_fn gets the list branch (mirroring str-find's), exec_like delegates the list case to it, and the silent memset is replaced with the type error the direct builtin raises.

Tests

  • test/rfl/query/guid_like_predicates.rfl — guid predicates find their rows, compose with aggregation, ordering raises; like over a list column in where; like over a numeric column raises.
  • test/rfl/strop/like.rfl — list-of-string/symbol inputs, mixed-type list raises.

Benchmark (13.9M-row splayed table, 10 cores)

predicate before after
where (== LCID g) 0 rows / workaround via mask 479 ms correct, 9.7 ms
where (like reason_text "*Pending*") 0 rows / workaround via str-find 121 ms correct, 62 ms

Both now within ~1.5x of kdb+ on the same data (6.5 / 50 ms).

The fused expression program has no 16-byte loads, and the elementwise
fallback routed GUID cells through the numeric loops — a guid equality
predicate silently returned zero rows at any table size. Bail GUID
columns out of the compiled program (EXPR_BAIL_GUID) and give the
fallback a memcmp branch for ==/!= over guid operands; ordering
operators raise a type error instead of comparing garbage.
@singaraiona

Copy link
Copy Markdown
Collaborator

Triage note on the red CI: the debug jobs fail on str/like_non_string — a pre-existing C test that pins the old silent-all-false behavior this PR deliberately replaces with a type error. Your own new tests pass; that one just needs its expectation updated to the new contract. The branch is also a couple of months behind dev now, so please rebase while you're at it. The direction (loud error instead of silent zero rows) is right.

Splayed string columns load as a list of string atoms
(col_load_str_list). ray_like_fn rejected that shape with a type error
while str-find accepted it, and exec_like's fallback memset the result
to all-false — a like predicate in select-where over such a column (or
any unsupported type) silently matched nothing. Add the list branch to
ray_like_fn (mirroring str-find), delegate exec_like's list case to it,
and replace the silent memset with the type error the direct builtin
raises.
@protocolstardust
protocolstardust force-pushed the fix-guid-like-predicates branch from a24744b to d8c7707 Compare August 22, 2026 08:22
@singaraiona

Copy link
Copy Markdown
Collaborator

Reviewed — including building this branch and reproducing the key findings empirically. The flat-column guid fix and the like list delegation both work when reached, but the headline fix doesn't cover partitioned tables, and there it now produces something worse than the original bug.

1. Blocker — parted guid columns bypass EXPR_BAIL_GUID, and col-vs-col == matches ALL rows. The new bail tests only col->type == RAY_GUID; a partitioned table's guid column carries a parted-tagged type (part.c:439) and sails past it. The fused program then gives the scan register type RAY_I64, and expr_load_i64's default: memset(dst, 0, n*8) zero-fills every lane on both sides. Reproduced on this branch: a single-partition table with two guid columns where zero rows match — where (== g h) returned all 3 rows (zeros compared to zeros). Guid-vs-constant on the same parted table fails differently: EXPR_BAIL_CONST routes it to the unfused executor, where the new branch's lhs->type == RAY_GUID check fails on the parted wrapper and it errors type: =: incomparable operand types, got ? and guid. So on partitioned tables the PR converts silent-zero-match into silent-all-match (col vs col) and a spurious error (col vs const). The bail needs the parted case (RAY_IS_PARTED && RAY_PARTED_BASETYPE == RAY_GUID), and the unfused guid branch needs to see flattened/parted input.

2. == over legacy STRL list columns still silently matches nothing. Reproduced: a splayed table whose s column is a legacy STRL file loads as 'LIST; where (== s "keep me") returns [] with no error, while the same predicate on a normalized STR column returns the row. This is the exact bug class the PR title names, on the exact column shape the PR's like fix targets — expr_compile has no bail for RAY_LIST and exec_elementwise_binary has no list branch. A shared "non-numeric column" gate before the numeric loops would make the next exotic shape error loudly instead of silently mismatching.

3. ilike kept the silent memset-zero branch (src/ops/string.c exec_ilike, ~80 lines below the edit). Reproduced on the same legacy list column: where (like s "*keep*")[1 3], where (ilike s "*KEEP*")[] with no error. Same column, same session, loud/correct vs silently empty.

4. Test coverage gap — the select-where list test never exercises the new branch. (table [s v] (list (list "keep me" ...) ...)) normalizes the column to RAY_STR at construction (verified: (type (get ts 's))'STR), so the test goes through the pre-existing STR path. The RAY_LIST delegation in exec_like is only reachable via a legacy STRL splayed load, which no test constructs — I verified by hand-building an STRL file that the branch does work, but the suite would stay green if it broke. A test that writes an actual STRL column (like col_format_generation.rfl does for the generation byte) would pin it.

5. Pre-existing but in the restructured dispatch: exec_like reads input->len and allocates the result before type dispatch, so a scalar string subject (where (like "keep me" "*keep*")) aliases the SSO bytes as a length and dies with oom: vec_new(cap=139381553795968) while the direct builtin returns true. Dispatching before the alloc — e.g. delegating non-vector and unsupported inputs to ray_like_fn, which also dedupes the near-identical type-error message — fixes it for free.

Efficiency (non-blocking): the RAY_LIST branch sits after the glob compile + result alloc it then throws away — hoisting it above both removes the waste; the delegate's list loop is serial and selection-blind while exec_like's STR path pool-dispatches. Likewise the new guid memcmp loop is serial while the adjacent STR branch parallelizes at RAY_PARALLEL_THRESHOLD.

For the record, things I checked that are fine: ray_data resolves slices correctly for 16-byte guid elements (slice_offset × ray_type_sizes[RAY_GUID]), the memcmp truth table matches fix_null_comparisons null-as-minimum semantics for EQ/NE (null guid is canonically 16 zero bytes), typed-null guid atoms always allocate obj so there's no NULL-deref path, the list-branch refcounting is exact, and rejecting ordered comparisons on guids in this path is commented and tested.

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.

2 participants