Subodh-17 opened a new pull request, #26376:
URL: https://github.com/apache/datafusion/pull/26376

   # Which issue does this PR close?
   
   Closes #26221.
   Closes #3353.
   
   ---
   
   # Rationale for this change
   
   When an unaliased SQL query aggregates the same column both with and without 
an explicit `CAST`—for example:
   ```sql
   SELECT sum(x), sum(CAST(x AS FLOAT)) FROM (VALUES (1.0), (2.0)) AS t(x);
   ```
   DataFusion previously failed during logical planning with a schema 
validation error:
   ```text
   SchemaError: DuplicateUnqualifiedField { name: "sum(CAST(t.x AS Float32))" }
   ```
   (or in earlier versions, stripping all casts during schema name generation 
and colliding on `"sum(t.x)"`).
   
   ### Root Cause
   1. **Implicit vs. Explicit Cast Display Ambiguity:**  
      During logical query analysis, DataFusion's type coercion framework 
automatically injects implicit casts (`Expr::Cast` / `Expr::TryCast`) to 
satisfy function signatures (e.g., `sum(x)` on `Int32` receives an implicit 
cast to `Int64` or `Float64`). However, `Cast` and `TryCast` previously stored 
only `expr: Box<Expr>` and `field: FieldRef`, with no provenance tracking 
whether the cast was explicitly written by the user or injected by the 
optimizer.
   2. **Schema Display Dilemma:**  
      When formatting schema names for function arguments (`SchemaDisplay`), if 
casts were globally preserved, implicit coercion casts appeared in output 
headers (e.g., `sum(CAST(t.x AS Int64))`), breaking display conventions and 
colliding with explicit casts to the same type. If casts were globally 
stripped, `sum(x)` and `sum(CAST(x AS FLOAT))` produced identical names 
(`sum(t.x)`), triggering duplicate field errors in `DFSchema::check_names()`.
   3. **Scalar UDF Optimizer Invariant Divergence:**  
      A naive fix that preserves casts across all comma-separated function 
argument formatting inadvertently altered `ScalarUDFImpl::schema_name` (e.g., 
`arrow_metadata(CAST(v1 AS DATE), 'k1')`). When `simplify_expressions` 
simplified redundant scalar casts and `optimize_projections` merged adjacent 
projections, intermediate unaliased expressions dropped the cast wrapper. The 
resulting schema name mismatch between pre- and post-optimization plans 
violated DataFusion's optimizer invariant assertion (`assert_expected_schema`).
   4. **Serialization Provenance Loss:**  
      Neither Protobuf (`CastNode`, `TryCastNode`) nor Substrait encodes 
whether a cast was implicit or explicit. Without provenance preservation and 
consumer reconciliation, distributed engines (e.g., Apache Ballista) and 
Substrait consumers would reconstruct all casts as explicit, causing schema 
discrepancies across round-trips.
   
   ---
   
   # What changes are included in this PR?
   
   ### 1. Cast Provenance Tracking (`datafusion-expr`)
   - Added `pub is_implicit: bool` to `struct Cast` and `struct TryCast`.
   - Added explicit constructors:
     - `Cast::new_implicit(expr, data_type)`
     - `Cast::new_implicit_from_field(expr, field)`
     - `TryCast::new_implicit(expr, data_type)`
     - `TryCast::new_implicit_from_field(expr, field)`
   - Preserved existing constructors (`Cast::new`, `Cast::new_from_field`, 
`TryCast::new`, `TryCast::new_from_field`) with `is_implicit = false` for 
explicit user casts.
   - Updated `type_coercion.rs` and `expr_schema.rs` (`Expr::cast_to`) to 
construct implicit casts using `new_implicit`.
   - Updated `tree_node.rs` and `order_by.rs` to propagate `is_implicit` during 
expression rewrites.
   
   ### 2. Semantic Equality, Ordering & Hashing Invariants
   - **Intentional Exclusion:** `PartialEq`, `Eq`, `PartialOrd`, and `Hash` on 
`Cast` and `TryCast` deliberately compare and hash solely `self.expr` and 
`self.field`, ignoring `self.is_implicit`.
   - **Rationale:** An explicit cast and an implicit cast targeting the same 
data type evaluate to mathematically and computationally identical operations. 
Omitting `is_implicit` from equality and hashing ensures that Common 
Subexpression Elimination (CSE), distinct grouping rules, and hash sets 
continue to deduplicate equivalent expressions without optimizer fragmentation 
or execution regressions. Physical execution planners 
(`datafusion-physical-expr`) similarly execute kernels identically regardless 
of provenance.
   
   ### 3. Decoupled Schema Naming for Aggregates vs. Scalars
   - **Aggregate Formatting (`UdafSchemaNameBuilder`):**  
     Introduced internal helper `schema_name_from_aggregate_args` with 
`preserve_casts: true`. When formatting aggregate argument schema names:
     - Explicit casts (`is_implicit = false`) are preserved: `sum(CAST(t.x AS 
Float64))`.
     - Implicit casts (`is_implicit = true`) are stripped: `sum(t.x)`.
     - Casts of NULL literals (e.g., `CAST(NULL AS VARCHAR)`) continue to 
display as `NULL`.
   - **Window Aggregate Formatting (`UdafWindowFunctionSchemaNameBuilder`):**  
     Uses `schema_name_from_function_args` with `preserve_casts: true`, 
ensuring window aggregate distinctions are also retained.
   - **Scalar UDF Formatting (`ScalarUDFImpl::schema_name`):**  
     Restored `schema_name_from_exprs_comma_separated_without_space` to 
`preserve_casts: false`. Scalar functions adhere to standard SQL display 
conventions (stripping casts from unaliased headers), ensuring schema names 
remain strictly invariant across algebraic simplifications in 
`simplify_expressions` and projection merges in `optimize_projections`.
   
   ### 4. Protobuf Wire Compatibility (`datafusion-proto` & 
`datafusion-proto-models`)
   - Added `optional bool is_implicit = 5;` to `CastNode` and `TryCastNode` in 
`datafusion.proto`.
   - Regenerated Rust prost bindings (`prost.rs`), Serde codecs (`pbjson.rs`), 
and common models (`datafusion_proto_common.rs`).
   - `to_proto.rs`: Serializes `is_implicit: Some(cast.is_implicit)`.
   - `from_proto.rs`: Deserializes `cast.is_implicit.unwrap_or(false)`. If tag 
5 is absent (e.g., from an older version), it safely defaults to `false`.
   
   ### 5. Substrait Roundtrip Reconciliation (`datafusion-substrait`)
   - In `consumer/utils.rs`, implemented `reconcile_cast_provenance`: when 
consumer schema names are renamed against `RelRoot.names`, if an explicit cast 
was authored whose target type coincided with the UDF coercion target (e.g., 
`avg(CAST(a AS DOUBLE))`), `reconcile_cast_provenance` checks whether restoring 
explicit cast provenance (`is_implicit = false`) yields an exact match with the 
target name, avoiding the injection of spurious projection aliases.
   - `consumer/expr/function_arguments.rs` and 
`consumer/expr/window_function.rs`: Added `mark_implicit_coercion_casts` to 
infer implicit coercion casts based on aggregate and window function signatures.
   
   ---
   
   # Backward Compatibility
   
   - **Protobuf Wire Compatibility:** Tag 5 is an `optional bool`. Older 
decoders encountering tag 5 will safely skip it as an unknown field.
   - **Legacy Protobuf Payloads:** When reading messages generated before this 
change, `is_implicit` resolves to `None` and safely defaults to `false`.
   - **Rust API Compatibility:** `Cast::new` and `Cast::new_from_field` retain 
their existing signatures and default to `is_implicit = false`.
   - **SQL / Optimizer Display Compatibility:** Top-level projection headers 
(e.g., `SELECT CAST(x AS INT)`) and scalar UDFs retain their existing display 
names (`x`). Only aggregate function outputs retain explicit cast qualifiers.
   
   ---
   
   # Are these changes tested?
   
   Yes. Targeted unit tests, integration tests, and SQL logic tests were 
executed and passed cleanly:
   
   1. **SQL Logic Tests (`datafusion-sqllogictest`):**
      - `test_files/aggregate.slt`: Verified unaliased `approx_distinct(c9), 
approx_distinct(cast(c9 as varchar))` (closing #3353) and added test queries 
for `sum(x), sum(CAST(x AS FLOAT))` and `count(x), count(CAST(x AS BIGINT))`.
      - `test_files/group_by.slt`: Updated explain plan outputs to reflect 
explicit casts in aggregate column headers.
      - `test_files/ordered_aggregate_spill.slt` & 
`test_files/parquet_statistics.slt`: Updated explain outputs where explicit 
casts in aggregate arguments are now properly preserved.
      - `test_files/cast_extension_type_metadata.slt`, `metadata.slt`, 
`datetime/timestamps.slt`, and `datetime/timestamps_timezone.slt`: Verified 
100% pass with zero optimizer invariant failures.
   2. **Substrait Integration Tests (`datafusion-substrait`):**
      - Added round-trip tests for aggregate and window expressions with 
implicit coercion casts, explicit casts matching coercion targets, explicit 
casts differing from coercion targets, and explicit `TRY_CAST`.
      - Full suite: **234 passed, 0 failed, 6 ignored**.
   3. **Protobuf Round-Trip Tests (`datafusion-proto`):**
      - Added `roundtrip_aggregate_colliding_risk_cast_plan` verifying 
multi-aggregate plans with colliding risks preserve schema names and pass 
`check_names()`.
      - Added `roundtrip_cast_try_cast_backward_compatibility` validating 
legacy payloads omitting tag 5.
      - Cast suite: **6 passed, 0 failed**.
   4. **SQL Integration Tests (`datafusion-sql`):**
      - Added `test_aggregate_with_and_without_cast_on_same_column`.
      - Full suite: **605 passed, 0 failed**.
   5. **Expression Unit Tests (`datafusion-expr`):**
      - Added `test_schema_name_top_level_cast` and 
`test_schema_name_function_args_cast`.
      - Added `test_udaf_schema_name_with_cast_distinction`.
      - All tests passed.
   6. **Code Hygiene:**
      - `cargo fmt --check`: Passed with exit code 0.
      - `git diff --check`: Passed with exit code 0.
   
   ---
   
   # Known Limitations
   
   - **Full Sqllogictest Suite Notice:**  
     When running the full `sqllogictests` suite (532 files) locally without 
cloning external git submodules, 523 files pass (98.3%) and 9 files fail 
(`describe.slt`, `create_external_table.slt`, `dynamic_file.slt`, 
`push_down_filter_regression.slt`, `predicates.slt`, `order.slt`, 
`parquet.slt`, `explain.slt`, `intersection.slt`). These 9 failures are 
environmental and caused by missing test data in uninitialized 
`parquet-testing` and `arrow-testing` git submodules (`No files found at 
.../parquet-testing/data/...`). They pass in CI where submodules are 
initialized.
   
   ---
   
   # Are there any user-facing changes?
   
   Yes. Unaliased SQL queries that aggregate the same column both with and 
without an explicit `CAST` will now succeed with distinct column headers 
instead of failing with a duplicate field error:
   ```sql
   SELECT sum(x), sum(CAST(x AS FLOAT)) FROM (VALUES (1.0), (2.0)) AS t(x);
   -- Output schema column names: "sum(t.x)", "sum(CAST(t.x AS Float32))"
   ```
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to