Search before asking
Version
8.0.90-v1.2.925-patch-9-6dc65e9df8 (rust-1.94.0-nightly-2026-08-20) — the latest patch release as of 2026-08-21.
8.0.90-v1.2.881-ca29960f5c — the original finding comes from a fuzzing campaign on that build.
8.0.90-v1.2.935-nightly-43b24ec040 (rust nightly 2026-08-23) — still reproduces (all matrix cells identical), so the bug is neither fixed on the latest nightly nor a recent regression.
What's Wrong?
This is closely related to (and extends) the previously-reported #20351 (LEFT SEMI join treats a NULL probe key as the column type's default value), but that report concluded INT probe keys were unaffected — this report shows the defect also hits INT/BIGINT probe keys whenever the join key requires a cross-type coercion, and adds a second symptom: the number of returned rows is unstable (data-dependent under-count).
A correlated EXISTS whose join key compares columns of different numeric types (e.g. b.decimal_col = a.int_col) is decorrelated into a LEFT SEMI hash join. On that path:
- A NULL probe key is treated as the (cast) target type's default value
0 and wrongly matches a build-side 0 — EXISTS incorrectly returns true for rows whose correlation column is NULL.
- The number of wrongly-returned rows is unstable: with three NULL probe rows all of them should be returned if the NULL keys match, but only one is; with two NULL probe rows and no other data, both are returned. Plain
EXISTS must never deduplicate probe rows, so the count itself is also wrong.
The identical predicate evaluated as a scalar in a derived-table projection (the logically-equivalent rewritten form) is evaluated correctly, so the defect is specific to the semi-join execution path.
NOT EXISTS is affected in the opposite direction (rows wrongly dropped), so the bug can both add and remove rows depending on predicate polarity.
How to Reproduce?
CREATE TABLE a(c1 BIGINT NULL);
CREATE TABLE b(c0 DECIMAL(20, 0) NULL);
INSERT INTO a VALUES (NULL), (NULL);
INSERT INTO b VALUES (0);
-- Query: returns 2 rows (both NULL rows) <- WRONG (no value of a matches b)
SELECT a.c1 FROM a WHERE EXISTS (SELECT 1 FROM b WHERE b.c0 = a.c1);
-- Equivalent scalar form: returns 0 rows <- CORRECT
SELECT ref0 FROM (
SELECT a.c1 AS ref0, EXISTS(SELECT 1 FROM b WHERE b.c0 = a.c1) AS ref1 FROM a
) s WHERE ref1;
With three NULL probe rows plus two non-matching values, the count becomes 1 instead of 0 (or 3) — showing the unstable row count:
CREATE TABLE a2(c1 BIGINT NULL);
INSERT INTO a2 VALUES (NULL), (NULL), (NULL), (2097735747), (2109914711);
SELECT count(*) FROM a2 WHERE EXISTS (SELECT 1 FROM b WHERE b.c0 = a2.c1);
-- returns 1 <- WRONG (expected 0; note it is neither 0 nor 3)
Verified behavior matrix (all on v1.2.925-patch-9, single node):
| # |
Probe side (outer) |
Build side (inner) |
Result |
Expected |
Verdict |
| 1 |
BIGINT × 2 NULL rows |
DECIMAL(20,0) {0} |
2 rows |
0 |
WRONG |
| 2 |
BIGINT 3 NULL + 2 non-matching |
DECIMAL(20,0) {0} |
1 row |
0 |
WRONG (unstable count) |
| 3 |
BIGINT 3 NULL + 2 non-matching |
BIGINT {0} (same type) |
0 rows |
0 |
correct |
| 4 |
BIGINT × 2 NULL rows |
DECIMAL(20,0) {205} (no zero) |
0 rows |
0 |
correct — the NULL key matches the default value 0, not arbitrary values |
| 5 |
DECIMAL(20,0) × 2 NULL rows |
BIGINT {0} (reversed coercion) |
2 rows |
0 |
WRONG — both directions affected |
| 6 |
same as 2, NOT EXISTS |
DECIMAL(20,0) {0,…} |
4 rows |
5 |
WRONG — reverse polarity drops rows |
| 7 |
scalar/derived-table form of 1 |
— |
0 rows |
0 |
correct |
| 8 |
BIGINT {48} (a real match) |
DECIMAL(20,0) {0,205,73,48} |
1 row |
1 |
correct — normal matching still works |
Cluster information: single node, local datafuselabs/databend Docker container (MySQL protocol). No special configuration. Found by an automated equivalence-testing fuzzer (SQLancer-derived) that compares a query against a logically-equivalent rewrite.
Relationship to open PR #20354
PR #20354 ("preserve null validity in correlated joins") removes the check_for_eliminate_valids short-circuit so the NULL-validity bitmap (build_valids_by_keys) is always computed from the actual key data — which should, in principle, also cover keys that pass through a cross-type coercion, since CAST(NULL AS DECIMAL) stays NULL at runtime. However:
- The PR is still open (not merged into
main as of the v1.2.935-nightly build of 2026-08-23), and the bug fully reproduces on that nightly — so it cannot have been fixed by the shipped code.
- The PR's new regression tests only cover same-type join keys (
DECIMAL(6,0), BOOLEAN, TIMESTAMP columns on both sides); there is no cross-type (INT = DECIMAL) case in the test suite.
- This report additionally documents a symptom the valids fix alone may not explain: the unstable output cardinality (3 NULL probe rows + 2 non-matching values → only 1 row returned; 2 NULL rows alone → 2 rows), which suggests the semi join also mishandles probe-row multiplicity after the wrong NULL match.
Are you willing to submit PR?
Search before asking
Version
8.0.90-v1.2.925-patch-9-6dc65e9df8(rust-1.94.0-nightly-2026-08-20) — the latest patch release as of 2026-08-21.8.0.90-v1.2.881-ca29960f5c— the original finding comes from a fuzzing campaign on that build.8.0.90-v1.2.935-nightly-43b24ec040(rust nightly 2026-08-23) — still reproduces (all matrix cells identical), so the bug is neither fixed on the latest nightly nor a recent regression.What's Wrong?
This is closely related to (and extends) the previously-reported #20351 (LEFT SEMI join treats a NULL probe key as the column type's default value), but that report concluded
INTprobe keys were unaffected — this report shows the defect also hitsINT/BIGINTprobe keys whenever the join key requires a cross-type coercion, and adds a second symptom: the number of returned rows is unstable (data-dependent under-count).A correlated
EXISTSwhose join key compares columns of different numeric types (e.g.b.decimal_col = a.int_col) is decorrelated into aLEFT SEMIhash join. On that path:0and wrongly matches a build-side0—EXISTSincorrectly returns true for rows whose correlation column is NULL.EXISTSmust never deduplicate probe rows, so the count itself is also wrong.The identical predicate evaluated as a scalar in a derived-table projection (the logically-equivalent rewritten form) is evaluated correctly, so the defect is specific to the semi-join execution path.
NOT EXISTSis affected in the opposite direction (rows wrongly dropped), so the bug can both add and remove rows depending on predicate polarity.How to Reproduce?
With three NULL probe rows plus two non-matching values, the count becomes 1 instead of 0 (or 3) — showing the unstable row count:
Verified behavior matrix (all on v1.2.925-patch-9, single node):
BIGINT× 2 NULL rowsDECIMAL(20,0){0}BIGINT3 NULL + 2 non-matchingDECIMAL(20,0){0}BIGINT3 NULL + 2 non-matchingBIGINT{0} (same type)BIGINT× 2 NULL rowsDECIMAL(20,0){205} (no zero)DECIMAL(20,0)× 2 NULL rowsBIGINT{0} (reversed coercion)NOT EXISTSDECIMAL(20,0){0,…}BIGINT{48} (a real match)DECIMAL(20,0){0,205,73,48}Cluster information: single node, local
datafuselabs/databendDocker container (MySQL protocol). No special configuration. Found by an automated equivalence-testing fuzzer (SQLancer-derived) that compares a query against a logically-equivalent rewrite.Relationship to open PR #20354
PR #20354 ("preserve null validity in correlated joins") removes the
check_for_eliminate_validsshort-circuit so the NULL-validity bitmap (build_valids_by_keys) is always computed from the actual key data — which should, in principle, also cover keys that pass through a cross-type coercion, sinceCAST(NULL AS DECIMAL)stays NULL at runtime. However:mainas of the v1.2.935-nightly build of 2026-08-23), and the bug fully reproduces on that nightly — so it cannot have been fixed by the shipped code.DECIMAL(6,0),BOOLEAN,TIMESTAMPcolumns on both sides); there is no cross-type (INT = DECIMAL) case in the test suite.Are you willing to submit PR?