planner: substitute merged DML IN/NOT IN subqueries via ListArg placeholder#19973
Conversation
…holder When an UPDATE SET expression contains an IN/NOT IN subquery whose routing is compatible with the outer route, the planner merges the subquery into the outer route. The merge-time AST rewrite walked expressions looking for *sqlparser.Argument placeholders to substitute back with the original *Subquery, but the DML SET path mints sqlparser.ListArg placeholders for IN/NOT IN (a separate AST type). The placeholder leaked into the emitted SQL with no UncorrelatedSubquery primitive to back it, so vttablet would have failed with "missing bind var __sqN". Add a sqlparser.ListArg arm to the type switch in rewriteMergedSubqueryExpr so the same substitution path applies. Fixes #19965 Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
Review ChecklistHello reviewers! 👋 Please follow this checklist when reviewing this Pull Request. General
Tests
Documentation
New flags
If a workflow is added or modified:
Backward compatibility
|
There was a problem hiding this comment.
Pull request overview
Fixes a vtgate planner correctness bug where merged IN/NOT IN subqueries inside UPDATE ... SET could leave behind a ListArg placeholder (e.g. ::__sqN) in the emitted SQL, causing execution-time failures due to missing bind vars.
Changes:
- Extend
rewriteMergedSubqueryExprto also recognize and rewritesqlparser.ListArgplaceholders when substituting merged subqueries back inline. - Add regression plan-test cases covering
UPDATE ... SETwith merge-compatibleINandNOT INsubqueries.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| go/vt/vtgate/planbuilder/operators/subquery_planning.go | Add sqlparser.ListArg handling so merged DML IN/NOT IN placeholders are rewritten back to the original inline subquery. |
| go/vt/vtgate/planbuilder/testdata/dml_cases.json | Add regression cases ensuring UPDATE ... SET merged IN/NOT IN subqueries no longer emit ::__sqN. |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #19973 +/- ##
===========================================
- Coverage 69.67% 3.47% -66.20%
===========================================
Files 1614 60 -1554
Lines 216793 9758 -207035
===========================================
- Hits 151044 339 -150705
+ Misses 65749 9419 -56330
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
The two new dml_cases.json entries hit MySQL errno 1093 ("can't specify
target table for update in FROM clause") because the merged subquery
references the same table being UPDATEd. Wrapping the inner reference
in a derived table avoids the restriction without changing the merge
path the test is meant to cover (verified: reverting the planner fix
still produces the leaked ::__sq1 placeholder).
Signed-off-by: Arthur Schreiber <arthur@planetscale.com>
…holder (vitessio#19973) Signed-off-by: Arthur Schreiber <arthur@planetscale.com> Signed-off-by: Tim Vaillancourt <tim@timvaillancourt.com>
Description
When an
UPDATESETexpression contains anIN/NOT INsubquery whose routing is compatible with the outer route, the planner merges the subquery into the outer route — but the merge-time AST rewrite only substituted back*sqlparser.Argumentplaceholders, neversqlparser.ListArg. DMLSETexpressions placeholderIN/NOT INsubqueries withListArg(a distinct AST type), so the placeholder leaked into the emitted SQL with noUncorrelatedSubqueryprimitive above to back it. Sending such a plan to vttablet would fail withmissing bind var __sqN.The fix adds a
case sqlparser.ListArgarm to the type switch inrewriteMergedSubqueryExpr(go/vt/vtgate/planbuilder/operators/subquery_planning.go) mirroring the existing*Argumentbranch — the samecursor.Replace(sq.originalSubquery)path then substitutes the original inline subquery, matching the shape thatDELETE … WHERE col = (1 in (select …))already produces.Two regression cases are added to
dml_cases.jsoncovering UPDATE+IN and UPDATE+NOT IN with merge-compatible routing. The existingUPDATE: same subquery as scalar SET and IN SETcase targetsuser_extrainside auserupdate, so the routes don't merge and the bug couldn't surface there.Adjacent rewrite paths (
Ordering.settleOrderingExpressions, projectionpe.Original) were intentionally left alone —isDML=trueis only set inupdate.gofor SET expressions, so aListArgplaceholder cannot reach those paths under any current call graph.Related Issue(s)
Fixes #19965
Checklist
Deployment Notes
None — purely planner-level correctness fix. No previously-working query shape changes plan; the affected shape (DML SET with merge-compatible IN/NOT IN subquery) was emitting a broken plan that would have failed at vttablet, so no production deployments are exercising it today.
AI Disclosure
Most of this was written by Claude Code — I just provided direction.