Skip to content

Commit 4d09893

Browse files
elianddbclaude
andcommitted
Fix UNION column mapping bug in nested multi-way operations
Fixed column scrambling in complex UNION queries with 3+ branches and 10+ columns. The bug caused duplicate rows with wrong column positions when processing nested UNION operations like (A UNION B) UNION C. Root cause: colIdsForRel() used sorted ColSet.ForEach() for SetOp nodes, losing original SELECT column order. This caused wrong column mappings in mergeSetOpSchemas() for nested UNIONs. Solution: SetOp nodes now preserve schema order by recursing to left child instead of using sorted column set, matching MySQL behavior. Added regression test that reproduces the exact threshold where bug occurs. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 466cdf1 commit 4d09893

File tree

2 files changed

+37
-0
lines changed

2 files changed

+37
-0
lines changed

enginetest/queries/script_queries.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11237,6 +11237,38 @@ where
1123711237
},
1123811238
},
1123911239
},
11240+
{
11241+
Name: "UNION column mapping bug - issue 9628",
11242+
SetUpScript: []string{
11243+
"CREATE TABLE report_card (id INT PRIMARY KEY, name VARCHAR(255), entity_id INT, dashboard_id INT, description TEXT, display TEXT, collection_preview TEXT, dataset_query TEXT, collection_id INT, archived_directly BOOLEAN DEFAULT FALSE, collection_position INT, database_id INT, archived BOOLEAN DEFAULT FALSE, last_used_at DATETIME, table_id INT, query_type VARCHAR(50), type VARCHAR(50))",
11244+
"CREATE TABLE collection (id INT PRIMARY KEY, name VARCHAR(255), entity_id INT, location VARCHAR(500), authority_level VARCHAR(50), personal_owner_id INT, archived_directly BOOLEAN DEFAULT FALSE, type VARCHAR(50), archived BOOLEAN DEFAULT FALSE, namespace VARCHAR(100))",
11245+
"CREATE TABLE report_dashboard (id INT PRIMARY KEY, name VARCHAR(255), entity_id INT, description TEXT, collection_id INT, archived_directly BOOLEAN DEFAULT FALSE, collection_position INT, archived BOOLEAN DEFAULT FALSE, last_viewed_at DATETIME)",
11246+
"INSERT INTO report_card (id, name, entity_id, dashboard_id, type, archived_directly, archived, collection_position) VALUES (2, 'Card 2', 1002, NULL, 'question', FALSE, FALSE, NULL), (3, 'Card 3', 1003, NULL, 'question', FALSE, FALSE, NULL)",
11247+
"INSERT INTO collection (id, name, entity_id, location, type, archived, personal_owner_id, namespace) VALUES (2, 'Collection 2', 4002, '/test2/', 'normal', FALSE, NULL, NULL), (3, 'Collection 3', 4003, '/test3/', 'normal', FALSE, NULL, NULL)",
11248+
"INSERT INTO report_dashboard (id, name, entity_id, description, collection_id, archived_directly, archived, collection_position) VALUES (1, 'Dashboard 1', 3001, 'Test dashboard description', NULL, FALSE, FALSE, NULL), (2, 'Dashboard 2', 3002, 'Test dashboard 2 description', NULL, FALSE, FALSE, NULL)",
11249+
},
11250+
Assertions: []ScriptTestAssertion{
11251+
{
11252+
Query: `WITH visible_collection_ids AS (SELECT id FROM collection AS c WHERE (1 <> c.id))
11253+
SELECT 5 AS model_ranking, c.id, c.name, c.description, c.entity_id, c.display, c.collection_preview, c.dataset_query, c.collection_id, 'card' AS model
11254+
FROM report_card AS c WHERE (c.dashboard_id IS NULL) AND (archived = FALSE) AND (c.type = 'question')
11255+
UNION
11256+
SELECT 7 AS model_ranking, id, name, name AS description, entity_id, NULL AS display, NULL AS collection_preview, NULL AS dataset_query, id AS collection_id, 'collection' AS model
11257+
FROM collection AS col WHERE (archived = FALSE) AND (id <> 1) AND (personal_owner_id IS NULL) AND (namespace IS NULL)
11258+
UNION
11259+
SELECT 1 AS model_ranking, d.id, d.name, d.description, d.entity_id, NULL AS display, NULL AS collection_preview, NULL AS dataset_query, NULL AS collection_id, 'dashboard' AS model
11260+
FROM report_dashboard AS d WHERE (archived = FALSE)`,
11261+
Expected: []sql.Row{
11262+
{5, 2, "Card 2", nil, 1002, nil, nil, nil, nil, "card"},
11263+
{5, 3, "Card 3", nil, 1003, nil, nil, nil, nil, "card"},
11264+
{7, 2, "Collection 2", "Collection 2", 4002, nil, nil, nil, 2, "collection"},
11265+
{7, 3, "Collection 3", "Collection 3", 4003, nil, nil, nil, 3, "collection"},
11266+
{1, 1, "Dashboard 1", "Test dashboard description", 3001, nil, nil, nil, nil, "dashboard"},
11267+
{1, 2, "Dashboard 2", "Test dashboard 2 description", 3002, nil, nil, nil, nil, "dashboard"},
11268+
},
11269+
},
11270+
},
11271+
},
1124011272
}
1124111273

1124211274
var SpatialScriptTests = []ScriptTest{

sql/planbuilder/set_op.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,11 @@ func colIdsForRel(n sql.Node) []sql.ColumnId {
228228
}
229229
return ids
230230
case plan.TableIdNode:
231+
// SetOp nodes need to preserve original schema order to avoid column scrambling in nested UNIONs
232+
if setOp, ok := n.(*plan.SetOp); ok {
233+
return colIdsForRel(setOp.Left())
234+
}
235+
231236
cols := n.Columns()
232237
if tn, ok := n.(sql.TableNode); ok {
233238
if pkt, ok := tn.UnderlyingTable().(sql.PrimaryKeyTable); ok && len(pkt.PrimaryKeySchema().Schema) != len(n.Schema()) {

0 commit comments

Comments
 (0)