fix: combine set-operation nullability across all inputs#221
Merged
tokoko merged 1 commit intoJul 16, 2026
Merged
Conversation
infer_rel_schema derived a SetRel's output schema from rel.set.inputs[0] only and never read rel.set.op, so for UNION/INTERSECTION the output nullability could be wrong (too strict) when inputs disagree on a field's nullability. This is reachable via the union/intersect/except_ verbs, whose SetRel stores no schema. Infer every input and combine each field's nullability per the operation (matching the Substrait spec's set-operation output-type derivation): UNION -> nullable if nullable in any input; INTERSECTION_PRIMARY -> nullable only when nullable in the primary and in some secondary (required otherwise); INTERSECTION_MULTISET(_ALL) -> required if required in any input; MINUS_* -> same as the primary. Field types are taken from the primary (the spec requires identical field types across inputs). Closes substrait-io#219.
nielspardon
force-pushed
the
fix/setrel-nullability-combination
branch
from
July 16, 2026 13:53
ad030d9 to
a6f9141
Compare
tokoko
approved these changes
Jul 16, 2026
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.
Problem
type_inference.infer_rel_schema'ssetbranch derived aSetRel's output schema fromrel.set.inputs[0]only and never readrel.set.op. Per the Substrait spec, set inputs share field types but may differ in nullability, and the output nullability must be combined across all inputs according to the operation. So forUNION/INTERSECTION, the inferred nullability could be wrong (too strict) when inputs disagree.Reachable through this library's own builders:
plan.set(...)and the DataFrameunion/intersect/except_verbs build aSetRelwith no stored schema, so a laterinfer_plan_schemaover a union of two sub-plans whose shared columns differ in nullability under-reported nullability.Fix
Infer every input and combine each field's nullability per the operation, matching the spec's set-operation output-type derivation:
Field types are taken from the primary input (the spec requires identical field types across inputs); only the top-level nullability is recombined, and
CopyFrompreserves type parameters and nested element types.Testing
test_inference_set_nullability— parametrized over all 8 concrete set ops using the spec's own worked example (three inputs, one column per required/nullable combination) and asserting the spec's exact output columns.test_inference_set_nullability_preserves_field_types— aUNION_ALLoverdecimal(10,2)/varchar(5)/list<string>columns, asserting the fullType.Structso type parameters, nested element types, and struct-level nullability are all verified.ruff format --checkandruff checkclean.An adversarial cross-check (independent re-derivation of all 9
SetOpcases against the spec, plus edge-case and test-completeness passes) confirmed the rules; a defensive guard was added so a fieldTypewith nokindset passes through unchanged rather than raising.Relationship to #206 / #220
Independent. #220 (registry scoping) also touches the
setbranch line to threadregistry=registry; whichever merges second is a trivial rebase (keep both changes).Closes #219.
🤖 Generated with AI