Repository navigation
Conversation
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
Add ColumnId, the optional id on Column and on DFSchema fields, the per-session ColumnIdGenerator and a report-only verifier. Nothing allocates ids yet, so plans and behaviour do not change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… them Scans and other leaves allocate ids, references bind to them, node schemas pass them through, and plans blended together are renumbered. The verifier runs in tests with an allowlist of rules that do not keep ids yet. Names still decide identity. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every plan builder has a generator. The outputs of Projection, Window, DistinctOn and Aggregate become NamedExpr with a required id, and the columns nodes define without expressions (union, recursive query, unnest, join mark, grouping id) get ids too. Grouping sets are flattened out of Aggregate.group_expr. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Rules find columns by id and build references with ids. A TableScan keeps the ids of every source column. The allowlist shrinks to the rules with known remaining issues. Plans do not change. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Carry ids through protobuf and Substrait, let extension nodes opt in, make the helpers and checks public for downstream code, and give MERGE INTO target columns ids. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The switch. Names come from definitions and are restored at the root and for writes, equality includes ids, outputs may share a name when their ids differ, columns with an id are looked up by id only, and rules no longer preserve names. __common_expr_N and __datafusion_extracted_N go away. Adds column_identity.slt with the bugs this fixes. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Bind pulled up references by id (apache#26234, apache#26033), build join schemas with ids (apache#14147), fix fallback and join key id allocation, read by id where DataFrame methods rebuild columns, keep same-named UNNEST outputs apart, and keep rejecting ambiguous joins. Adds regression tests for apache#13476, apache#26620 and apache#21066. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Skip the name check when all fields have ids, build a scan's source schema only where needed, allocate a scan's ids in one block, compare id ranges before hashing when blending plans and subqueries, and renumber a CTE only from its second reference on. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Targeted id-based fixes for apache#15558 (substitute through an equality of the middle subquery) and apache#21727 (flatten EXISTS or IN into EXISTS or IN), to rules slated for replacement by apache#5492. Shapes these do not cover now error with a stated reason. Remove the verifier's allowlist. Co-Authored-By: Claude Opus 5.5 <[email protected]>
cetra3
force-pushed
the
column-identity
branch
from
November 6, 2026 04:03
83b8baf to
e0677c4
Compare
Contributor
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
This is a draft to discuss a direction, not a PR to merge as is. It shows what giving logical plan columns an identity looks like end to end, and which bugs it fixes. It replaces the earlier version of this draft, which put the ids on implicit aliases. The commits are nine stages, so each can be read on its own (see "What changes are included").
The core change
A column gets a
ColumnIdwhere it is defined, and references read it by that id. The name becomes a label.Ids come from a
ColumnIdGeneratorper session. Columns that a node defines without an expression (union and recursive query outputs, unnested columns, the join mark column) keep their ids in the node's schema.TableScankeeps the ids of its source columns in a sharedArc<[ColumnId]>, so rebuilding a scan doesn't allocate per column.Which issue does this PR close?
It closes no issues as a draft. It is related to the leaf expression and filter pushdown epic, #25459, and to the older discussions about column identity (#8379, #12723, #1468).
Bugs fixed
Each row was run on
mainat888879d01cand on this branch. All are open except #26619.Wrong results
main1;2;1;(1,1),(2,0)(c,-9),(b,-10)(a,-2),(d,-3)1); Q2 and Q3No field named ...1,1;1;1Errors on valid queries
mainNo field named t.a1 2 4;1 2 xNo field named x.a(1,1),(1,2)No field named l.id(1,10),(2,20)No field named __correlated_sq_1.y1, 2(both queries)main; the projection form still fails withNo field named __correlated_sq_1.c0qualified field name e.email and unqualified field name email which would be ambiguousb@x, a@yduplicate unqualified field name, orProjections/Windows require unique expression names(5 forms)duplicate unqualified field name "approx_distinct(t.c)"99 99Projections require unique expression namesc1columnsProjections require unique expression names1 1Projections require unique expression names1, 3, 5Projections require unique expression names(SELECT ts, id, *)"?table?".id1__common_expr_1 is a reserved DataFusion column nameNo field named t0.g(a column two levels up)1No field named e1.dept_id(a column two levels up)(c,60),(e,30)Other
mainall window expressions shall have the same sort keysget_fieldis extracted and computed twice#21727 and #15558 use targeted rewrites that only cover some shapes; see "Decorrelation: deferred work". #26622 was on the earlier list, but it now gives the same output on
main, so it is not counted here.Rationale for this change
The optimizer identifies a column by its qualifier and name. Two different columns can have the same name: a column and a cast of it, two scans of the same table, the outputs of two references to a CTE, or two aggregates that differ only by a cast. When a rule moves or rebuilds a node, it finds the columns it needs by name, and with such plans it finds the wrong one, or none. Users see wrong results,
No field namederrors, duplicate name errors and panics.Each of these bugs has been fixed, or is being fixed, in the rule that hit it. The cause is shared, so the class keeps coming back. Spark (
ExprId), DuckDB (ColumnBinding), Trino (symbols) and CockroachDB (ColumnID) all give each column an identity independent of its name.What changes are included in this PR?
179 files, +18,951/−4,450. Each commit is one stage, and each builds and passes on its own (see the testing section for the exception).
ColumnId, optional ids onColumnandDFSchema, the generator, a report-only verifier. Nothing allocates ids, so nothing changes.NamedExpr, node-defined columns, grouping sets flattened out ofAggregate.group_expr.MERGE INTO.Stages 1 and 2 are the smallest, but they fix nothing by themselves: the benefit starts at stage 6, because names decide identity until then.
What the switch changes:
NamePreserveroralias_if_changed.is_equivalent_up_to_column_idsandExpr::without_column_idscompare without them.__common_expr_Nand__datafusion_extracted_Nare gone. Common subexpressions are named after their expressions, and extraction is a typed mark onNamedExpr.Decorrelation: deferred work
Ids fix decorrelation bugs that are about binding: #26234, #26033, #26692, and the projection form of #26619. They do not make decorrelation general. That is #5492, and this PR leaves it open.
Stage 9 is a stopgap. On #21727, the request was not to extend the existing subquery rewrites because of the risk of more wrong results. Stage 9 does add to them, so it is the last commit, its rewrites are in their own module (
decorrelate_nested.rs), and they are documented as going away with #5492. Both match columns by id, and apply only where they are exact:x = outer.colin the middle subquery lets nested subqueries readx. Only for the same type, not floats or nested types, andxmay be nullable only ifouter.colis.EXISTSorINdirectly inside anEXISTSorINbecomes an inner join in the middle subquery, because only the middle's set of rows matters.Stage 9 can be dropped. #21727 and #15558 would go back to failing, and the verifier's allowlist would return. It also carries
PushDownFilterfixes that would need to be kept.Two-level shapes that still fail. These stop with
Physical plan does not support logical expression Exists/InSubquery/ScalarSubquery, andcolumn_identity.slttests that none of them returns rows:NOT EXISTSorNOT INat the nested levelEXISTSunderORLIMIT,DISTINCTor window in the middle subqueryOR, on floats, or across a castONclause ofMERGE(merge_into.slt)Decorrelating these needs the column two levels up to be in scope where the predicate runs. The middle subquery can't output it without changing its row count, so this takes a dependent join: push the distinct values of the outer columns down as a domain, group or join on them, and join back.
Issues from the #5492 list that this PR does not change. Same results on
mainand on this branch:#26704 (subqueries in an aggregate) is also unchanged. It needs a new rewrite, not a fix to column identity.
What ids could offer #5492 (a proposal, not built here):
with_fresh_column_idsmakes that copy without name clashes.DecorrelatePredicateSubqueryandScalarSubqueryToJoinrules could then be deleted.What is the testing strategy for this PR?
datafusion/sqllogictest/test_files/column_identity.slthas a regression test for the fixed issues and for the shapes that must keep failing. Error when joining dataframes with duplicate column names if dataframes generated from file #14147 and Unparser with PostgreSqlDialect fails after Optimization Pass #21066 have Rust tests, as they need the DataFrame and unparser APIs.DATAFUSION_COLUMN_ID_VERIFY=1) checks after every analyzer and optimizer rule that references have an id, that the id is in scope, and that the name agrees. At the top of the stack it has no allowlist.datafusion-clitests, and the examples.leaf_pushdown_fuzzandleaf_pushdown_fuzz_extended, with the verifier on only. Both pass without the verifier, and from stage 6 on.Performance
Planning time was measured on the
sql_plannerbenchmarks before rebasing onto the currentmain, against934691e9af. Across 142 benchmarks the median change was +2.4% and the mean −0.8%.mainphysical_select_all_from_1000logical_select_all_from_1000physical_many_self_joinslogical_select_one_from_700physical_select_one_from_700physical_unnest_to_joinphysical_intersectionWindow benchmarks are 7–10% slower and TPC-H physical plans 5–8%. What remains is the extra passes over the plan: binding when the analyzer starts, the final relabel, and finding the highest id.
These numbers need rerunning. #26630 (a name index on
DFSchema) has merged since. Lookups by id are still linear scans, so they may not share that gain, and wide schemas may compare worse than shown.Are there any user-facing changes?
Yes. This needs the
api changelabel. The upgrade guide (56.0.0.md) covers the API changes.SELECT a AS c1, b AS c1,SELECT ts, id, *.Columnis#[non_exhaustive]and has anid.Projection,Window,DistinctOnandAggregatetakeNamedExprs.Expr::GroupingSetno longer appears inAggregate.group_expr.OptimizerConfig::column_id_generatoris required.Union::try_newtake a generator.==.INlist as a transformation.Open questions
NamedExprversus optional ids. Required ids on definitions make up a large part of stage 3. An alternative is optional ids enforced by the verifier, withNamedExpras a later tightening.Debughides ids. A computed column keeps the name of its expression, sot.a + t.b * Int64(2)in a logical plan can mean the column namedt.a + t.b, times 2.CASTkeeps its input's name.This PR was written with the help of an AI assistant (Claude Code).
🤖 Generated with Claude Code