Repository navigation
fix: Set Substrait output_type on window functions and LIKE - #25367
Conversation
Substrait documents output_type on both ScalarFunction and WindowFunction as the return type of the function, and a consumer that reads it rejects a call that leaves it unset: substrait-java refuses a window function plan and a LIKE plan with "Type is not set". Both types are derived from the expression itself, so they match what DataFusion derives: from_window_function from Expr::WindowFunction, and make_substrait_like_expr from Expr::Like, which also covers the not() wrapper a negated LIKE emits. make_substrait_window_function had a single caller and its body now sits in from_window_function, and make_substrait_like_expr takes the Like rather than its five fields, so neither grows an extra parameter. A DataFusion round trip cannot catch this, because the consumer reads output_type only when it converts a cast.
dd81646 to
be39554
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #25367 +/- ##
==========================================
+ Coverage 82.33% 82.61% +0.27%
==========================================
Files 1137 1147 +10
Lines 432498 444837 +12339
Branches 432498 444837 +12339
==========================================
+ Hits 356116 367504 +11388
- Misses 54843 55001 +158
- Partials 21539 22332 +793 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
make_substrait_like_expr had one caller once it took the Like itself, so its body now sits in from_like, the same as the window function helper. Also restore the doc comment on to_substrait_unary_scalar_fn, which the previous commit dropped by accident.
|
Bumping this — still open for review whenever someone has bandwidth. |
kosiew
left a comment
There was a problem hiding this comment.
Thanks for working on this. The changes look good overall. I left one non-blocking suggestion to strengthen the regression coverage.
| /// Substrait requires the return type on a window function call, and a | ||
| /// consumer that reads it rejects the call when it is unset. | ||
| #[test] | ||
| fn window_function_output_type() -> datafusion::common::Result<()> { |
There was a problem hiding this comment.
Could you also add a COUNT(*) OVER () case, for example using count_all_window(), and assert that its output is required i64? The current test only covers nullable output, so this would catch a regression where output_type is always emitted as nullable.
Guards against a regression where output_type is always emitted as nullable, which the existing window_function_output_type test (over a nullable column) would not catch.
|
@kosiew added |
kosiew
left a comment
There was a problem hiding this comment.
Thanks for working on this. The changes correctly populate Substrait output types for window functions and LIKE expressions while preserving DataFusion's derived type and nullability. The added COUNT(*) window test also covers the important non-nullable case.
|
🚀 |
## Which issue does this PR close? - Closes apache#25368. ## Rationale for this change `functions_string.yaml` defines one function for this, with two arguments and an option: ```yaml name: like impls: - args: - value: "varchar<L1>" name: "input" - value: "varchar<L2>" name: "match" options: case_sensitivity: values: [ CASE_SENSITIVE, CASE_INSENSITIVE, CASE_INSENSITIVE_ASCII ] return: "boolean" ``` The producer emitted three arguments, registered `ilike` for the case insensitive form, and never set the option. Neither `ilike` nor an escape argument appears in any extension file, in the pinned crate or upstream. ## What changes are included in this PR? - `make_substrait_like_expr` registers `like`, emits the two arguments the definition takes, and sets `case_sensitivity` to `CASE_INSENSITIVE` for `ILIKE`. A plain `LIKE` sets no option, which leaves the default. - `LIKE ... ESCAPE` is now `not_impl_err`. There is no escape in the definition, so the third argument only produced a call a consumer cannot bind. - The consumer reads `case_sensitivity`, taking the first value it supports and refusing the ones it does not, as Substrait requires. It still accepts the `ilike` name and the three argument form, so plans written by an older DataFusion still load. ## What is the testing strategy for this PR? - New producer tests: `LIKE` emits `like` with two arguments and no option, `ILIKE` emits the `case_sensitivity` option, and an escape character is rejected. - `roundtrip_like` and `roundtrip_ilike` still pass, which is what covers the consumer: with the option reading removed, `roundtrip_ilike` fails because `ILIKE` comes back as `LIKE`. Removing the producer half likewise fails the new producer test. - `cargo test -p datafusion-substrait` passes and `./ci/scripts/rust_clippy.sh` is clean. Measured against substrait-java 0.103.0 and Spark 3.5.4, over `t(s)` holding `abc` and `ABC`, with the apache#11545 URN patched and `output_type` filled (apache#25049): | plan | before | after | | --- | --- | --- | | `s LIKE 'a%'` | `AnalysisException: WRONG_NUM_ARGS ... requires 2 parameters but the actual number is 3` | runs, returns `abc` | | `s ILIKE 'a%'` | `IllegalArgumentException: Unexpected scalar function with key ilike:str_str` | runs, returns `abc` | **The second row is a trade-off worth stating.** `ILIKE` used to fail loudly there, and now it is accepted but answered case sensitively, because substrait-spark does not read the option: `case_sensitivity` appears nowhere in its sources. Substrait says a consumer that does not recognise an option must reject the call, so that looks like a gap on that side, and I am happy to report it there. If you would rather not emit something a known consumer mishandles, the alternative is to reject `ILIKE` as well until a consumer honours the option, and I will change it. ## Are there any user-facing changes? `LIKE` and `ILIKE` are emitted in the form the extension defines, so consumers that bind arguments by the declaration can now read them. `LIKE ... ESCAPE` is rejected instead of being emitted in a shape no consumer can bind; DataFusion round trips it today only because its own consumer reads the same private convention. This touches `make_substrait_like_expr`, which apache#25367 folds into `from_like`, so whichever lands second needs a small rebase. --------- Co-authored-by: namanjain24-sudo <180642416+namanjain24-sudo@users.noreply.github.com>
Which issue does this PR close?
Rationale for this change
The producer left
output_typeunset on window function calls and onLIKE/ILIKE, including thenotthat wraps a negated one. Substrait documents bothExpression.WindowFunction.output_typeandExpression.ScalarFunction.output_typeas:A consumer that reads the field rejects such a call. substrait-java 0.103.0 refuses both plans, from
ProtoTypeConverter.from:125:SELECT sum(i) OVER (ORDER BY i) FROM tUnsupportedOperationException: Type is not setatProtoExpressionConverter.fromWindowFunction:406SELECT i FROM t WHERE CAST(i AS VARCHAR) LIKE '1%'UnsupportedOperationException: Type is not setatProtoExpressionConverter:201, viaProtoRelConverter.newFilterA DataFusion round trip cannot catch this: the consumer reads
output_typeonly inconsumer/expr/cast.rs, so producer and consumer agree on the omission.#15831 and #20597 set this field for binary and unary expressions,
from_functionand higher order functions, and #25049 and #25090 coverAggregateFunction. These were the remaining expression kinds.What changes are included in this PR?
Both types come from the expression itself, so they match what DataFusion derives rather than being restated in the producer:
from_window_functionderives the type fromExpr::WindowFunction(..).to_field(schema)and writes it on the call it builds.make_substrait_window_functionhad one caller and eight parameters once the type was added, so its body now sits infrom_window_functionand the helper is gone.from_likederives the type fromExpr::Like(..).to_field(schema)and sets it on thelikecall and on thenotwrapper of a negated one, which yields that same type.make_substrait_like_exprhadfrom_likeas its only caller, so, as with the window helper, its body now sits infrom_like.What is the testing strategy for this PR?
binary_expr_output_type:window_function_output_typeassertssum(i)over a nullablei64carries a nullablei64, andlike_output_typeasserts aLIKEover a nullable input carries a nullable boolean, on thelikecall, on thenotof a negated one, and on thelikenested inside thatnot.window_function_output_typefails, without theLIKEchange onlylike_output_typefails.cargo test -p datafusion-substraitpasses (60 unit, 210 integration with the 6 that were already ignored, 3 doc tests), and./ci/scripts/rust_clippy.sh, the workspace clippy CI runs, is clean.sumoutput_typeunsetlikenotaround a negatedlikeType is not set. Both now get throughProtoPlanConverter, while the plans built frommainstill fail there. Each then stops further along for reasons that have nothing to do with this field: substrait-spark has no visitor for a window function that appears in a project expression, and Spark'sliketakes two arguments while the call we emit passes three, the third being the escape character. That second one is fix: Emit LIKE the way the Substrait extension defines it #25442, which emitsLIKEin the two-argument form the extension defines.Are there any user-facing changes?
No API changes. Plans produced for window functions and
LIKEnow carry the expression's type, which consumers that require it will accept.