fix: report a non-nullable return field for named_struct() and struct() - #25306
Merged
Merged
Conversation
Both constructors build their output `StructArray` without a null buffer, so the struct row they produce is never NULL, but they reported a nullable return field. Because `ExprSchemable::nullable` reads that field for a scalar function, the simplifier could not fold `IS NOT NULL` on them to `true`. A guard such as `WHERE s IS NOT NULL` on a struct built by a view therefore kept the whole struct expression alive, and projection pushdown could not prune the scan to the fields that are actually read. With the return field marked non-nullable the guard folds away and the scan reads only the accessed column.
Contributor
Author
|
@kosiew would you mind taking a look at this change please? |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #25306 +/- ##
==========================================
- Coverage 81.91% 81.91% -0.01%
==========================================
Files 1134 1134
Lines 425655 425663 +8
Branches 425655 425663 +8
==========================================
- Hits 348688 348665 -23
- Misses 56288 56314 +26
- Partials 20679 20684 +5 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Jefffrey
approved these changes
Sep 15, 2026
Contributor
Author
|
thanks @Jefffrey ! |
adriangb
added a commit
to pydantic/datafusion
that referenced
this pull request
Sep 15, 2026
strawgate
pushed a commit
to pydantic/datafusion
that referenced
this pull request
Sep 16, 2026
…() (apache#25306) - Closes apache#25305. `named_struct(...)` and `struct(...)` never produce a NULL row: `invoke_with_args` builds the output `StructArray` with no null buffer. Both functions nevertheless reported a nullable return field, and `ExprSchemable::nullable` for a scalar function reads that field. As a result the simplifier rule that rewrites `a IS NOT NULL` to `true` for a non-nullable `a` never fired on a struct constructor. The user-visible cost is lost pruning. A guard such as `WHERE s IS NOT NULL` on a struct built by a view cannot remove any row, but it keeps the whole struct expression alive, so projection pushdown can no longer prune the scan to the fields that are read: ```sql SET datafusion.explain.format = 'indent'; CREATE TABLE t (a INT, b INT, c INT) AS VALUES (1, 2, 3), (NULL, 5, 6); CREATE VIEW v AS SELECT named_struct('a', a, 'b', b, 'c', c) AS s FROM t; EXPLAIN SELECT s['b'] FROM v WHERE s IS NOT NULL; ``` Before: ``` logical_plan Projection: __datafusion_extracted_1 AS v.s[b] SubqueryAlias: v Projection: __datafusion_extracted_1 Filter: named_struct(Utf8("a"), t.a, Utf8("b"), t.b, Utf8("c"), t.c) IS NOT NULL Projection: t.b AS __datafusion_extracted_1, t.a, t.b, t.c TableScan: t projection=[a, b, c] ``` After: ``` logical_plan Projection: __datafusion_extracted_1 AS v.s[b] SubqueryAlias: v Projection: t.b AS __datafusion_extracted_1 TableScan: t projection=[b] ``` This matches the plan the same query already gets without the guard, and lets the existing `get_field(named_struct(...), 'f')` simplification (apache#22239) remove the constructor. - `datafusion/functions/src/core/named_struct.rs`: `return_field_from_args` now reports a non-nullable field instead of a nullable one. - `datafusion/functions/src/core/struct.rs`: adds a `return_field_from_args` override that reports a non-nullable field. The default implementation it previously used made the field nullable. Both are a one-line nullability correction; the inner struct fields stay nullable, since an individual member value can still be NULL. - New `sqllogictest` cases at the end of `datafusion/sqllogictest/test_files/struct.slt` covering the folded scalar results (`named_struct(...) IS NOT NULL`, `struct(...) IS NULL`), the `EXPLAIN` for `WHERE s IS NOT NULL` over a view showing `TableScan: t projection=[b]`, the values that query returns, and the `CASE WHEN named_struct(...) IS NOT NULL` fold. - Existing `datafusion-functions` unit tests: `cargo test -p datafusion-functions --lib`, 356 passed. - The full `sqllogictest` suite: all 518 files pass, with no other expectation changes needed. Yes, but no API change. - The schema nullability of the output of `named_struct()` and `struct()` changes from nullable to non-nullable. This now matches the array those functions actually build. - `IS NULL` and `IS NOT NULL` applied directly to one of these constructors constant-fold to `false` and `true`. Query results are unchanged; plans get smaller and can prune more. (cherry picked from commit 32662e7)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Which issue does this PR close?
Rationale for this change
named_struct(...)andstruct(...)never produce a NULL row:invoke_with_argsbuilds the outputStructArraywith no null buffer. Both functions nevertheless reported a nullable return field, andExprSchemable::nullablefor a scalar function reads that field. As a result the simplifier rule that rewritesa IS NOT NULLtotruefor a non-nullableanever fired on a struct constructor.The user-visible cost is lost pruning. A guard such as
WHERE s IS NOT NULLon a struct built by a view cannot remove any row, but it keeps the whole struct expression alive, so projection pushdown can no longer prune the scan to the fields that are read:Before:
After:
This matches the plan the same query already gets without the guard, and lets the existing
get_field(named_struct(...), 'f')simplification (#22239) remove the constructor.What changes are included in this PR?
datafusion/functions/src/core/named_struct.rs:return_field_from_argsnow reports a non-nullable field instead of a nullable one.datafusion/functions/src/core/struct.rs: adds areturn_field_from_argsoverride that reports a non-nullable field. The default implementation it previously used made the field nullable.Both are a one-line nullability correction; the inner struct fields stay nullable, since an individual member value can still be NULL.
What is the testing strategy for this PR?
sqllogictestcases at the end ofdatafusion/sqllogictest/test_files/struct.sltcovering the folded scalar results (named_struct(...) IS NOT NULL,struct(...) IS NULL), theEXPLAINforWHERE s IS NOT NULLover a view showingTableScan: t projection=[b], the values that query returns, and theCASE WHEN named_struct(...) IS NOT NULLfold.datafusion-functionsunit tests:cargo test -p datafusion-functions --lib, 356 passed.sqllogictestsuite: all 518 files pass, with no other expectation changes needed.Are there any user-facing changes?
Yes, but no API change.
named_struct()andstruct()changes from nullable to non-nullable. This now matches the array those functions actually build.IS NULLandIS NOT NULLapplied directly to one of these constructors constant-fold tofalseandtrue. Query results are unchanged; plans get smaller and can prune more.