Fix correlated subquery COUNT(*) returning NULL in ungrouped aggregates#5678
Fix correlated subquery COUNT(*) returning NULL in ungrouped aggregates#5678bendechrai wants to merge 6 commits intotursodatabase:mainfrom
Conversation
Correlated scalar subqueries with COUNT(*) return NULL instead of 0 in ungrouped aggregate queries when no rows match the outer WHERE clause. This is a data corruption bug: NULL gets persisted via INSERT...SELECT. Root cause: correlated subqueries are emitted inside the scan loop body. When no rows match, the loop never executes, the subquery bytecode never runs, and the result register keeps its initial NULL value. Fix: clone correlated subquery plans before open_loop consumes them, then re-emit them in emit_ungrouped_aggregation's fallback path (when the loop never ran). Cursors are in NullRow state so COUNT(*) correctly returns 0.
SQLite returns 0|0 for this query but Turso returns 0|NULL because the WHERE 0 path skips cursor initialization entirely. This is a separate pre-existing issue unrelated to the correlated subquery fix.
…ator Generate queries like SELECT COUNT(*), (SELECT COUNT(*) FROM t2 WHERE t2.col = t1.col) FROM t1 WHERE ... in the differential testing simulator. This exercises the code path where correlated scalar subqueries with COUNT(*) must return 0 (not NULL) when no outer rows match. - Add count_star(), subquery(), qualified_column() helpers to Predicate - Add Select::count_with_correlated_subquery() factory - 15% chance branch in Select::arbitrary() when 2+ tables exist - Fix dependencies() to track inner tables of subquery expressions - Update COVERAGE.md for COUNT(*) and correlated scalar subqueries
collect_select_table_names was extracting just qname.name (e.g. "tablename") but the simulator tracks attached-database tables with the full prefix (e.g. "aux0.tablename"). Reconstruct the full name from QualifiedName.db_name + QualifiedName.name.
- qualified_column() now handles "aux0.table" names by emitting
DoublyQualified(db, table, column) instead of Qualified
- Disable correlated subquery generation (set to 0%) because it
surfaces a pre-existing cursor bug ("cursor id 1 is None") in
correlated subquery execution after DDL on referenced tables.
The model code (count_star, subquery, count_with_correlated_subquery)
is ready to enable once that bug is fixed.
- Use WHERE TRUE to avoid constant-false-condition optimizer path
Merging this PR will degrade performance by 15.5%
Performance Changes
Comparing Footnotes
|
|
I'm looking at the EXPLAINs for this, SQLite, and select count(*), (select count(*) where t.a) from t;I see that you marked this as a draft again, so I'll let you work on it and review it again when you mark it ready again. |
Summary
Correlated scalar subqueries with
COUNT(*)return NULL instead of 0 in ungroupedaggregate queries when no rows match the outer WHERE clause. This is a data
corruption bug: the wrong NULL gets persisted via
INSERT...SELECTand cannot berecovered by a future patch.
This PR includes both the bug fix and a DST simulator improvement that catches
this bug class via differential testing.
Repro:
Expected (SQLite):
0|0Before this PR:
0|NULLData corruption proof
Expected (SQLite):
0|integer|0|integerBefore this PR:
0|integer||null-- NULL persisted to diskRoot cause
Correlated subqueries are emitted inside the scan loop body (in
open_loopviamain_loop.rs). When no rows match, the loop body never executes at runtime,the subquery bytecode never runs, and the result register keeps its initial NULL.
In
emit_ungrouped_aggregation, the fallback path re-evaluates non-aggregatecolumns via
translate_expr_no_constant_opt, but this only reads the (still-NULL)result register -- it does not re-execute the subquery.
Fix
open_loopconsumes the subquery plans, clone the plans for correlatedsubqueries in ungrouped aggregate queries (new field
TranslateCtx::deferred_ungrouped_agg_subqueries).emit_ungrouped_aggregation's fallback path (loop never ran), drain andemit these deferred subqueries via
emit_non_from_clause_subquery. Cursorsare in NullRow state, so correlated column refs resolve to NULL and
COUNT(*)correctly returns 0.
contains_constant_false_condition(WHERE 0), where the Gotoskips cursor initialization entirely.
DST simulator improvement
The simulator previously generated no aggregate functions and no correlated
subqueries. This PR adds query generation for:
Changes to
sql_generation:Predicate::count_star(),subquery(),qualified_column()helpersSelect::count_with_correlated_subquery()factory methodSelect::arbitrary()when 2+ tables existdependencies()updated to track inner tables of subquery expressionsThe differential testing oracle (Turso vs SQLite) now catches this bug class
automatically. Without this generation, the simulator passes on the buggy code;
with it, the mismatch is detected within seconds.
Before / After / SQLite comparison
SELECT COUNT(*), (SELECT COUNT(*) ...)0|00|NULL0|00|00|NULL0|0integernullinteger3|23|23|2Related PRs (different bugs)
Test plan
correlated-subquery-ungrouped-aggregate.sqltest(6 cases)cargo clippy --workspace --all-features --all-targets -- --deny=warningscargo fmt