You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found while reviewing #28, which fixes the same defect class for Expr::CompoundFieldAccess. This is Expr::CompoundIdentifier — the same names written without a subscript — and it is a fourth fabrication site of the kind #23 was about.
The same column, spelled two ways, gives two different answers
The second is wrong in a way that is easy to miss: the dotted name is crammed into the table field of a TableRef whose schema stays null. That TableRef is not equal to the one in tables.inputs, which is correctly { schema: "sch", table: "tbl" } — so a consumer joining column lineage to table lineage by TableRef finds no match. This is ordinary schema.table.column SQL, not an exotic shape, and fully qualifying is exactly what people do in a join:
SELECTsch.tbl.a FROMsch.tblJOINsch.otherONsch.tbl.id =sch.other.id
inputs: sch.other, sch.tbl
a -> Concrete { table: "sch.tbl", column: "a" }
Cause
The binding is registered under the last name component and looked up by the whole dotted string.
build/select.rs keys the binding by table_ref.table:
let alias_name = alias
.as_ref().map_or_else(|| table_ref.table.clone(), |a| a.name.value.clone());self.add_binding(alias_name,Binding::Table(table_ref));
build/select.rs::split_compound builds the qualifier by joining every component but the last:
let qualifier = parts[..len - 1].iter().map(|p| p.value.as_str()).collect::<Vec<_>>().join(".");
So FROM sch.tbl registers "tbl", SELECT sch.tbl.col asks for "sch.tbl", the lookup misses, and resolve/mod.rs invents a relation from the string it failed to resolve:
They can only agree when the reference carries exactly one qualifier component. SELECT sch.tbl.col FROM sch.tbl AS tbl misses too — the alias is tbl, the qualifier is still sch.tbl.
Three symptoms, one arm
That None arm is reached three different ways, and Concrete is wrong in all three:
Concrete { table: "t", column: "payload" } — payload is the physical column, items a struct field
SELECT zz.col FROM t
Concrete { table: "zz" }
Unresolved { column: "col" } — nothing in scope is named zz
The middle row is what #28 fixes for the subscripted spelling (t.payload.items[1]), so after #28 those two spellings of the same expression disagree.
The last row is the plain #23 case. #23 routed three fabrication sites to Unresolved and left this one, because it only reached the unqualified paths.
What a fix needs
The qualifier has to be matched against bindings as a relation name of one or more components rather than as a single string, longest prefix first, before falling back:
try the longest leading run of components that names a binding — that is the relation, and the next component is the column;
otherwise, if the first component alone names a binding, the second is the column and the rest is a struct field path (this is what collect_compound_field_ancestors already does in fix: stop naming relations and columns that do not exist #28 for the subscripted form);
otherwise nothing in scope owns the column, so Unresolved.
a.b.c is genuinely ambiguous between (1) and (2) at the syntax level, and the scope is the only thing that can settle it — the same argument #28's commit makes. Worth doing both spellings in one change so they cannot drift apart again.
Related: #23 (the rule this breaks), #28 (same class, CompoundFieldAccess), #14/#16 (360e1e7 reworks this area and adds bound-field machinery).
Found while reviewing #28, which fixes the same defect class for
Expr::CompoundFieldAccess. This isExpr::CompoundIdentifier— the same names written without a subscript — and it is a fourth fabrication site of the kind #23 was about.The same column, spelled two ways, gives two different answers
The second is wrong in a way that is easy to miss: the dotted name is crammed into the
tablefield of aTableRefwhoseschemastaysnull. ThatTableRefis not equal to the one intables.inputs, which is correctly{ schema: "sch", table: "tbl" }— so a consumer joining column lineage to table lineage byTableReffinds no match. This is ordinaryschema.table.columnSQL, not an exotic shape, and fully qualifying is exactly what people do in a join:Cause
The binding is registered under the last name component and looked up by the whole dotted string.
build/select.rskeys the binding bytable_ref.table:build/select.rs::split_compoundbuilds the qualifier by joining every component but the last:So
FROM sch.tblregisters"tbl",SELECT sch.tbl.colasks for"sch.tbl", the lookup misses, andresolve/mod.rsinvents a relation from the string it failed to resolve:They can only agree when the reference carries exactly one qualifier component.
SELECT sch.tbl.col FROM sch.tbl AS tblmisses too — the alias istbl, the qualifier is stillsch.tbl.Three symptoms, one arm
That
Nonearm is reached three different ways, andConcreteis wrong in all three:SELECT sch.tbl.col FROM sch.tblConcrete { table: "sch.tbl" }Concrete { schema: "sch", table: "tbl", column: "col" }SELECT t.payload.items FROM tConcrete { table: "t.payload" }Concrete { table: "t", column: "payload" }—payloadis the physical column,itemsa struct fieldSELECT zz.col FROM tConcrete { table: "zz" }Unresolved { column: "col" }— nothing in scope is namedzzThe middle row is what #28 fixes for the subscripted spelling (
t.payload.items[1]), so after #28 those two spellings of the same expression disagree.The last row is the plain #23 case. #23 routed three fabrication sites to
Unresolvedand left this one, because it only reached the unqualified paths.What a fix needs
The qualifier has to be matched against bindings as a relation name of one or more components rather than as a single string, longest prefix first, before falling back:
collect_compound_field_ancestorsalready does in fix: stop naming relations and columns that do not exist #28 for the subscripted form);Unresolved.a.b.cis genuinely ambiguous between (1) and (2) at the syntax level, and the scope is the only thing that can settle it — the same argument #28's commit makes. Worth doing both spellings in one change so they cannot drift apart again.Related: #23 (the rule this breaks), #28 (same class,
CompoundFieldAccess), #14/#16 (360e1e7reworks this area and adds bound-field machinery).