Skip to content

Fully-qualified column references fabricate a relation from the dotted qualifier #29

Description

@funcpp

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

SELECT tbl.col     FROM sch.tbl   -- Concrete { schema: "sch", table: "tbl", column: "col" }
SELECT sch.tbl.col FROM sch.tbl   -- Concrete { schema: null,  table: "sch.tbl", column: "col" }

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:

SELECT sch.tbl.a FROM sch.tbl JOIN sch.other ON sch.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:

None => Some(ColumnOrigin::Concrete {
    table: TableRef::new(qual.as_str()),
    column: name.clone(),
}),

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:

now should be
SELECT sch.tbl.col FROM sch.tbl Concrete { table: "sch.tbl" } Concrete { schema: "sch", table: "tbl", column: "col" }
SELECT t.payload.items FROM t Concrete { table: "t.payload" } 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:

  1. try the longest leading run of components that names a binding — that is the relation, and the next component is the column;
  2. 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);
  3. 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).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions