You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Substrait's IfThen has no notion of a base expression. Every IfClause is a
standalone boolean condition, and then is the value that clause yields:
messageIfThen {
// A list of one or more IfClausesrepeatedIfClauseifs=1;
// The returned Expression if no IfClauses are satisifiedExpressionelse=2;
messageIfClause {
Expressionif=1;
Expressionthen=2;
}
}
The producer nevertheless uses IfThen for CASE <base> WHEN <value> THEN ...,
encoding the base expression as a leading IfClause whose then is left unset
(from_case):
The following clauses then carry the raw WHEN operand in if. Neither the
leading clause nor the WHEN operands are boolean, so the emitted IfThen is not
something a spec-conforming consumer can evaluate.
That convention is private to DataFusion. The consumer decodes it back
(from_if_then):
// If the first element does not have a `then` part, then we can assume it's a base expression
so a DataFusion-to-DataFusion round trip is unaffected and no existing test
fails.
Another implementation reading the same plan does not get that far. substrait-java
(io.substrait:core:0.103.0) rejects it outright, because ProtoExpressionConverter
converts each clause's then unconditionally and the leading clause has none:
java.lang.IllegalArgumentException: Unknown type: REXTYPE_NOT_SET
at io.substrait.expression.proto.ProtoExpressionConverter.from(ProtoExpressionConverter.java:364)
at io.substrait.expression.proto.ProtoExpressionConverter.lambda$from$1(ProtoExpressionConverter.java:212)
Line 212 is ExpressionCreator.ifThenClause(from(t.getIf()), from(t.getThen())).
No extension URNs are involved: this plan registers no functions at all, so the
failure is on the encoding itself rather than on #11545.
The non-boolean conditions are a second, quieter problem. substrait-java does not
type-check IfClause.if, so removing the leading clause and feeding it the two i64 conditions is accepted. That half is a silent divergence from the spec
rather than an error, which is why no round trip anywhere surfaces it.
To Reproduce
SELECT CASE a WHEN 1 THEN 'x' WHEN 2 THEN 'y' ELSE 'z' END FROM data
Save this as datafusion/substrait/examples/case_base_probe.rs. It produces the
plan and prints the IfThen as it appears in the protobuf, without going through
the consumer:
use datafusion::common::Result;use datafusion::prelude::*;use datafusion_substrait::logical_plan::producer::to_substrait_plan;use prost::Message;use substrait::proto::expression::RexType;use substrait::proto::{plan_rel, rel};#[tokio::main(flavor = "current_thread")]asyncfnmain() -> Result<()>{let ctx = SessionContext::new();
ctx.sql("CREATE TABLE data(a BIGINT) AS VALUES (1), (2), (3)").await?
.collect().await?;let plan = ctx
.sql("SELECT CASE a WHEN 1 THEN 'x' WHEN 2 THEN 'y' ELSE 'z' END FROM data").await?
.into_optimized_plan()?;let proto = to_substrait_plan(&plan,&ctx.state())?;
std::fs::write("case.pb", proto.encode_to_vec())?;letSome(plan_rel::RelType::Root(root)) = proto.relations[0].rel_type.as_ref()else{panic!("expected Root")};letSome(rel::RelType::Project(project)) =
root.input.as_ref().unwrap().rel_type.as_ref()else{panic!("expected Project")};for expr in&project.expressions{letSome(RexType::IfThen(if_then)) = expr.rex_type.as_ref()else{continue;};println!("IfThen with {} clauses, else present: {}",
if_then.ifs.len(), if_then.r#else.is_some());for(i, clause)in if_then.ifs.iter().enumerate(){let kind = match clause.r#if.as_ref().unwrap().rex_type.as_ref().unwrap(){RexType::Literal(l) => format!("Literal({:?})", l.literal_type.as_ref().unwrap()),RexType::Selection(_) => "Selection(field reference)".to_string(),RexType::ScalarFunction(f) => {format!("ScalarFunction(anchor {}, output_type {:?})",
f.function_reference,
f.output_type.as_ref().map(|t| t.kind.as_ref().unwrap()))}
other => format!("{other:?}"),};println!(" ifs[{i}]: if = {kind}");println!(" then set: {}", clause.then.is_some());}}Ok(())}
$ cargo run --locked -p datafusion-substrait --example case_base_probeIfThen with 3 clauses, else present: true ifs[0]: if = Selection(field reference) then set: false ifs[1]: if = Literal(I64(1)) then set: true ifs[2]: if = Literal(I64(2)) then set: true
Three clauses, with conditions a, 1 and 2. All three are i64, and the
first has no result at all.
The case.pb the probe writes is what substrait-java rejects. To see that
directly, on a JDK with no build tool:
With io.substrait:core:0.103.0 and its runtime dependencies on the classpath:
$ javac -cp "lib/*" -d . CaseProbe.java && java -cp "lib/*:." CaseProbejava.lang.IllegalArgumentException: Unknown type: REXTYPE_NOT_SET at io.substrait.expression.proto.ProtoExpressionConverter.from(ProtoExpressionConverter.java:364) at io.substrait.expression.proto.ProtoExpressionConverter.lambda$from$1(ProtoExpressionConverter.java:212)
For comparison, the searched form CASE WHEN a = 1 THEN 'x' ELSE 'z' END is
emitted correctly, as a single clause whose condition is an equal call with output_type boolean.
Expected behavior
Every emitted IfClause should have a boolean condition and a then value.
Substrait has no switch-style construct that fits this case in general: SwitchExpression exists, but its IfValue.if is a Literal, so it cannot
express CASE a WHEN b + 1 THEN ..., and DataFusion's own consumer currently
rejects it with not_impl_err!("Switch expression not supported"). The general
translation is therefore to desugar the base expression, emitting <base> = <value> as each clause condition:
ifs[0]: if = equal(a, 1), then = 'x'
ifs[1]: if = equal(a, 2), then = 'y'
else : 'z'
DataFusion matches a base expression with = semantics
(compare_with_eq
uses Arrow's eq), so this preserves the plan's meaning, including a NULL WHEN
operand never matching. The producer already applies exactly this kind of
desugaring to BETWEEN in from_between.
A base CASE would then round trip as the equivalent searched CASE, keeping
its projection name and schema.
Additional context
Distinct from the round-trip failures collected under [Epic] A collection of Substrait conversion issues #16248: this round trip
succeeds. The problem is only visible by reading the emitted protobuf, because
producer and consumer share the private convention.
Adding SwitchExpression support, on both sides, would preserve the base-CASE
structure for the subset where every WHEN operand is a literal. That is worth
doing separately; it does not cover the general case, and the emitted plan
should be valid regardless.
Verified against the algebra.proto shipped in the pinned substrait 0.63.0
crate.
Describe the bug
Substrait's
IfThenhas no notion of a base expression. EveryIfClauseis astandalone boolean condition, and
thenis the value that clause yields:The producer nevertheless uses
IfThenforCASE <base> WHEN <value> THEN ...,encoding the base expression as a leading
IfClausewhosethenis left unset(
from_case):The following clauses then carry the raw WHEN operand in
if. Neither theleading clause nor the WHEN operands are boolean, so the emitted
IfThenis notsomething a spec-conforming consumer can evaluate.
That convention is private to DataFusion. The consumer decodes it back
(
from_if_then):// If the first element does not have a `then` part, then we can assume it's a base expressionso a DataFusion-to-DataFusion round trip is unaffected and no existing test
fails.
Another implementation reading the same plan does not get that far. substrait-java
(
io.substrait:core:0.103.0) rejects it outright, becauseProtoExpressionConverterconverts each clause's
thenunconditionally and the leading clause has none:Line 212 is
ExpressionCreator.ifThenClause(from(t.getIf()), from(t.getThen())).No extension URNs are involved: this plan registers no functions at all, so the
failure is on the encoding itself rather than on #11545.
The non-boolean conditions are a second, quieter problem. substrait-java does not
type-check
IfClause.if, so removing the leading clause and feeding it the twoi64conditions is accepted. That half is a silent divergence from the specrather than an error, which is why no round trip anywhere surfaces it.
To Reproduce
Save this as
datafusion/substrait/examples/case_base_probe.rs. It produces theplan and prints the
IfThenas it appears in the protobuf, without going throughthe consumer:
Three clauses, with conditions
a,1and2. All three arei64, and thefirst has no result at all.
The
case.pbthe probe writes is what substrait-java rejects. To see thatdirectly, on a JDK with no build tool:
With
io.substrait:core:0.103.0and its runtime dependencies on the classpath:For comparison, the searched form
CASE WHEN a = 1 THEN 'x' ELSE 'z' ENDisemitted correctly, as a single clause whose condition is an
equalcall withoutput_typeboolean.Expected behavior
Every emitted
IfClauseshould have a boolean condition and athenvalue.Substrait has no switch-style construct that fits this case in general:
SwitchExpressionexists, but itsIfValue.ifis aLiteral, so it cannotexpress
CASE a WHEN b + 1 THEN ..., and DataFusion's own consumer currentlyrejects it with
not_impl_err!("Switch expression not supported"). The generaltranslation is therefore to desugar the base expression, emitting
<base> = <value>as each clause condition:DataFusion matches a base expression with
=semantics(
compare_with_equses Arrow's
eq), so this preserves the plan's meaning, including a NULL WHENoperand never matching. The producer already applies exactly this kind of
desugaring to
BETWEENinfrom_between.A base
CASEwould then round trip as the equivalent searchedCASE, keepingits projection name and schema.
Additional context
succeeds. The problem is only visible by reading the emitted protobuf, because
producer and consumer share the private convention.
phaseleft atAGGREGATION_PHASE_UNSPECIFIED): aproducer field whose value is wrong by the spec but invisible to DataFusion's
own consumer.
SwitchExpressionsupport, on both sides, would preserve the base-CASEstructure for the subset where every WHEN operand is a literal. That is worth
doing separately; it does not cover the general case, and the emitted plan
should be valid regardless.
algebra.protoshipped in the pinnedsubstrait0.63.0crate.