Describe the bug
named_struct(...) and struct(...) can never return a NULL row: invoke_with_args builds the output StructArray with no null buffer. However, both functions report a nullable return field. Since ExprSchemable::nullable for a scalar function reads that field, the simplifier rule that rewrites a IS NOT NULL to true when a is not nullable never fires for these constructors.
The user-visible effect is that a guard such as WHERE s IS NOT NULL on a struct built by a view keeps the entire struct expression alive. Projection pushdown can then no longer prune the scan down to the fields that are actually read, even though the existing get_field(named_struct(...), 'f') simplification (#22239) would otherwise make the struct disappear entirely.
To Reproduce
Run with datafusion-cli -f repro.sql (DataFusion 55.1.0, main at the time of writing):
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;
-- prunes to projection=[b]
EXPLAIN SELECT s['b'] FROM v;
-- same query with a redundant guard: the whole named_struct is kept
EXPLAIN SELECT s['b'] FROM v WHERE s IS NOT NULL;
EXPLAIN SELECT CASE WHEN named_struct('a', a) IS NOT NULL THEN b END FROM t;
Current behavior
The unguarded query prunes correctly:
logical_plan
Projection: __datafusion_extracted_1 AS v.s[b]
SubqueryAlias: v
Projection: t.b AS __datafusion_extracted_1
TableScan: t projection=[b]
Adding WHERE s IS NOT NULL, which cannot remove any row, defeats the pruning and reads all three columns:
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]
The CASE WHEN form keeps the constructor and the extra column too:
logical_plan
Projection: CASE WHEN named_struct(Utf8("a"), t.a) IS NOT NULL THEN t.b END
TableScan: t projection=[a, b]
Expected behavior
IS NOT NULL on a struct constructor should fold to true (and IS NULL to false), because the constructor never produces a NULL row. The guard should then disappear and the scan should read only b in all three plans:
logical_plan
Projection: __datafusion_extracted_1 AS v.s[b]
SubqueryAlias: v
Projection: t.b AS __datafusion_extracted_1
TableScan: t projection=[b]
logical_plan
Projection: t.b AS CASE WHEN named_struct(Utf8("a"),t.a) IS NOT NULL THEN t.b END
TableScan: t projection=[b]
Cause
return_field_from_args in datafusion/functions/src/core/named_struct.rs marks the returned Field nullable, and struct.rs relies on the default return_field_from_args, which does the same. Both should report a non-nullable field to match what invoke_with_args actually builds.
Additional context
DataFusion 55.1.0.
Describe the bug
named_struct(...)andstruct(...)can never return a NULL row:invoke_with_argsbuilds the outputStructArraywith no null buffer. However, both functions report a nullable return field. SinceExprSchemable::nullablefor a scalar function reads that field, the simplifier rule that rewritesa IS NOT NULLtotruewhenais not nullable never fires for these constructors.The user-visible effect is that a guard such as
WHERE s IS NOT NULLon a struct built by a view keeps the entire struct expression alive. Projection pushdown can then no longer prune the scan down to the fields that are actually read, even though the existingget_field(named_struct(...), 'f')simplification (#22239) would otherwise make the struct disappear entirely.To Reproduce
Run with
datafusion-cli -f repro.sql(DataFusion 55.1.0,mainat the time of writing):Current behavior
The unguarded query prunes correctly:
Adding
WHERE s IS NOT NULL, which cannot remove any row, defeats the pruning and reads all three columns:The
CASE WHENform keeps the constructor and the extra column too:Expected behavior
IS NOT NULLon a struct constructor should fold totrue(andIS NULLtofalse), because the constructor never produces a NULL row. The guard should then disappear and the scan should read onlybin all three plans:Cause
return_field_from_argsindatafusion/functions/src/core/named_struct.rsmarks the returnedFieldnullable, andstruct.rsrelies on the defaultreturn_field_from_args, which does the same. Both should report a non-nullable field to match whatinvoke_with_argsactually builds.Additional context
DataFusion 55.1.0.