Skip to content

Improve substrait NameTracker so it doesn't require uuids #17508

Description

@alamb

the following PR adds uuids to certain substrait identifiers to disambiguate them, but this may make the plans non reproducable. @Blizzara has some ideas how how we can avoid the UUIDs

FWIW, I looked a bit at what it'd take to fix the tracker. I think a core of the issue is that DF checks name ambiguity in two ways: there's the AmbiguousColumn exception you're running into, and then there is a validate_unique_names() function which gets called on the creation of the Project. The former needs unique non-qualified names, while the latter needs unique schema names (which can be qualified).

An easy fix for the former would be to change name_for_alias() into qualified_name()._1 here

match self.get_unique_name(expr.name_for_alias()?) {
. However, that then regresses the latter check (including in the test case for this PR), since there will then be a project node with an expr CAST(B.C as Utf8) with a qualified name ([no qualifier], "B.C") and a schema name "B.C", as well as a reference to the original column B.C with a qualified name ("B", "C") and also schema name "B.C". As the qualified name's name parts are different, it wouldn't be renamed (after the change I propose), and then it'd fail the validate_unique_names() check. So maybe for a proper fix, NameTracker would need to track both the schema name and the name-part of the qualified name, and rename until both are unique.

(A simple example of the behavior of the CAST and validate_unique_names() is that SELECT data.a, CAST(data.a as string) from data; also fails in datafusion-cli.)

Originally posted by @Blizzara in #17299 (comment)

Activity

  1. dd-annarose commented on Nov 20, 2025

    @dd-annarose
    Contributor

    do we want to make it possible for two columns in the output schema to have the same exact qualified name? Postgresql does allow this: SELECT data.a, CAST(data.a as string) from data; would yield 2 columns, both named data.a.

    This is not a great feature, and I understand forbidding it. But this PR aligns DF on the behavior of Posgres & spark

    I would also like to match the behavior of Postgres and Spark where CAST does not appear in the field names in the schema

    I think a first step towards a solution to this issue would either:

    • making CAST(B.C as Utf8) have qualified name ("B", "C") so that the name tracker can detect column B.C has an already seen name
    • OR making it so that unaliased CASTs do appear in the field name in the schema, something like B.C::TEXT
  2. LiaCastaneda commented on Nov 21, 2025

    @LiaCastaneda
    Contributor

    making CAST(B.C as Utf8) have qualified name ("B", "C")

    IIRC two columns with the same qualified name and same field name, might result in an schema error as well during logical planning (see here).

    OR making it so that unaliased CASTs do appear in the field name in the schema, something like B.C::TEXT

    if we had

    SELECT 
      CAST(B.C as TEXT),
      CAST(B.C as TEXT)
    FROM table
    

    could it still conflict?

  3. dd-annarose commented on Nov 24, 2025

    @dd-annarose
    Contributor
    1. IIUC the name tracker should go over it and we'd have B.C and B.C__temp__0, or am I not understanding properly? Is is a hard requirement, even before the substrait parsing? I'm not sure then why we'd accept a query like SELECT data.a, CAST(data.a as string) from data;
    SELECT B.C, CAST(B.C as TEXT) FROM table;
    

    we'd get columns "B.C" and "B.C"

    1. I think your example is a bit of a mix between my two proposals; I was thinking of making CASTs appear in the case where we still don't want duplicate col names, so something like
    SELECT B.C, CAST(B.C as TEXT) FROM table;
    

    we'd get columns "B.C" and "B.C::TEXT"

  4. LiaCastaneda commented on Nov 25, 2025

    @LiaCastaneda
    Contributor

    Sorry I think I misunderstood the issue, I graphed this to better picture this, please let me know if I'm understanding this wrong

    SELECT B.C, CAST(B.C as TEXT) FROM table; yields to the following structure

    
    ╔═══════════════════╦══════════════╦══════════════╦═══════════════╗
    ║   Expression      ║ Qualifier    ║ qualified_   ║ schema_name() ║
    ║                   ║ .0           ║ name().1     ║               ║
    ╠═══════════════════╬══════════════╬══════════════╬═══════════════╣
    ║ Column(B.C)       ║ Some("B")    ║ "C"          ║ "B.C"         ║
    ║                   ║ ↑            ║ ↑            ║ ↑             ║
    ║                   ║ Has table!   ║ Field only   ║ Combined      ║
    ╠═══════════════════╬══════════════╬══════════════╬═══════════════╣
    ║ CAST(B.C as Utf8) ║ None         ║ "B.C"        ║ "B.C"         ║
    ║                   ║ ↑            ║ ↑            ║ ↑             ║
    ║                   ║ No table!    ║ Full string  ║ Same string   ║
    ╚═══════════════════╩══════════════╩══════════════╩═══════════════╝
    

    The null literal situation from the issue description yields to the following structure (without the uuid workaround):

    ╔═══════════════════════════╦═══════════════╦═══════════════╦═══════════════════╗
    ║   Expression              ║ Qualifier     ║ qualified_    ║ schema_name()     ║
    ║                           ║ .0            ║ name().1      ║ (name_for_alias)  ║
    ╠═══════════════════════════╬═══════════════╬═══════════════╬═══════════════════╣
    ║ lit(NULL)                 ║ None          ║ "UTF8(NULL)"  ║ "UTF8(NULL)"      ║
    ║ (new literal in project)  ║               ║               ║                   ║
    ╠═══════════════════════════╬═══════════════╬═══════════════╬═══════════════════╣
    ║ Column(left.UTF8(NULL))   ║ Some("left")  ║ "UTF8(NULL)"  ║ "left.UTF8(NULL)" ║
    ║ (from input after join)   ║               ║               ║                   ║
    ╚═══════════════════════════╩═══════════════╩═══════════════╩═══════════════════╝
    

    The solutions proposed in the issue description -> using qualified_name().1 instead of name_for_alias() to detect when to rename would directly fix the null literal case wihtout needing to use uuids. But for the cast situation the name tracker would never trigger a rename since it sees a different qualified_name().1, and because the schema_name are the same they fail later in validate_unique_names

    iiuc we want to solve both situations while avoiding the uuid workaround.

    do we want to make it possible for two columns in the output schema to have the same exact qualified name?

    Unless I'm missing something, I don't see the harm in allowing it if Postgres already does it.

    making CAST(B.C as Utf8) have qualified name ("B", "C") so that the name tracker can detect column B.C has an already seen name

    You mean doing this for a fix where the name tracker tracks both schema_name and qualified_name().1 as the issue says? Do you know why right now the name tracker is not triggering a rename if both schema names are the same? (which iiuc is what name_for_alias returns)

    🤔 I think if you alias the cast right now together with the Literal :

        let maybe_apply_alias = match e {
            lit @ Expr::Literal(_, _) => lit.alias(uuid::Uuid::new_v4().to_string()),
            cast @Expr::Cast(_) => cast.alias(name),
            _ => e,
        };
    

    Even if this fixes the issue for the Cast, would it be possible that there are other expressins that fall under this same behaviour as well and would need aliasing also?

  5. dd-annarose commented on Nov 26, 2025

    @dd-annarose
    Contributor

    Do you know why right now the name tracker is not triggering a rename if both schema names are the same? (which iiuc is what name_for_alias returns)

    Right now, the name tracker does! I was unclear, sorry! I meant for the suggested solution from @alamb where we use qualified_name().1. Then, we'd need the cast to have the same qualified name as the column selection.

    I'm not sure the issue that the alias fixes? aliasing with the qualified name? As far as I could tell, casts seem to be the only edge case to address.

  6. dd-annarose commented on Dec 4, 2025

    @dd-annarose
    Contributor

    take

  7. added a commit that references this issue on Feb 24, 2026
    d59cdfe
  8. added a commit that references this issue on Feb 25, 2026
    2088ea3
  9. added a commit that references this issue on Feb 25, 2026
    0272667
  10. added a commit that references this issue on Feb 25, 2026
    4aa7071
  11. added a commit that references this issue on Feb 26, 2026
    7ed98ff
  12. added a commit that references this issue on Mar 31, 2026
    1a1fb91
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions