Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions enginetest/queries/join_queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,86 @@ on w = 0;`,
}

var JoinScriptTests = []ScriptTest{
{
// Correlated EXISTS whose body is a scalar aggregate (implicit grouping)
// must remain TRUE even when the match set is empty: aggregates emit one
// row over empty input. Stripping *plan.GroupBy during semi-join unnest
// used to turn empty-input cardinality 1 into 0.
Name: "EXISTS with scalar aggregate body (empty-match cardinality)",
SetUpScript: []string{
"CREATE TABLE t(id INT PRIMARY KEY, a INT);",
"CREATE TABLE u(x INT);",
"CREATE TABLE e(y INT);",
"INSERT INTO t VALUES (1,10),(2,20),(3,30);",
// Only id=1 has a match; 2 and 3 are empty-match outer rows.
"INSERT INTO u VALUES (10),(10);",
},
Assertions: []ScriptTestAssertion{
// Bare-column control: existence follows real matches only.
{
Query: "SELECT id FROM t WHERE EXISTS (SELECT u.x FROM u WHERE u.x = t.a) ORDER BY id",
Expected: []sql.Row{
{1},
},
},
// Scalar SUM over empty match set still yields one row (NULL), so
// EXISTS is TRUE for every outer row.
{
Query: "SELECT id FROM t WHERE EXISTS (SELECT SUM(u.x) FROM u WHERE u.x = t.a) ORDER BY id",
Expected: []sql.Row{
{1},
{2},
{3},
},
},
// NOT EXISTS of a scalar aggregate is always FALSE per outer row.
{
Query: "SELECT id FROM t WHERE NOT EXISTS (SELECT SUM(u.x) FROM u WHERE u.x = t.a) ORDER BY id",
Expected: []sql.Row{},
},
// COUNT(*) is the same scalar-aggregate shape.
{
Query: "SELECT id FROM t WHERE EXISTS (SELECT COUNT(*) FROM u WHERE u.x = t.a) ORDER BY id",
Expected: []sql.Row{
{1},
{2},
{3},
},
},
// Explicit GROUP BY does not emit a row for empty groups, so EXISTS
// is FALSE when there is no match.
{
Query: "SELECT id FROM t WHERE EXISTS (SELECT SUM(u.x) FROM u WHERE u.x = t.a GROUP BY u.x) ORDER BY id",
Expected: []sql.Row{
{1},
},
},
// Uncorrelated scalar-aggregate EXISTS in FILTER position with an
// empty match set: previously wrong on main (Limit-CrossJoin path
// after GroupBy strip). Broken on base; fixed by this patch.
{
Query: "SELECT id FROM t WHERE EXISTS (SELECT SUM(u.x) FROM u WHERE u.x = 999) ORDER BY id",
Expected: []sql.Row{
{1},
{2},
{3},
},
},
{
Query: "SELECT id FROM t WHERE NOT EXISTS (SELECT SUM(u.x) FROM u WHERE u.x = 999) ORDER BY id",
Expected: []sql.Row{},
},
// Fully empty table: scalar SUM still emits one NULL row.
{
Query: "SELECT id FROM t WHERE EXISTS (SELECT SUM(e.y) FROM e) ORDER BY id",
Expected: []sql.Row{
{1},
{2},
{3},
},
},
},
},
{
Name: "Simple join query",
SetUpScript: []string{},
Expand Down
15 changes: 14 additions & 1 deletion sql/analyzer/unnest_exists_subqueries.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,20 @@ func simplifyPartialJoinParents(n sql.Node) (sql.Node, bool) {
switch n := ret.(type) {
case *plan.Having:
return nil, false
case *plan.Project, *plan.GroupBy, *plan.Sort, *plan.Distinct, *plan.TopN, *plan.Limit:
case *plan.GroupBy:
// Scalar aggregates (empty GROUP BY key list) emit exactly one row
// even when the child produces no rows. Stripping them changes
// empty-input cardinality from 1 to 0, so correlated EXISTS over a
// scalar aggregate wrongly drops all outer rows (and NOT EXISTS
// wrongly keeps them). Refuse unnest for this shape so the
// ExistsSubquery path evaluates the aggregate correctly. Explicit
// GROUP BY keys do not have this empty-input row guarantee and may
// still be stripped.
if len(n.GroupByExprs) == 0 {
return nil, false
}
ret = n.Children()[0]
case *plan.Project, *plan.Sort, *plan.Distinct, *plan.TopN, *plan.Limit:
// TODO: In most cases, it's necessary to remove *plan.Limit because child Filter nodes will have been
// hoisted out. But what if Limit.Limit evals to 0? https://github.com/dolthub/dolt/issues/10493
ret = n.Children()[0]
Expand Down
Loading