Skip to content

Commit fb85009

Browse files
authored
Merge pull request #3318 from dolthub/elian/10113
dolthub/dolt#10113: Fix DELETE queries with NOT EXISTS uninitialized subqueries
2 parents b2f8f5c + ba97442 commit fb85009

File tree

7 files changed

+81
-39
lines changed

7 files changed

+81
-39
lines changed

enginetest/queries/script_queries.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,38 @@ type ScriptTestAssertion struct {
122122
// Unlike other engine tests, ScriptTests must be self-contained. No other tables are created outside the definition of
123123
// the tests.
124124
var ScriptTests = []ScriptTest{
125+
{
126+
// https://github.com/dolthub/dolt/issues/10113
127+
Name: "DELETE with NOT EXISTS subquery",
128+
SetUpScript: []string{
129+
`CREATE TABLE IF NOT EXISTS student (
130+
id BIGINT AUTO_INCREMENT,
131+
name VARCHAR(50) NOT NULL,
132+
PRIMARY KEY (id)
133+
);`,
134+
`CREATE TABLE IF NOT EXISTS student_hobby (
135+
id BIGINT AUTO_INCREMENT,
136+
student_id BIGINT NOT NULL,
137+
hobby VARCHAR(50) NOT NULL,
138+
PRIMARY KEY (id)
139+
);`,
140+
"INSERT INTO student (id, name) VALUES (1, 'test1');",
141+
"INSERT INTO student (id, name) VALUES (2, 'test2');",
142+
"INSERT INTO student_hobby (id, student_id, hobby) VALUES (1, 1, 'test1');",
143+
"INSERT INTO student_hobby (id, student_id, hobby) VALUES (2, 2, 'test2');",
144+
"INSERT INTO student_hobby (id, student_id, hobby) VALUES (3, 100, 'test3');",
145+
"INSERT INTO student_hobby (id, student_id, hobby) VALUES (4, 100, 'test3');",
146+
},
147+
Assertions: []ScriptTestAssertion{
148+
{
149+
Query: "delete from student_hobby where not exists (select 1 from student where student.id = student_hobby.student_id);",
150+
},
151+
{
152+
Query: "SELECT * FROM student_hobby ORDER BY id;",
153+
Expected: []sql.Row{{1, 1, "test1"}, {2, 2, "test2"}},
154+
},
155+
},
156+
},
125157
{
126158
// https://github.com/dolthub/dolt/issues/9987
127159
Name: "GROUP BY nil pointer dereference in Dispose when Next() never called",

sql/analyzer/apply_foreign_keys.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,8 @@ func applyForeignKeysToNodes(ctx *sql.Context, a *Analyzer, n sql.Node, cache *f
189189
if n.HasExplicitTargets() {
190190
return n.WithExplicitTargets(foreignKeyHandlers), transform.NewTree, nil
191191
} else {
192-
newNode, err := n.WithChildren(foreignKeyHandlers...)
193-
if err != nil {
194-
return nil, transform.SameTree, err
195-
}
196-
return newNode, transform.NewTree, nil
192+
// For simple DELETEs, update the targets array with foreign key handlers.
193+
return n.WithTargets(foreignKeyHandlers), transform.NewTree, nil
197194
}
198195
default:
199196
return n, transform.SameTree, nil

sql/analyzer/fix_exec_indexes.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ func assignExecIndexes(ctx *sql.Context, a *Analyzer, n sql.Node, scope *plan.Sc
5353
}
5454
}
5555
case *plan.DeleteFrom:
56-
if n.RefsSingleRel && !n.HasExplicitTargets() && scope.IsEmpty() && !n.IsProcNested {
56+
if n.RefsSingleRel && !n.HasExplicitTargets() && scope.IsEmpty() && !n.IsProcNested && !n.Child.Schema().HasVirtualColumns() {
5757
// joins, subqueries, triggers, and procedures preclude fast indexing
5858
return offsetAssignIndexes(n), transform.NewTree, nil
5959
}

sql/plan/delete.go

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@ var ErrDeleteFromNotSupported = errors.NewKind("table doesn't support DELETE FRO
2828
// DeleteFrom is a node describing a deletion from some table.
2929
type DeleteFrom struct {
3030
UnaryNode
31-
// targets are the explicitly specified table nodes from which rows should be deleted. For simple DELETES against a
32-
// single source table, targets do NOT need to be explicitly specified and will not be set here. For DELETE FROM JOIN
33-
// statements, targets MUST be explicitly specified by the user and will be populated here.
34-
explicitTargets []sql.Node
31+
// targets contains the table nodes from which rows should be deleted. For simple DELETEs, this contains the single
32+
// inferred table. For DELETE FROM JOIN, this contains the explicitly specified tables.
33+
targets []sql.Node
34+
// hasExplicitTargets indicates whether the targets were explicitly specified in SQL (e.g., "DELETE t1, t2 FROM ...
35+
// ") vs inferred (e.g., "DELETE FROM table WHERE ...").
36+
hasExplicitTargets bool
3537
// Returning is a list of expressions to return after the delete operation. This feature is not
3638
// supported in MySQL's syntax, but is exposed through PostgreSQL's syntax.
3739
Returning []sql.Expression
@@ -44,37 +46,42 @@ var _ sql.Node = (*DeleteFrom)(nil)
4446
var _ sql.CollationCoercible = (*DeleteFrom)(nil)
4547

4648
// NewDeleteFrom creates a DeleteFrom node.
47-
func NewDeleteFrom(n sql.Node, targets []sql.Node) *DeleteFrom {
49+
func NewDeleteFrom(n sql.Node, targets []sql.Node, hasExplicitTargets bool) *DeleteFrom {
4850
return &DeleteFrom{
49-
UnaryNode: UnaryNode{n},
50-
explicitTargets: targets,
51+
UnaryNode: UnaryNode{n},
52+
targets: targets,
53+
hasExplicitTargets: hasExplicitTargets,
5154
}
5255
}
5356

54-
// HasExplicitTargets returns true if the target delete tables were explicitly specified. This can only happen with
55-
// DELETE FROM JOIN statements – for DELETE FROM statements using a single source table, the target is NOT explicitly
56-
// specified and is assumed to be the single source table.
57+
// HasExplicitTargets returns true if the target delete tables were explicitly specified in SQL. This can only happen
58+
// with DELETE FROM JOIN statements. For DELETE FROM statements using a single source table, the target is NOT
59+
// explicitly specified and is assumed to be the single source table.
5760
func (p *DeleteFrom) HasExplicitTargets() bool {
58-
return len(p.explicitTargets) > 0
61+
return p.hasExplicitTargets
5962
}
6063

6164
// WithExplicitTargets returns a new DeleteFrom node instance with the specified |targets| set as the explicitly
6265
// specified targets of the delete operation.
6366
func (p *DeleteFrom) WithExplicitTargets(targets []sql.Node) *DeleteFrom {
6467
copy := *p
65-
copy.explicitTargets = targets
68+
copy.targets = targets
69+
copy.hasExplicitTargets = true
6670
return &copy
6771
}
6872

69-
// GetDeleteTargets returns the sql.Nodes representing the tables from which rows should be deleted. For a DELETE FROM
70-
// JOIN statement, this will return the tables explicitly specified by the caller. For a DELETE FROM statement this will
71-
// return the single table in the DELETE FROM source that is implicitly assumed to be the target of the delete operation.
73+
// WithTargets returns a new DeleteFrom node instance with the specified |targets| set, preserving the
74+
// hasExplicitTargets flag. This is used for simple DELETEs where targets need to be updated (e.g., with
75+
// foreign key handlers) without changing whether they were explicitly specified.
76+
func (p *DeleteFrom) WithTargets(targets []sql.Node) *DeleteFrom {
77+
copy := *p
78+
copy.targets = targets
79+
return &copy
80+
}
81+
82+
// GetDeleteTargets returns the sql.Nodes representing the tables from which rows should be deleted.
7283
func (p *DeleteFrom) GetDeleteTargets() []sql.Node {
73-
if len(p.explicitTargets) == 0 {
74-
return []sql.Node{p.Child}
75-
} else {
76-
return p.explicitTargets
77-
}
84+
return p.targets
7885
}
7986

8087
// Schema implements the sql.Node interface.
@@ -101,7 +108,7 @@ func (p *DeleteFrom) Resolved() bool {
101108
return false
102109
}
103110

104-
for _, target := range p.explicitTargets {
111+
for _, target := range p.targets {
105112
if target.Resolved() == false {
106113
return false
107114
}
@@ -155,7 +162,7 @@ func (p *DeleteFrom) WithChildren(children ...sql.Node) (sql.Node, error) {
155162
return nil, sql.ErrInvalidChildrenNumber.New(p, len(children), 1)
156163
}
157164

158-
deleteFrom := NewDeleteFrom(children[0], p.explicitTargets)
165+
deleteFrom := NewDeleteFrom(children[0], p.targets, p.hasExplicitTargets)
159166
deleteFrom.Returning = p.Returning
160167
return deleteFrom, nil
161168
}

sql/planbuilder/dml.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,14 @@ func (b *Builder) buildDelete(inScope *scope, d *ast.Delete) (outScope *scope) {
447447
b.qFlags.Set(sql.QFlagDelete)
448448

449449
outScope = b.buildFrom(inScope, d.TableExprs)
450+
451+
// Capture the table node for simple DELETEs before buildWhere wraps it
452+
var targets []sql.Node
453+
var hasExplicitTargets bool
454+
if len(d.Targets) == 0 {
455+
targets = []sql.Node{outScope.node}
456+
}
457+
450458
b.buildWhere(outScope, d.Where)
451459
orderByScope := b.analyzeOrderBy(outScope, outScope, d.OrderBy)
452460
b.buildOrderBy(outScope, orderByScope)
@@ -459,8 +467,8 @@ func (b *Builder) buildDelete(inScope *scope, d *ast.Delete) (outScope *scope) {
459467
outScope.node = plan.NewLimit(limit, outScope.node)
460468
}
461469

462-
var targets []sql.Node
463470
if len(d.Targets) > 0 {
471+
hasExplicitTargets = true
464472
targets = make([]sql.Node, len(d.Targets))
465473
for i, tableName := range d.Targets {
466474
tabName := tableName.Name.String()
@@ -488,7 +496,7 @@ func (b *Builder) buildDelete(inScope *scope, d *ast.Delete) (outScope *scope) {
488496
}
489497
}
490498

491-
del := plan.NewDeleteFrom(outScope.node, targets)
499+
del := plan.NewDeleteFrom(outScope.node, targets, hasExplicitTargets)
492500
del.RefsSingleRel = !outScope.refsSubquery
493501
del.IsProcNested = b.ProcCtx().DbName != ""
494502
outScope.node = del

sql/planbuilder/parse_old_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5082,7 +5082,7 @@ func TestParseCreateTrigger(t *testing.T) {
50825082
plan.NewFilter(
50835083
expression.NewEquals(expression.NewUnresolvedColumn("a"), expression.NewUnresolvedQualifiedColumn("old", "b")),
50845084
plan.NewUnresolvedTable("baz", ""),
5085-
), nil),
5085+
), nil, false),
50865086
plan.NewInsertInto(sql.UnresolvedDatabase(""), plan.NewUnresolvedTable("zzz", ""), plan.NewValues([][]sql.Expression{{
50875087
expression.NewUnresolvedQualifiedColumn("old", "a"),
50885088
expression.NewUnresolvedQualifiedColumn("old", "b"),

sql/planbuilder/scalar.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -372,14 +372,12 @@ func (b *Builder) buildScalar(inScope *scope, e ast.Expr) (ex sql.Expression) {
372372
return values
373373
}
374374
case *ast.ExistsExpr:
375-
sqScope := inScope.push()
376-
sqScope.initSubquery()
377-
selScope := b.buildSelectStmt(sqScope, v.Subquery.Select)
378-
selectString := ast.String(v.Subquery.Select)
379-
sq := plan.NewSubquery(selScope.node, selectString)
380-
sq = sq.WithCorrelated(sqScope.correlated())
381-
b.qFlags.Set(sql.QFlagScalarSubquery)
382-
return plan.NewExistsSubquery(sq)
375+
subquery := b.buildScalar(inScope, v.Subquery)
376+
subqueryPlan, ok := subquery.(*plan.Subquery)
377+
if !ok {
378+
b.handleErr(fmt.Errorf("expected Subquery from ExistsExpr, got %T", subquery))
379+
}
380+
return plan.NewExistsSubquery(subqueryPlan)
383381
case *ast.TimestampFuncExpr:
384382
var (
385383
unit sql.Expression

0 commit comments

Comments
 (0)