Skip to content

expr: Convert the remaining hand-written functions to the sqlfunc macro - #39062

Merged
antiguru merged 20 commits into
sqlfunc-binary-direct-unwrapfrom
sqlfunc-convert-int16
Oct 14, 2026
Merged

antiguru merged 20 commits into
sqlfunc-binary-direct-unwrapfrom
sqlfunc-convert-int16

Conversation

@antiguru

@antiguru antiguru commented Sep 23, 2026 •

Copy link
Copy Markdown
Member

Converts the remaining hand-written scalar functions to #[sqlfunc], one commit per source file or function: the Cast*ToNumeric casts (int16, int32, int64, uint16, uint32, uint64, float32, float64, jsonb), PadChar, AdjustNumericScale, CastArrayToString, CastRangeToString, and all of date.rs, time.rs, map.rs, record.rs, list.rs, timestamp.rs and string.rs. Each commit message accounts for every method the deleted impl overrode.

No declared property changes. The registry reports only the macro provenance fields moving. Where a hand-written impl declared a property more conservatively than the body needs, the conversion pins the declared value: could_error on record_get, and introduces_nulls, could_error and a nullable output on map_build_from_record_list. #39814 corrects both.

Supersedes #39079 to #39097, which held one conversion each.

Based on #39061.

🤖 Generated with Claude Code

@antiguru
antiguru requested a review from a team as a code owner September 23, 2026 14:24
@antiguru
antiguru force-pushed the sqlfunc-convert-int16 branch from 34f958f to 8bbbe96 Compare September 23, 2026 14:33
@antiguru
antiguru requested a review from a team as a code owner September 23, 2026 17:54
@antiguru
antiguru force-pushed the sqlfunc-convert-int16 branch from 8bbbe96 to 1bad261 Compare September 23, 2026 17:54
@antiguru
antiguru force-pushed the sqlfunc-convert-int16 branch from 1bad261 to 14443d5 Compare September 23, 2026 18:09
@antiguru
antiguru added this pull request to stack #38942 September 23, 2026 18:15

@mgree mgree left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Even Claude only has silly comments.

@mgree mgree left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a fine refactor to me. I wonder if we need funcname! at all anymore, but not urgent.

Comment on lines +72 to +73
// The hand-written impl declared a nullable output that introduces NULLs and can
// error, although the body does neither. The properties are kept as declared.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make an issue in linear to fix this later?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issue needed: #39814, two PRs up the stack, drops these pins and lets the macro derive introduces_nulls and could_error as false with a non-nullable output. It is kept separate so this PR changes no declared properties.

Posted by Claude Code.

@antiguru

antiguru commented Oct 14, 2026 •

Copy link
Copy Markdown
Member Author

On func_name!: it still names 17 functions that stay hand-written: CaseLiteral, the generic Cast*<E> casts and TryCast<E>, and the variadics And, Or, Coalesce, Greatest, Least and ErrorIfNull. It can go once those convert or get their FuncName another way.

Posted by Claude Code.

antiguru and others added 20 commits October 14, 2026 20:54
`CastInt16ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on the
existing struct instead of defining a unit struct. The hand-written `Display`
wrote the fixed string `smallint_to_numeric` without reading the payload, so
`sqlname` reproduces it and the impl is deleted. The macro then generates `FuncName`
with `NAME` from the annotated function's name, which is the
`"cast_int16_to_numeric"` the manual `func_name!` entry declared, so that entry
is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `i16` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `could_error`: carried across as `self.0.is_some()`. The derived default reads
  the `Result` output and would widen it to an unconditional `true`.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `propagates_nulls` and
`preserves_uniqueness` were not overridden and keep deriving as before.

The registry snapshot moves only in its provenance fields: `sqlfunc_signature`,
`sqlfunc_decl` and `body_fingerprint` go from null to populated for both the
unscaled and the scaled `cast_int16_to_numeric` record. No declared property
changed, so the digest is regenerated in place without bumping `LIR_VERSION`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`PadChar` reads its `length` field in both the call and the output type, so it
takes a `&self` receiver and an `output_type_expr`. The expression collapses to
what the hand-written `output_sql_type` returned: it sets nullability from the
input, and the macro's tail adds only `propagates_nulls && input_type.nullable`,
which the non-nullable `&str` input makes the same term.

`introduces_nulls` derives false from the `Char<String>` output. `could_error`
stays derived, false for a non-`Result` output, matching an impl that never
overrode it.

The `Display` impl wrote a fixed string, so `sqlname` reproduces it. Note that
the SQL name is `padchar` while the function is `pad_char`, so the `func_name!`
entry the macro replaces keeps its own spelling.

The registry reports only `sqlfunc_signature` moving, on both the bounded and
unbounded samples. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastFloat32ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `real_to_numeric` without reading the payload,
so `sqlname` reproduces it and the impl is deleted. The macro then generates
`FuncName` with `NAME` from the annotated function's name, which is the
`"cast_float32_to_numeric"` the manual `func_name!` entry declared, so that
entry is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `f32` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `could_error` was not overridden and
keeps deriving to `true` from the `Result` output, as do `propagates_nulls` and
`preserves_uniqueness`.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_float32_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastFloat64ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `double_to_numeric` without reading the
payload, so `sqlname` reproduces it and the impl is deleted. The macro then
generates `FuncName` with `NAME` from the annotated function's name, which is
the `"cast_float64_to_numeric"` the manual `func_name!` entry declared, so that
entry is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `f64` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `could_error` was not overridden and
keeps deriving to `true` from the `Result` output, as do `propagates_nulls` and
`preserves_uniqueness`.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_float64_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastInt32ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `integer_to_numeric` without reading the
payload, so `sqlname` reproduces it and the impl is deleted. The macro then
generates `FuncName` with `NAME` from the annotated function's name, which is
the `"cast_int32_to_numeric"` the manual `func_name!` entry declared, so that
entry is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `i32` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `could_error`: carried across as `self.0.is_some()`. The derived default reads
  the `Result` output and would widen it to an unconditional `true`.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `propagates_nulls` and
`preserves_uniqueness` were not overridden and keep deriving as before.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_int32_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastInt64ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `bigint_to_numeric` without reading the
payload, so `sqlname` reproduces it and the impl is deleted. The macro then
generates `FuncName` with `NAME` from the annotated function's name, which is
the `"cast_int64_to_numeric"` the manual `func_name!` entry declared, so that
entry is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `i64` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `could_error`: carried across as `self.0.is_some()`. The derived default reads
  the `Result` output and would widen it to an unconditional `true`.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `propagates_nulls` and
`preserves_uniqueness` were not overridden and keep deriving as before.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_int64_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastUint16ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `uint2_to_numeric` without reading the payload,
so `sqlname` reproduces it and the impl is deleted. The macro then generates
`FuncName` with `NAME` from the annotated function's name, which is the
`"cast_uint16_to_numeric"` the manual `func_name!` entry declared, so that entry
is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `u16` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `could_error`: carried across as `self.0.is_some()`. The derived default reads
  the `Result` output and would widen it to an unconditional `true`.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `propagates_nulls` and
`preserves_uniqueness` were not overridden and keep deriving as before.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_uint16_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastUint32ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `uint4_to_numeric` without reading the payload,
so `sqlname` reproduces it and the impl is deleted. The macro then generates
`FuncName` with `NAME` from the annotated function's name, which is the
`"cast_uint32_to_numeric"` the manual `func_name!` entry declared, so that entry
is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `u32` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `could_error`: carried across as `self.0.is_some()`. The derived default reads
  the `Result` output and would widen it to an unconditional `true`.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `propagates_nulls` and
`preserves_uniqueness` were not overridden and keep deriving as before.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_uint32_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastUint64ToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `uint8_to_numeric` without reading the payload,
so `sqlname` reproduces it and the impl is deleted. The macro then generates
`FuncName` with `NAME` from the annotated function's name, which is the
`"cast_uint64_to_numeric"` the manual `func_name!` entry declared, so that entry
is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `u64` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `could_error`: carried across as `self.0.is_some()`. The derived default reads
  the `Result` output and would widen it to an unconditional `true`.
* `inverse`: carried across.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `propagates_nulls` and
`preserves_uniqueness` were not overridden and keep deriving as before.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_uint64_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`AdjustNumericScale` carries the target `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `adjust_numeric_scale` without reading the
payload, so `sqlname` reproduces it and the impl is deleted. The macro then
generates `FuncName` with `NAME` from the annotated function's name, which is
the `"adjust_numeric_scale"` the manual `func_name!` entry declared, so that
entry is deleted to avoid a duplicate impl.

The deleted `EagerUnaryFunc` impl overrode only two methods:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `Numeric` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `could_error`, `propagates_nulls`,
`preserves_uniqueness`, `inverse` and `is_monotone` were not overridden and keep
deriving as before.

The registry reports only `sqlfunc_signature` moving, on the single
`adjust_numeric_scale` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastJsonbToNumeric` carries an optional `NumericMaxScale`, so the annotated
function takes a `&self` receiver and the macro generates `call` as a method on
the existing struct instead of defining a unit struct. The hand-written
`Display` wrote the fixed string `jsonb_to_numeric` without reading the payload,
so `sqlname` reproduces it and the impl is deleted. The macro then generates
`FuncName` with `NAME` from the annotated function's name, which is the
`"cast_jsonb_to_numeric"` the manual `func_name!` entry declared, so that entry
is deleted to avoid a duplicate impl.

Every method the deleted `EagerUnaryFunc` impl overrode is accounted for:

* `call`: the annotated function's body, unchanged.
* `output_sql_type`: expressed as `output_type_expr`, which reads `self.0` and
  the `input_type` binding. The generated tail ORs in `propagates_nulls &&
  input_type.nullable`, and `propagates_nulls` derives to `true` from the
  non-nullable `JsonbRef<'a>` input, so the result is the same
  `nullable(input_type.nullable)` the deleted body returned.
* `is_monotone`: carried across.

`introduces_nulls` is not overridden: `Result<Numeric, EvalError>` derives
`false`, which is what the impl inherited. `could_error` was not overridden and
keeps deriving to `true` from the `Result` output, as do `propagates_nulls`,
`preserves_uniqueness` and `inverse`.

The registry reports only `sqlfunc_signature` moving, on both the unscaled and
the scaled `cast_jsonb_to_numeric` record. No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastDateToTimestamp`, `CastDateToTimestampTz` and `ExtractDate` all read their
payload in `call`, so each takes a `&self` receiver.

The two casts need `output_type_expr`: their `output_sql_type` built the
precision out of the payload, which the derived `CheckedTimestamp` column type
cannot express. The expression sets nullability from the input and the macro's
unary tail adds only `propagates_nulls && input_type.nullable`, which the
non-nullable `Date` input makes the same term. `introduces_nulls` derives false
from the non-`Option` output.

`ExtractDate` needs no output modifier. Its `output_sql_type` returned
`Numeric { max_scale: None }.nullable(input.nullable)`, which is exactly what
`Result<Numeric, EvalError>` derives once the tail is applied.

Per-override accounting, for all three: `call` becomes the function body.
`preserves_uniqueness = true` and `inverse` carry over on the casts as
modifiers, `is_monotone = true` likewise, and `ExtractDate` keeps its
payload-dependent `is_monotone` as an expression over `self`. `could_error`
stays derived, true for the `Result` outputs, matching impls that never
overrode it. `propagates_nulls` was never overridden and derives true from the
non-nullable input.

The casts' `Display` wrote a fixed string, so `sqlname` reproduces it, and the
SQL names differ from the function names. `ExtractDate` formats its payload, so
it keeps its hand-written `Display` under `skip_display = true`.

The registry reports only `sqlfunc_signature` moving, on all six records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ExtractTime`, `DatePartTime` and `TimezoneTime` all read their payload in
`call`, so each takes a `&self` receiver.

None of them needs an output modifier. Each hand-written `output_sql_type`
returned the derived output column type with nullability taken from the input,
and the macro's unary tail is `output.nullable(nullable || (propagates_nulls &&
input_type.nullable))`, which for the non-nullable `NaiveTime` input and a
non-nullable output collapses to exactly that.

Per-override accounting, for all three: `call` becomes the function body, and
`output_sql_type` becomes the derived default. Nothing else was overridden, so
`could_error` stays derived, true for the two `Result` outputs and false for
`TimezoneTime`, and `propagates_nulls` derives true from the non-nullable
input.

All three `Display` impls format the payload, so they stay hand-written under
`skip_display = true`.

`TimezoneTime`'s function name collides with the module's free `timezone_time`.
The generated method is inherent, so the two coexist, and the body spells the
call `self::timezone_time` to say which one it reaches.

The registry reports only `sqlfunc_signature` moving, on all five records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The impl was hand-written because it allocates its result, which the eager
trait now covers: a `String` output is pushed into the arena by
`OutputDatumType::into_result`, exactly as the body did by hand.

The body also checked NULL itself, and under the macro that check has to come
from the input type. The input is `ExcludeNull<Datum<'a>>`, because
`stringify_datum` takes a raw `Datum` and a bare `Datum` accepts NULL.
`ExcludeNull` reports `nullable() == false`, so the derived `propagates_nulls`
is true, matching the impl's declaration, and its `try_from_result` turns
`Datum::Null` into `Err(Ok(Datum::Null))`, which the evaluation layer returns
as `Ok(Datum::Null)`. That is the manual check, so the manual check is gone.
`introduces_nulls` derives false from the non-`Option` `String` inside the
`Result`, matching the declaration.

Per-override accounting: `eval` becomes the function body minus the NULL check.
`output_sql_type` returned `String.nullable(input_type.nullable)`, which is
what the derived `String` output plus the unary tail gives for a non-nullable
input, so no output modifier is needed. `preserves_uniqueness = true` carries
over. `propagates_nulls` and `introduces_nulls` are derived as above.
`could_error` was not overridden and inherited `LazyUnaryFunc`'s conservative
`true`, which the fallible `Result` output derives. `is_monotone` and
`is_eliminable_cast` returned the trait defaults and are dropped, as is
`inverse`, whose `TODO?` about moving `typeconv` into `expr` now sits on the
function.

`Display` wrote a fixed string, so `sqlname` reproduces it.

The registry reports only `sqlfunc_signature` moving, on the one record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The impl was hand-written because it allocates its result, which the eager
trait now covers: a `String` output is pushed into the arena by
`OutputDatumType::into_result`, exactly as the body did by hand.

The body also checked NULL itself, and under the macro that check has to come
from the input type. The input is `ExcludeNull<Datum<'a>>`, because
`stringify_datum` takes a raw `Datum` and a bare `Datum` accepts NULL.
`ExcludeNull` reports `nullable() == false`, so the derived `propagates_nulls`
is true, matching the impl's declaration, and its `try_from_result` turns
`Datum::Null` into `Err(Ok(Datum::Null))`, which the evaluation layer returns
as `Ok(Datum::Null)`. That is the manual check, so the manual check is gone.
`introduces_nulls` derives false from the non-`Option` `String` inside the
`Result`, matching the declaration.

Per-override accounting: `eval` becomes the function body minus the NULL check.
`output_sql_type` returned `String.nullable(input_type.nullable)`, which is
what the derived `String` output plus the unary tail gives for a non-nullable
input, so no output modifier is needed. `preserves_uniqueness = true` carries
over. `propagates_nulls` and `introduces_nulls` are derived as above.
`could_error` was not overridden and inherited `LazyUnaryFunc`'s conservative
`true`, which the fallible `Result` output derives. `is_monotone` and
`is_eliminable_cast` returned the trait defaults and are dropped, as is
`inverse`, whose `TODO?` about `typeconv` now sits on the function.

`Display` wrote a fixed string, so `sqlname` reproduces it. With the impl gone
the file has no `Display` left, so `std::fmt` and the now unused `Eval`,
`LazyUnaryFunc`, `RowArena` and `SqlColumnType` imports go with it.

The registry reports only `sqlfunc_signature` moving, on the one record.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both impls were hand-written because they allocate their result, which the
eager trait now covers, and both checked NULL in the body, which under the
macro has to come from the input type.

`CastMapToString` takes `ExcludeNull<Datum<'a>>`, because `stringify_datum`
takes a raw `Datum` and a bare `Datum` accepts NULL. `ExcludeNull` reports
`nullable() == false`, so the derived `propagates_nulls` is true, matching the
declaration, and its `try_from_result` turns `Datum::Null` into
`Err(Ok(Datum::Null))`, which the evaluation layer returns as `Ok(Datum::Null)`.
`introduces_nulls` derives false from the `String` inside the `Result`. Its
`output_sql_type` returned `String.nullable(input_type.nullable)`, which is
what the derived output plus the unary tail gives for a non-nullable input, so
it needs no output modifier. `preserves_uniqueness = true` carries over, and
`could_error` derives true from the fallible output, matching the
`LazyUnaryFunc` default the impl inherited.

`MapBuildFromRecordList` takes `DatumList<'a>`, which the body already required
by unwrapping the input as a list. `DatumList` reports `nullable() == false` on
its own, so no `ExcludeNull` wrapper is needed and the derived
`propagates_nulls` is true. It returns `DatumMap<'a>`, because the body always
builds a map. The impl declared `introduces_nulls` and `could_error` true and a
nullable output, although the body returns NULL for no non-NULL input and
cannot fail. The conversion keeps all three as declared: `introduces_nulls =
true` and `could_error = true` pin the flags the `DatumMap` output would derive
false, and the `output_type_expr` that builds the map type from the payload
keeps `nullable(true)`.

Per-override accounting beyond the above, for both: `eval` becomes the function
body minus the NULL check, and `preserves_uniqueness` (on
`MapBuildFromRecordList`), `inverse`, `is_monotone` and `is_eliminable_cast`
returned the trait defaults and are dropped.

Both `Display` impls wrote a fixed string, so `sqlname` reproduces them, and
`map_build` differs from the function name. With no `Display` left the file
drops `std::fmt` along with the now unused `Eval`, `LazyUnaryFunc` and
`SqlColumnType` imports.

The registry reports only `sqlfunc_signature` moving, on both records. No
declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both impls checked NULL in the body, which under the macro has to come from the
input type.

`CastRecordToString` was hand-written because it allocates its result, which the
eager trait now covers: a `String` output is pushed into the arena by
`OutputDatumType::into_result`. Its input is `ExcludeNull<Datum<'a>>`, because
`stringify_datum` takes a raw `Datum` and a bare `Datum` accepts NULL.
`ExcludeNull` reports `nullable() == false`, so the derived `propagates_nulls`
is true, matching the declaration, and its `try_from_result` turns
`Datum::Null` into `Err(Ok(Datum::Null))`, which the evaluation layer returns as
`Ok(Datum::Null)`. `introduces_nulls` derives false from the `String` inside the
`Result`. `output_sql_type` returned `String.nullable(input_type.nullable)`,
which the derived output plus the unary tail gives for a non-nullable input, so
no output modifier is needed. `preserves_uniqueness = true` carries over,
`could_error` derives true from the fallible output, and `inverse`,
`is_monotone` and `is_eliminable_cast` returned the trait defaults and are
dropped. The `TODO?` on `inverse` now sits on the function. `Display` wrote a
fixed string, so `sqlname` reproduces it.

`RecordGet` allocates nothing and returns a field borrowed from its input, so
its output is a bare `Datum<'a>` and it takes no arena. Its input is
`DatumList<'a>`, which is what the body unwrapped the record to; `DatumList`
reports `nullable() == false` on its own, so no `ExcludeNull` wrapper is needed
and the derived `propagates_nulls` is true.

`RecordGet`'s `output_sql_type` reads the input, so it becomes an
`output_type_expr` that returns the selected field's column type. The
hand-written version also ORed `input_type.nullable` onto that type, which the
unary tail now does, so the expression drops the term. `introduces_nulls`
derives true from the bare `Datum` output, which is what the impl declared. The
output is a bare `Datum`, from which `could_error` would derive false, so the
modifier pins the `true` the impl declared by inheriting `LazyUnaryFunc`'s
conservative default. `preserves_uniqueness`, `inverse`, `is_monotone` and
`is_eliminable_cast` returned the trait defaults and are dropped, and its
`Display` formats the field index, so it stays hand-written under `skip_display
= true`.

`func_registry_records_sqlfunc_sources` used `record_get` as its example of a
function with no recorded source. It now has one, so the assertion moves to
`extract_interval`.

The registry reports only `sqlfunc_signature` moving, on both records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`CastListToString` was hand-written because it allocates its result, which the
eager trait now covers: a `String` output is pushed into the arena by
`OutputDatumType::into_result`. It also checked NULL in the body, which under
the macro has to come from the input type. Its input is
`ExcludeNull<Datum<'a>>`, because `stringify_datum` takes a raw `Datum` and a
bare `Datum` accepts NULL. `ExcludeNull` reports `nullable() == false`, so the
derived `propagates_nulls` is true, matching the declaration, and its
`try_from_result` turns `Datum::Null` into `Err(Ok(Datum::Null))`, which the
evaluation layer returns as `Ok(Datum::Null)`. `introduces_nulls` derives false
from the `String` inside the `Result`.

Its remaining overrides: `output_sql_type` returned
`String.nullable(input_type.nullable)`, which the derived output plus the unary
tail gives for a non-nullable input, so no output modifier is needed.
`preserves_uniqueness = true` carries over, `could_error` derives true from the
fallible output, matching the `LazyUnaryFunc` default the impl inherited, and
`inverse`, `is_monotone` and `is_eliminable_cast` returned the trait defaults
and are dropped. The `TODO?` on `inverse` now sits on the function. `Display`
wrote a fixed string, so `sqlname` reproduces it.

`ListLengthMax` is an `EagerBinaryFunc` that reads `max_layer` in `call`, so it
takes a `&self` receiver. Its `output_sql_type` spelled out the macro's own
binary tail, minus the non-nullable-position term, which the `Option` output's
`nullable` of true subsumes, so it needs no output modifier. Nothing else was
overridden: `could_error` and `introduces_nulls` derive true from
`Result<Option<i32>, EvalError>`, `propagates_nulls` derives true from the
non-nullable inputs, and `is_infix_op`, `is_monotone`, `is_infinity_monotone`
and `negate` keep the trait defaults. Its `Display` wrote `list_length_max`,
which is the function name, so the generated impl reproduces it without a
`sqlname`. The struct doc loses only its now satisfied TODO.

`doc/developer/sqlfunc.md` counted the convertible hand-written
`LazyUnaryFunc` implementations. Seven of the ten are now converted, so the
paragraph names the three that remain, all in `string.rs`.

The registry reports only `sqlfunc_signature` moving, on both records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All sixteen hand-written `EagerUnaryFunc` impls in the file read their payload in
`call`, so each takes a `&self` receiver and the macro attaches the annotated
function as an inherent method on the existing struct.

Four of them need `output_type_expr`. `CastTimestampToTimestampTz`,
`AdjustTimestampPrecision`, `CastTimestampTzToTimestamp` and
`AdjustTimestampTzPrecision` build the output precision out of `self.to`, which
the derived `CheckedTimestamp` column type cannot express. Each expression sets
nullability from the input, and the unary tail adds only `propagates_nulls &&
input_type.nullable`, which the non-nullable timestamp input makes the same
term. `introduces_nulls` derives false from the non-`Option` output. Their
shared `preserves_uniqueness` body carries over verbatim as a block expression
over `self`.

The other twelve need no output modifier. `Result<Numeric, EvalError>`,
`Result<f64, EvalError>`, `Result<CheckedTimestamp<_>, EvalError>` and `String`
derive `Numeric { max_scale: None }`, `Float64`, `Timestamp`/`TimestampTz` with
`precision: None`, and `String` respectively, which is exactly what each
`output_sql_type` returned once the tail is applied.

Per-override accounting: `call` becomes the function body. `is_monotone` carries
over as a constant on the four precision functions and the two `date_trunc`
functions, and as an expression over `self` on `ExtractTimestamp` and
`ExtractTimestampTz`. `inverse` carries over on the two casts. The two
`AdjustTimestamp*Precision` impls overrode `inverse` with `None`, which is the
trait default, so the override disappears. `could_error` stays derived, true for
the `Result` outputs and false for the two `to_char` functions, matching impls
that never overrode it. `propagates_nulls` was never overridden and derives true
from the non-nullable inputs.

The four precision functions wrote a fixed string in `Display`, so `sqlname`
reproduces it. The other twelve format their payload, so they keep their
hand-written `Display` under `skip_display = true`.

`func_registry_records_sqlfunc_sources` used `extract_interval` as its example of
a function with no recorded source. It now uses `cast_array_to_array`, one of the
compound-type casts generic over an `Eval` implementor, a shape the macro does
not cover.

The registry reports only `sqlfunc_signature` moving, on all twenty-six records.
No declared property changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eleven implementations in `src/expr/src/scalar/func/impls/string.rs` move under
`#[sqlfunc]`. The four `impl<E: Eval> LazyUnaryFunc` blocks interleaved among
them are generic over a sub-expression type and are untouched.

`CastStringToNumeric`, `CastStringToTimestamp` and `CastStringToTimestampTz`
read their payload in both `call` and `output_sql_type`, so each takes a `&self`
receiver and an `output_type_expr` that builds the scale or precision out of
that payload. The expression sets nullability from the input, and the macro's
unary tail adds only `propagates_nulls && input_type.nullable`, which the
non-nullable `&str` input makes the same term. `introduces_nulls` derives false
from the non-`Option` outputs. Their `Display` wrote a fixed string, so
`sqlname` reproduces it, and `inverse` carries over as a modifier.

`CastStringToChar` and `CastStringToVarChar` need the same shape of
`output_type_expr`, plus `could_error = self.fail_on_len &&
self.length.is_some()`, which a `Result` output would otherwise derive as an
unconditional true. `CastStringToVarChar` also keeps `preserves_uniqueness` and
`is_eliminable_cast` as expressions over `self.length`. `CastStringToChar`'s
`is_eliminable_cast` returned the trait default, so the override goes and its
reason moves onto the function. Both format their payload, so they keep their
`Display` impls under `skip_display = true`.

`IsLikeMatch` and `IsRegexpMatch` need no output modifier: their
`output_sql_type` returned `Bool.nullable(input.nullable)`, which is what a
`bool` output plus the tail gives. Both read their payload in `call` and format
it in `Display`, so they take a `&self` receiver and `skip_display = true`.

`CastStringToInt2Vector`, `RegexpMatch` and `RegexpSplitToArray` were
hand-written `LazyUnaryFunc` implementations because they allocate into a
`RowArena` and check NULL in the body. `EagerUnaryFunc::call` supplies the
arena, and the NULL check moves into the input type. The first two take `&'a
str`, which is already non-nullable. `RegexpMatch` takes
`ExcludeNull<Datum<'a>>`, because `regexp_match_static` wants a raw `Datum` and
a bare `Datum` accepts NULL; `ExcludeNull` reports `nullable() == false`, so the
derived `propagates_nulls` is true, matching the declaration, and its
`try_from_result` turns `Datum::Null` into `Err(Ok(Datum::Null))`, which the
evaluation layer returns as `Ok(Datum::Null)`. `CastStringToInt2Vector` returns
`Int2Vector<'a>`, whose column type the macro derives, so it needs no output
modifier, and it reads no payload, so the macro generates its unit struct.
`RegexpMatch` and `RegexpSplitToArray` return a `Datum`, whose column type is
not statically known, so each carries an `output_type_expr`.
`RegexpSplitToArray` returns NULL only for a NULL input, so it declares
`introduces_nulls = false` over the `Datum` default. `RegexpMatch` returns NULL
when the pattern does not match, which is what the `Datum` default says.

`RegexpReplace` is the one `EagerBinaryFunc`. It reads `regex` and `limit` in
`call`, so it takes a `&self` receiver, and its `output_sql_type` spelled out
the macro's own binary tail minus the non-nullable-position term, which the
non-nullable `&str` inputs make redundant, so it needs no output modifier. Its
`Display` reads the pattern, the case sensitivity and the limit, so it keeps its
hand-written impl under `skip_display = true`.

`doc/developer/sqlfunc.md` named the three convertible `LazyUnaryFunc`
implementations that remained. There are none left, so the section now states
that the two shapes it describes account for every hand-written implementation
under `src/expr/src/scalar/func/`.

The registry reports only `sqlfunc_signature` moving, on all sixteen records.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@antiguru
antiguru force-pushed the sqlfunc-convert-int16 branch from 5cc444b to a0de7ee Compare October 14, 2026 19:12
@antiguru
antiguru merged commit a14c69b into main Oct 14, 2026
83 checks passed
@antiguru
antiguru deleted the sqlfunc-convert-int16 branch October 14, 2026 19:50
antiguru added a commit that referenced this pull request Oct 14, 2026
Fifteen `#[sqlfunc]` functions returned a bare `Datum` although each
always produces an array, a list, a map or a JSON value. Each therefore
restated its nullability with `introduces_nulls`. They now return
`Array<'a>`, `DatumList<'a>` or `DatumMap<'a>`, or an `Option` of one
where the function can return NULL, and the macro derives the value. The
shared helpers return the typed values too, and the two `jsonb_delete`
functions drop their `output_type_expr`.

The registry reports only `sqlfunc_signature` moving, on all fifteen
records.

Based on #39062.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
antiguru added a commit that referenced this pull request Oct 14, 2026
…d_from_record_list (#39814)

Two functions declared properties more conservatively than their bodies
need, inherited from hand-written impls and kept by their conversion to
`#[sqlfunc]` in #39062. This drops the pins and lets the macro derive
the properties from the output type.

* `record_get` returns a field of its input record and never produces an
`EvalError`, but it declared `could_error = true`, the `LazyUnaryFunc`
default. It now derives `false` from the non-`Result` output.
* `map_build_from_record_list` skips NULL records and keys and always
returns a map, but it declared `introduces_nulls` and `could_error` true
and a nullable output. Both flags now derive `false` from the `DatumMap`
output, and the output type is non-nullable.

The registry reports exactly these moves. The current `LIR_VERSION`
hasn't shipped, so the snapshots are regenerated in place. The
sqllogictest files that mention `record_get` show no plan changes, and
the sqllogictest shards passed on an earlier version of the stack that
carried the `map_build_from_record_list` change.

Based on #39105.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

2 participants