Skip to content

Unparser with PostgreSqlDialect fails after Optimization Pass #21066

Open

Description

@cetra3

Describe the bug

The Unparser has a bug in the way that it treats certain PostgreSQL formatted queries, after optimization, which seems to be because requires_derived_table_alias is true:

I.e, optimizing then unparsing this query with postgresql dialect breaks:

WITH base AS (SELECT name, salary FROM t)
SELECT name,
    CASE WHEN SUM(salary) > 0 THEN 1 ELSE 0 END AS x,
    CASE WHEN SUM(salary) > 0 THEN SUM(salary) ELSE 0 END AS y
FROM base GROUP BY name

To Reproduce

Here's a test we can add somewhere that fails:

#[tokio::test]
async fn test_cse_derived_projection_roundtrip() {
    let ctx = SessionContext::new();
    ctx.sql("CREATE TABLE t (name TEXT, salary DOUBLE) AS VALUES ('a', 1.0), ('b', 2.0)")
        .await.unwrap().collect().await.unwrap();

    let sql = "\
        WITH base AS (SELECT name, salary FROM t) \
        SELECT name, \
            CASE WHEN SUM(salary) > 0 THEN 1 ELSE 0 END AS x, \
            CASE WHEN SUM(salary) > 0 THEN SUM(salary) ELSE 0 END AS y \
        FROM base GROUP BY name";

    let df = ctx.sql(sql).await.unwrap();
    let optimized = ctx.state().optimize(df.logical_plan()).unwrap();
    let unparser = Unparser::new(&datafusion_sql::unparser::dialect::PostgreSqlDialect {});
    let unparsed = unparser.plan_to_sql(&optimized).unwrap().to_string();

    ctx.sql(&unparsed).await.unwrap_or_else(|e| {
        panic!("Roundtrip failed.\n\nOriginal: {sql}\nUnparsed: {unparsed}\nError: {e}")
    });
}

This fails with the following error message on main:

Roundtrip failed.

Original: WITH base AS (SELECT name, salary FROM t) SELECT name, CASE WHEN SUM(salary) > 0 THEN 1 ELSE 0 END AS x, CASE WHEN SUM(salary) > 0 THEN SUM(salary) ELSE 0 END AS y FROM base GROUP BY name
Unparsed: SELECT "base"."name", CASE WHEN "__common_expr_1" THEN 1 ELSE 0 END AS "x", CASE WHEN "__common_expr_1" THEN "sum(base.salary)" ELSE 0.0 END AS "y" FROM (SELECT (sum("base"."salary") > 0.0) AS "__common_expr_1", "base"."name", sum("base"."salary") FROM "t" AS "base" GROUP BY "base"."name") AS "derived_projection"
Error: Schema error: No field named base.name. Valid fields are derived_projection.__common_expr_1, derived_projection.name, derived_projection."sum(base.salary)".

Expected behavior

This should parse correctly

Additional context

No response

Activity

  1. added theissue type on Mar 20, 2026
  2. adriangb commented on Mar 21, 2026

    @adriangb
    Contributor

    @cetra3 would you mind including the error that you get when running this example? It's useful to be able to cross reference against other issues.

  3. cetra3 commented on Mar 22, 2026

    @cetra3
    ContributorAuthor

    Done!

  4. amitvijapur commented on Sep 22, 2026

    @amitvijapur
    Contributor

    I ran the test from the description against main at ee8344c today. The No field named base.name error is gone: the outer projection now qualifies name with derived_projection, since rebase_derived_input_expr in unparser/plan.rs rewrites the outer expressions when the dialect requires a derived table alias, and #25521 (merged today) makes the inner select emit sum("base"."salary") AS "sum(base.salary)" so that reference resolves too.

    The round trip still fails, but on a different check:

    Error during planning: __common_expr_1 is a reserved DataFusion column name, please use another name
    

    check_plan in sql/src/statement.rs rejects any field whose name starts with __common_expr, so the SQL that the unparser produces for an optimized plan cannot be planned again by DataFusion. The emitted SQL itself is fine: after a textual rename of __common_expr_ the query plans and runs, for this query and for CSE shapes without a CTE, under both PostgreSqlDialect and DefaultDialect.

    So the qualifier bug this issue is about is fixed, and what remains is a choice between renaming reserved aliases in the unparser before emission or narrowing check_plan to the top-level schema, which reopens the collision the guard was added for (test_reserved_column_name* in sql_integration.rs pins the current behaviour). That seems worth a maintainer's call before anyone writes it, and probably a separate issue, since it is not specific to PostgreSQL.

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

    bugSomething isn't working

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions