diff --git a/enginetest/queries/script_queries.go b/enginetest/queries/script_queries.go index 275c914271..2895f4630a 100644 --- a/enginetest/queries/script_queries.go +++ b/enginetest/queries/script_queries.go @@ -120,6 +120,26 @@ type ScriptTestAssertion struct { // Unlike other engine tests, ScriptTests must be self-contained. No other tables are created outside the definition of // the tests. var ScriptTests = []ScriptTest{ + { + // https://github.com/dolthub/dolt/issues/9797 + Name: "EXISTS subquery returns duplicate rows with PRIMARY KEY", + SetUpScript: []string{ + "CREATE TABLE t(c0 INT, c1 INT, PRIMARY KEY(c0, c1));", + "INSERT INTO t VALUES (1, 1);", + "INSERT INTO t VALUES (2, 2);", + "INSERT INTO t VALUES (2, 3);", + }, + Assertions: []ScriptTestAssertion{ + { + Query: "SELECT * FROM t WHERE EXISTS (SELECT 1 FROM t AS x WHERE x.c0 = t.c0);", + Expected: []sql.Row{ + {1, 1}, + {2, 2}, + {2, 3}, + }, + }, + }, + }, { // https://github.com/dolthub/dolt/issues/9794 Name: "UPDATE with TRIM function on TEXT column", diff --git a/sql/analyzer/indexed_joins.go b/sql/analyzer/indexed_joins.go index 5d4ade4fbb..fe1aedb5a8 100644 --- a/sql/analyzer/indexed_joins.go +++ b/sql/analyzer/indexed_joins.go @@ -472,9 +472,9 @@ func convertSemiToInnerJoin(m *memo.Memo) error { } // join and its commute are a new group - joinGrp := m.MemoizeInnerJoin(nil, semi.Left, rightGrp, plan.JoinTypeInner, semi.Filter) + joinGrp := m.MemoizeInnerJoin(nil, semi.Left, rightGrp, plan.JoinTypeSemi, semi.Filter) // TODO: can't commute if right SubqueryAlias references outside scope (OuterScopeVisibility/IsLateral) - m.MemoizeInnerJoin(joinGrp, rightGrp, semi.Left, plan.JoinTypeInner, semi.Filter) + m.MemoizeInnerJoin(joinGrp, rightGrp, semi.Left, plan.JoinTypeSemi, semi.Filter) // project belongs to the original group leftCols := semi.Left.RelProps.OutputCols() diff --git a/sql/plan/join.go b/sql/plan/join.go index 8709b45cbe..a996007a21 100644 --- a/sql/plan/join.go +++ b/sql/plan/join.go @@ -177,7 +177,7 @@ func (i JoinType) IsPartial() bool { switch i { case JoinTypeSemi, JoinTypeAnti, JoinTypeAntiIncludeNulls, JoinTypeSemiHash, JoinTypeAntiHash, JoinTypeAntiHashIncludeNulls, JoinTypeAntiLookup, JoinTypeAntiLookupIncludeNulls, - JoinTypeSemiLookup: + JoinTypeSemiLookup, JoinTypeSemiMerge, JoinTypeAntiMerge, JoinTypeAntiMergeIncludeNulls: return true default: return false diff --git a/sql/plan/subquery.go b/sql/plan/subquery.go index 296ef0ed9f..289fcf4e41 100644 --- a/sql/plan/subquery.go +++ b/sql/plan/subquery.go @@ -431,6 +431,7 @@ func (s *Subquery) HashMultiple(ctx *sql.Context, row sql.Row) (sql.KeyValueCach // HasResultRow returns whether the subquery has a result set > 0. func (s *Subquery) HasResultRow(ctx *sql.Context, row sql.Row) (bool, error) { + // First check if the query was cached. s.cacheMu.Lock() cached := s.resultsCached diff --git a/sql/rowexec/join_iters.go b/sql/rowexec/join_iters.go index fbfcca13cc..c64494733e 100644 --- a/sql/rowexec/join_iters.go +++ b/sql/rowexec/join_iters.go @@ -367,6 +367,10 @@ func (i *existsIter) Next(ctx *sql.Context) (sql.Row, error) { nextState = esIncRight } case esRet: + if i.typ.IsSemi() { + // For semi-joins, after returning a match, move to next left row + nextState = esIncLeft + } return i.removeParentRow(i.primaryRow.Copy()), nil default: return nil, fmt.Errorf("invalid exists join state")