adriangb opened a new issue, #25669:
URL: https://github.com/apache/datafusion/issues/25669

   # Describe the bug
   
   A `UNION ALL` whose arms select an untyped placeholder under an alias 
(`SELECT $1 AS a UNION ALL SELECT $2 AS a`) cannot be analyzed or optimized 
before the placeholders are bound. The error names the alias as a missing 
column:
   
   ```
   Schema error: No field named a.
   ```
   
   This affects two public paths:
   
   1. `PREPARE p AS SELECT $1 AS a UNION ALL SELECT $2 AS a` fails in the 
optimizer rule `optimize_unions`.
   2. `SessionState::optimize` on the plan from 
`SessionState::create_logical_plan` (placeholders still unbound) fails in the 
analyzer rule `type_coercion`. Optimizing first and binding with 
`LogicalPlan::replace_params_with_values` afterwards is therefore impossible 
for this shape.
   
   Binding first (`DataFrame::with_param_values`, then execute) works. A single 
arm without `UNION` works. The same `UNION` without the alias (`SELECT $1 UNION 
ALL SELECT $2`) works. Declaring the placeholder type works: `CAST($1 AS 
VARCHAR) AS a`, or `PREPARE p(VARCHAR, VARCHAR) AS ...`.
   
   `EXPLAIN SELECT $1 AS a UNION ALL SELECT $2 AS a` shows the analyzer failure:
   
   ```
   | logical_plan after type_coercion             | Schema error: No field 
named a. |
   ```
   
   # To Reproduce
   
   ## datafusion-cli
   
   ```sql
   PREPARE p AS SELECT $1 AS a UNION ALL SELECT $2 AS a;
   ```
   
   Actual:
   
   ```
   Optimizer rule 'optimize_unions' failed
   caused by
   Schema error: No field named a.
   ```
   
   The same happens in a CTE that is joined to a table, which is the realistic 
shape (a small list of key pairs supplied as parameters):
   
   ```sql
   CREATE TABLE t (k1 VARCHAR, k2 VARCHAR) AS VALUES ('a0', 'b0'), ('a1', 
'b1'), ('a2', 'b2');
   
   PREPARE q AS
   WITH keys AS (
     SELECT $1 AS k1, $2 AS k2
     UNION ALL
     SELECT $3 AS k1, $4 AS k2
   )
   SELECT t.k1 FROM t JOIN keys ON t.k1 = keys.k1 AND t.k2 = keys.k2;
   ```
   
   ```
   Optimizer rule 'optimize_unions' failed
   caused by
   Schema error: No field named k1.
   ```
   
   ## Rust (optimize, then bind)
   
   ```rust
   use datafusion::common::{ParamValues, ScalarValue};
   use datafusion::error::Result;
   use datafusion::prelude::*;
   
   #[tokio::main]
   async fn main() -> Result<()> {
       let ctx = SessionContext::new();
       let state = ctx.state();
       let plan = state
           .create_logical_plan("SELECT $1 AS a UNION ALL SELECT $2 AS a")
           .await?;
   
       // Fails here:
       //   type_coercion
       //   caused by
       //   Schema error: No field named a.
       let optimized = state.optimize(&plan)?;
   
       let bound = optimized.replace_params_with_values(&ParamValues::List(vec![
           ScalarValue::from("x").into(),
           ScalarValue::from("y").into(),
       ]))?;
       let _ = state.optimize(&bound)?;
       Ok(())
   }
   ```
   
   `Debug` output of the error:
   
   ```
   Context("type_coercion", SchemaError(FieldNotFound { field: Column { 
relation: None, name: "a" }, valid_fields: [] }, Some("")))
   ```
   
   Named placeholders (`SELECT $x AS a UNION ALL SELECT $y AS a`) fail the same 
way.
   
   # Expected behavior
   
   The plan analyzes and optimizes with the placeholders unbound, as it does 
for the same query without `UNION` and for the same query with `CAST($1 AS 
VARCHAR)`. After binding, `SELECT $1 AS a UNION ALL SELECT $2 AS a` with `('x', 
'y')` returns the two rows `x` and `y`.
   
   # Additional context
   
   The unoptimized plan is:
   
   ```
   Union [a:Null;N]
     Projection: $1 AS a [a:Null;N]
       EmptyRelation: rows=1 []
     Projection: $2 AS a [a:Null;N]
       EmptyRelation: rows=1 []
   ```
   
   Both failing rules (`TypeCoercionRewriter::coerce_union` in the analyzer and 
`OptimizeUnions` in the optimizer) call `coerce_plan_expr_for_schema` on each 
`UNION` arm. For a `Projection` arm, that function calls 
`coerce_exprs_for_schema(expr, input.schema(), union_schema)`, which calls 
`expr.get_type(input.schema())` on each projection expression.
   
   `Expr::get_type` in `datafusion/expr/src/expr_schema.rs` has a special case 
for an alias of an untyped placeholder:
   
   ```rust
   Expr::Alias(Alias { expr, name, .. }) => match &**expr {
       Expr::Placeholder(Placeholder { field, .. }) => match &field {
           None => schema.data_type(&Column::from_name(name)).cloned(),
           Some(field) => Ok(field.data_type().clone()),
       },
       _ => expr.get_type(schema),
   },
   ```
   
   When the placeholder has no type, it looks up the alias name (`a`) as a 
column in the schema that it receives. Here that schema is the projection's 
input (`EmptyRelation`, no fields), so the lookup fails. The special case 
appears to assume that the schema is the projection's own output schema. It 
came from #4701 (prepared statement parameter type inference).
   
   `Expr::to_field` does not have this special case. For the same expression it 
returns a `Null` field, so `Projection::try_new` builds the schema `a:Null` 
without error. `get_type` and `to_field` therefore disagree for 
`Alias(Placeholder { field: None })`.
   
   Possible fixes:
   
   - In the `Alias(Placeholder { field: None })` branch of `get_type`, return 
`DataType::Null` (the same result as a bare untyped placeholder and as 
`to_field`) when the alias name is not in the schema, or remove the branch.
   - Or make `coerce_exprs_for_schema` resolve the current type of a projection 
expression from the projection's output schema (the field at the same index), 
not from the input schema.
   
   Related, but different:
   
   - #18102 and #18522: `PREPARE myplan AS SELECT $1 AS one, $2 AS two` failed 
with "No field named one" in an older release. That case now works. The `UNION` 
case still fails.
   - #8819: optimizing a `LogicalPlan` that contains placeholders must not fail.
   
   # Version
   
   - `apache/datafusion` `main` at commit 
`7570366fd929daf9ced744bb8397686b50565b18` (2026-09-23, workspace version 
55.1.0), both with `datafusion-cli` built from that commit and with the Rust 
program above
   - Also reproduced with released `datafusion-cli` 54.0.0
   


-- 
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