Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support duplicate column aliases in queries #13489

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

findepi
Copy link
Member

@findepi findepi commented Nov 19, 2024

Which issue does this PR close?

Rationale for this change

In SQL, selecting single column multiple times is legal and most modern
databases support this. This commit adds such support to DataFusion too.

What changes are included in this PR?

Are these changes tested?

yes

Are there any user-facing changes?

yes, more valid queries are supported

@github-actions github-actions bot added sql SQL Planner logical-expr Logical plan and expressions optimizer Optimizer rules core Core DataFusion crate sqllogictest SQL Logic Tests (.slt) common Related to common crate proto Related to proto crate labels Nov 19, 2024
@findepi findepi force-pushed the findepi/support-duplicate-column-aliases-0bf339 branch 2 times, most recently from 7d63cee to bd2307e Compare November 19, 2024 14:03
@jonahgao
Copy link
Member

allow creation of schemas for duplicated names

I think it's a good idea, as it allows the schema of the top plan to include duplicate names, thereby resolving #6543.

We can delay the name ambiguity check until a real column reference occurs. But currently, it seems that this check is not sufficient. For example

DataFusion CLI v43.0.0
> select t.a from (select 1 as a, 2 as a) t;
+---+
| a |
+---+
| 1 |
+---+
1 row(s) fetched.

This query did not return an error like it does in PostgreSQL and before. Perhaps we should improve ambiguity check when searching for field names from schemas after removing check_names.

/// A struct with same fields as [`CreateExternalTable`] struct so that the DDL can be conveniently
/// destructed with validation that each field is handled, while still requiring that all
/// construction goes through the [`CreateExternalTable::new`] constructor or the builder.
pub struct CreateExternalTableFields {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think non_exhaustive discourages destructuring, but CreateExternalTableFields makes it possible again. CreateExternalTableFields and CreateExternalTable have the same fields, and I'm a bit worried that it introduces some code duplication 🤔.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it does introduce code duplication and so does the builder.
When handling the Create Table/View, deconstruction is valuable without .., as it guarantees that any new field will force the code to be revisited (rather than new fields being ignored).

However, in Rust deconstruction without .. is possible only when construction is possible, and direct construction being possible precludes construction-time checks, which is undesirable.

Alternatively to this, we could allow construction of ill-formed Create Table/View objects, and have check somewhere else (plan validator), but i would be worried that such a delayed check could be missed in some code flows. The field duplication isn't a problem from maintainability perspective after all.

In SQL, selecting single column multiple times is legal and most modern
databases support this. This commit adds such support to DataFusion too.
@findepi
Copy link
Member Author

findepi commented Nov 20, 2024

We can delay the name ambiguity check until a real column reference occurs. But currently, it seems that this check is not sufficient. For example

DataFusion CLI v43.0.0
> select t.a from (select 1 as a, 2 as a) t;

Good catch. This is easy to solve.

The less easy part is that

select * from (select 1 as a, 2 as a) t;

should work. However, the * gets expanded to Expr expressions and these expressions have no way to differentiate between the two columns from a. This is because schema is used for both initial query analysis as well as in logical plans. Relates to #1468.

@findepi findepi force-pushed the findepi/support-duplicate-column-aliases-0bf339 branch from bd2307e to e7e778c Compare November 20, 2024 14:20
@findepi findepi marked this pull request as draft November 20, 2024 14:20
@jonahgao
Copy link
Member

The less easy part is that

select * from (select 1 as a, 2 as a) t;

should work. However, the * gets expanded to Expr expressions and these expressions have no way to differentiate between the two columns from a. This is because schema is used for both initial query analysis as well as in logical plans. Relates to #1468.

We might need to introduce column index to differentiate them.
Since this case was not previously supported either, maybe we can handle it later.

@findepi
Copy link
Member Author

findepi commented Nov 22, 2024

We might need to introduce column index to differentiate them.

This works and is definitely easiest to weave into current code, but IMO is very slippery slope.
On the last DF contributor call @alamb mentioned many hours spent debugging why column ordinals are incorrect when working on PostgreSQL internals.
Being cautious of engineers' sanity I would strongly prefer globally unique symbols, because then errors are possible to catch.

@alamb
Copy link
Contributor

alamb commented Nov 23, 2024

On the last DF contributor call @alamb mentioned many hours spent debugging why column ordinals are incorrect when working on PostgreSQL internals.
Being cautious of engineers' sanity I would strongly prefer globally unique symbols, because then errors are possible to catch.

Yes, specifically when I was working on postgres internals ~15 years ago, all column references were effectively offsets of the input schema and I remember spending lots of time debugging when the offsets weren't right -- it was hard to keep track of what the offsets were supposed to be

That being said, DataFusion PhysicalExpr columns use ordinal offsets and they don't seem to have generated too many debugging headaches

@findepi
Copy link
Member Author

findepi commented Nov 23, 2024

That being said, DataFusion PhysicalExpr columns use ordinal offsets and they don't seem to have generated too many debugging headaches

That may be because we prune columns on the LP level already?

@alamb
Copy link
Contributor

alamb commented Nov 23, 2024

That being said, DataFusion PhysicalExpr columns use ordinal offsets and they don't seem to have generated too many debugging headaches

That may be because we prune columns on the LP level already?

That is likely, though there is projection pushdown in the physical optimizer too: https://docs.rs/datafusion/latest/datafusion/physical_optimizer/projection_pushdown/index.html 🤔

@findepi
Copy link
Member Author

findepi commented Nov 23, 2024

This is anecdote-based, so i will add mine. In Trino the planner uses symbols (single per-query global namespace). Bugs happen and these are caught by ValidateDependenciesChecker (example failure trinodb/trino#22806). With ordinal-based, every such bug would be incorrect but potentially executable plan, so instead of clear error, it could produce incorrect results. Why would we prefer that?

@findepi
Copy link
Member Author

findepi commented Nov 25, 2024

PhysicalExpr columns use ordinal offsets and they don't seem to have generated too many debugging headaches

a gin has been invoked - #13559

@alamb
Copy link
Contributor

alamb commented Nov 25, 2024

PhysicalExpr columns use ordinal offsets and they don't seem to have generated too many debugging headaches

a gin has been invoked - #13559

🤣 😭

@alamb
Copy link
Contributor

alamb commented Nov 25, 2024

Bugs happen and these are caught by ValidateDependenciesChecker (example failure trinodb/trino#22806).

That error message is 😍

Suppressed: java.lang.Exception: Current plan:
                Output[columnNames = [a, _col1, _col2, _col3, _col4, _col5, _col6]]
                │   Layout: [field:integer, sum:bigint, sum_15:bigint, sum_16:bigint, sum_17:bigint, sum_18:bigint, sum_19:bigint]
                │   a := field
                │   _col1 := sum
                │   _col2 := sum_15
                │   _col3 := sum_16
                │   _col4 := sum_17
                │   _col5 := sum_18
                │   _col6 := sum_19
                └─ Aggregate[keys = [field]]
                   │   Layout: [field:integer, sum:bigint, sum_15:bigint, sum_16:bigint, sum_17:bigint, sum_18:bigint, sum_19:bigint]
                   │   sum := sum(sum_28)
                   │   sum_15 := sum(sum_29)
                   │   sum_16 := sum(sum_30)
                   │   sum_17 := sum(sum_31)
                   │   sum_18 := sum(sum_32)
                   │   sum_19 := sum(sum_33)
                   └─ Project[]
                      │   Layout: [field:integer, sum_28:bigint, sum_29:bigint, sum_30:bigint, sum_31:bigint, sum_32:bigint, sum_33:bigint]
                      │   sum_28 := (CASE WHEN (field_0 = integer '1') THEN sum_21 ELSE bigint '0' END)
                      │   sum_29 := (CASE WHEN (field_0 = integer '2') THEN sum_23 ELSE CAST(field_1 AS bigint) END)
                      │   sum_30 := (CASE WHEN (field_0 = integer '3') THEN sum_23 ELSE CAST(field_2 AS bigint) END)
                      │   sum_31 := (CASE WHEN (field_0 = integer '4') THEN sum_25 ELSE bigint '0' END)
                      │   sum_32 := (CASE WHEN (field_0 = integer '5') THEN sum_27 ELSE bigint '0' END)
                      │   sum_33 := (CASE WHEN (field_0 = integer '6') THEN sum_23 ELSE CAST(field_3 AS bigint) END)
                      └─ Aggregate[type = FINAL, keys = [field_0, field]]
                         │   Layout: [field_0:integer, field:integer, sum_21:bigint, sum_23:bigint, sum_25:bigint, sum_27:bigint]
                         │   sum_21 := sum(sum_34)
                         │   sum_23 := sum(sum_35)
                         │   sum_25 := sum(sum_36)
                         │   sum_27 := sum(sum_37)
                         └─ LocalExchange[partitioning = HASH, arguments = [field::integer]]
                            │   Layout: [field_0:integer, field:integer, sum_34:bigint, sum_35:bigint, sum_36:bigint, sum_37:bigint]
                            └─ Aggregate[type = PARTIAL, keys = [field_0, field]]
                               │   Layout: [field_0:integer, field:integer, sum_34:bigint, sum_35:bigint, sum_36:bigint, sum_37:bigint]
                               │   sum_34 := sum(expr_20)
                               │   sum_35 := sum(expr_22)
                               │   sum_36 := sum(expr_24)
                               │   sum_37 := sum(expr_26)
                               └─ Values[]
                                      Layout: [field_0:integer, field:integer, expr_20:bigint, expr_22:bigint, expr_24:bigint, expr_26:bigint]
                                      (integer '1', integer '1', bigint '1', bigint '0', bigint '1', bigint '1')
                                      (integer '2', integer '2', bigint '2', bigint '0', bigint '2', bigint '2')
                                      (integer '3', integer '3', bigint '3', bigint '0', bigint '3', bigint '3')

		at io.trino.sql.planner.sanity.PlanSanityChecker.validate(PlanSanityChecker.java:120)
		... 17 more

With ordinal-based, every such bug would be incorrect but potentially executable plan, so instead of clear error, it could produce incorrect results. Why would we prefer that?

I agree -- an error vs incorrect results sounds like the right tradeoff to me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
common Related to common crate core Core DataFusion crate logical-expr Logical plan and expressions optimizer Optimizer rules proto Related to proto crate sql SQL Planner sqllogictest SQL Logic Tests (.slt) substrait
Projects
None yet
3 participants