Skip to content

expr: Accept stateful unary and binary functions in the sqlfunc macro - #38941

Open
antiguru wants to merge 8 commits into
sqlfunc-canonicalizationfrom
sqlfunc-stateful-unary-binary
Open

antiguru wants to merge 8 commits into
sqlfunc-canonicalizationfrom
sqlfunc-stateful-unary-binary

Conversation

@antiguru

@antiguru antiguru commented Sep 19, 2026

Copy link
Copy Markdown
Member

EagerUnaryFunc::call now receives a &RowArena, and the #[sqlfunc] macro accepts a &self receiver and a trailing &RowArena at unary and binary arity rather than only at variadic. Together these unblock the ten hand-written LazyUnaryFunc implementations under src/expr/src/scalar/func/impls/ that exist in that form only because they need an arena to allocate their output.

A skip_display modifier suppresses the generated Display impl, for structs whose displayed name depends on their own state. RangeCreate is the first function converted on top of both capabilities. It carries an elem_type field, so it needs the stateful shape, and it picks its name from that field, so it keeps a hand-written Display.

The documentation commits correct the macro reference. Three surfaces describe which modifiers apply to which arity, and all three had drifted from the tables in shape.rs that actually decide it.

Evidence the conversion preserves semantics. The scalar function registry from #38868 reports exactly one change across the whole catalog: range_create's sqlfunc_signature goes from null to the macro's signature. Every declared property is byte-identical, including propagates_nulls, introduces_nulls, could_error, input_types, and output_type. test/sqllogictest/range.slt passes 1534 of 1534.

One question for the reviewer. Regenerating func_registry.json changes version 1's digest in func_registry_digests.json. The test's guidance is to bump LIR_VERSION if version 1 has shipped and to regenerate in place if it has not. plan.rs ties the bump to a scalar function's declared properties changing, and none did, so this regenerates in place without bumping. But #38868 is contained in v26.43.0-rc.1, so if that counts as shipped the version should be bumped instead.

Based on #38929.

🤖 Generated with Claude Code

@antiguru
antiguru added this pull request to stack #38942 September 19, 2026 21:17
@antiguru antiguru changed the title sqlfunc stateful unary binary expr: Accept stateful unary and binary functions in the sqlfunc macro Sep 19, 2026
@antiguru
antiguru marked this pull request as ready for review September 19, 2026 21:18
@antiguru
antiguru requested review from a team as code owners September 19, 2026 21:18
antiguru and others added 8 commits September 19, 2026 23:31
`EagerBinaryFunc::call` and `EagerVariadicFunc::call` take an arena, unary did
not. That gap blocks the compound-type casts that allocate their output, which
have to stay hand-written `LazyUnaryFunc` impls to reach an arena at all.

`EagerUnaryFunc::call` now takes `temp_storage: &'a RowArena`, and the blanket
`LazyUnaryFunc` impl forwards the arena it already holds. `Shape::takes_arena`
answers true for every arity, so `#[sqlfunc]` emits the parameter on unary
`call` and forwards it when the annotated function asks for one. A unary
function with a trailing `&RowArena` is therefore accepted rather than rejected.

The hand-written unary impls take the parameter and ignore it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`#[sqlfunc(StructName, ..)]` on a function with a `&self` receiver now works for
unary and binary, as it already did for variadic. The macro attaches an inherent
method and the trait impl to the existing struct instead of defining a unit
struct.

Both arms previously indexed arguments from position zero, so the receiver reached
`arg_type` and produced `compile_error!("Unsupported argument type")`. Because the
arities share one generator, this is an offset and a call expression rather than
two copies of the variadic logic.

Also inlines `Shape::takes_arena`, which had become a constant-valued method with
a single call site now that every arity takes the arena, and drops the test that
looped over shapes asserting it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
generate's doc comment and the attribute-parsing comment in sqlfunc both
described the struct-name argument as exclusive to variadic dispatch. Any
arity accepts one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Twenty-four of the hand-written scalar functions format their name from struct
state, for example `extract_{unit}_ts`. Suppressing the generated `Display` lets
them keep the impl they already have, which is simpler than teaching `sqlname` to
evaluate an expression with `self` in scope.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The insta_test_skip_display assert checked for "impl std :: fmt :: Display"
with spaces around the double colons, the raw TokenStream::to_string() idiom.
But test_sqlfunc formats output through prettyplease, which never spaces
double colons, so the assert could never fire and only the snapshot enforced
suppression. Match the unspaced form prettyplease actually produces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`RangeCreate` carries an `elem_type` field, so it needs the stateful shape the
macro now accepts: the function takes `&self` and the macro emits the `call`
body as a method on the existing struct. Its `Display` impl picks a name from
`elem_type` rather than using a fixed string, so `skip_display` keeps the
hand-written impl and the macro emits only `FuncName`. That removes the manual
`func_name!` entry, which would otherwise collide with the generated impl.

The output type is expressed with `output_type_expr` because it reads
`elem_type` and ignores the input types, matching the hand-written body.
`introduces_nulls = false` carries across the one nullability override; the
remaining trait methods derive the same values from the signature that they
derived from the associated types before, so the generated impl matches.

Also corrects two stale claims in the macro documentation: unary functions now
accept a `&RowArena` and a `&self` receiver, and `output_type_expr` applies to
all three arities.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three documents describe which modifiers apply to which arity: the field docs on
`Modifiers`, the macro's rustdoc, and `doc/developer/sqlfunc.md`. They had
drifted from the tables in `shape.rs`, which are the actual authority. Corrects
`output_type_expr` to all arities, `is_infix_op` to binary and variadic,
`is_monotone` to all arities, and adds the three modifiers the rustdoc omitted
entirely.

Moves the struct name, `&self` receiver, and `&RowArena` sections out from under
the variadic heading, since all three apply at every arity, and states that the
generated struct and the generated `Display` are each conditional.

Adds the `non_nullable_input_is_nullable` term to the null-handling formula. The
formula omitted it, so it could not be used to reason about a function whose
parameters do not all accept NULL.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`sqlname` feeds only the generated `Display` impl, so pairing it with
`skip_display` silently discards it. Rejecting the combination turns a modifier
that does nothing into a compile error, and both modifier references now say so.

Records the interlock behind unary's nullability tail. Unary omits the
non-nullable-position term the other two arities carry, which is sound only
because `propagates_nulls` is not a unary modifier, so unary's value always
equals what that term would test. A test already pins that absence, but it did
so without saying why, leaving a maintainer who deliberately adds the modifier
free to delete the assertion. The assertion now names what depends on it.

Corrects three claims the tree no longer supports. `is_infix_op` has a default
on every `Eager*Func` trait, so it does not need to be specified. The nine
`LazyUnaryFunc` implementations that stay hand-written are the ones generic over
`Eval`, not every compound-type cast, and the ten others are convertible now
that `EagerUnaryFunc::call` supplies a `RowArena`. `ListLengthMax` gave its
`max_layer` field as the reason it could not use the macro, which a `&self`
receiver now handles.

Replaces the struct-name example, which showed a two-parameter signature that
detects as binary under a name saying variadic, with the real `array_fill`
signature. Normalizes the test module on `super::` for its own helper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antiguru
antiguru force-pushed the sqlfunc-stateful-unary-binary branch from 9ef1418 to abbd17d Compare September 19, 2026 21:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant