fix: fallback UPDATE planner produces nondeterministic project layout - #24794
Conversation
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
XuPeng-SH
left a comment
There was a problem hiding this comment.
LGTM after reviewing the current head from 3 angles.
- Logic/correctness: the fix removes both nondeterministic table-block ordering and nondeterministic per-column block ordering in fallback UPDATE planning, and the downstream consumers bind updated values by column name via updateColPosMap rather than depending on the prior random layout.
- Behavior/compatibility: this is a layout-stabilization change, not a semantic rewrite of UPDATE results; it makes planner output deterministic across builds without changing the intended column-to-value mapping.
- Tests: the new deterministic planner unit test directly covers the reported regression path. A SQL-level BVT could be a nice follow-up hardening, but I do not see a blocking gap for this fix.
Approve.
Merge Queue Status
This pull request spent 1 hour 6 minutes 14 seconds in the queue, including 1 hour 4 minutes 30 seconds running CI. Required conditions to merge
|
What type of PR is this?
Which issue(s) this PR fixes:
issue ##24718
What this PR does / why we need it:
Root cause
In pkg/sql/plan/build_constraint_util.go inside getUpdateTableInfo (around line 218):
// remove unused table
newTblInfo := &dmlTableInfo{...}
for alias, columns := range usedTbl { // ← non-deterministic map iteration
idx := tblInfo.alias[alias]
...
newTblInfo.tableDefs = append(newTblInfo.tableDefs, tblDef)
newTblInfo.alias[alias] = len(newTblInfo.tableDefs) - 1
...
}
usedTbl is a map[string]map[string]tree.Expr populated from the SET list. For UPDATE emp, dept SET emp.comm=1, dept.loc=... it has two keys ("emp", "dept"). Iterating it with range randomizes
the order per Go map semantics, so newTblInfo.tableDefs ends up as either [emp, dept] or [dept, emp].
The downstream fallback UPDATE builder (selectUpdateTables / rewriteGeneratedColumnsForUpdate in build_update.go) faithfully follows whatever order it receives — placing each target's columns,
SET expressions, and recomputed generated-column projections in a contiguous block. The plan it produces is internally correct in both orderings.
The test TestUpdateFallbackGeneratedColumnsUseOnUpdateAfterRewrite then asserts a fixed position in the project list:
generatedExpr := requireFallbackSourceProjectExpr(t, logicPlan.GetQuery(),
len(...emp.Cols)+2+len(...dept.Cols)+1, // total projectLen=16
len(...emp.Cols)+1, "on-update-marker") // pos=10 (expects emp first)
Position 10 holds the recomputed emp.ename only when emp is the first block. When the map iteration yields [dept, emp], the emp block lives at offsets 5..14, the recomputed ename lands at index
15, and position 10 is emp.sal — exactly what the failure trace shows.
Verdict
Real underlying bug in upstream main (introduced by 7ff125d): newTblInfo.tableDefs ordering depends on Go map iteration randomness. Should iterate tblInfo.tableDefs in index order (or sort
by tblInfo.alias[alias]) instead of ranging over usedTbl.
The new test added in the same commit only happens to pass most of the time because [emp, dept] is randomly chosen ~50%.
Not related to your gpu_plugin_all branch — neither getUpdateTableInfo nor the test exists there independently; both came in from main via merge.
Suggested upstream fix: replace the range usedTbl loop with an iteration over tblInfo.tableDefs by index, looking up usedTbl[alias] per table to preserve source order.