Skip to content

expr: Correct over-conservative properties of record_get and map_build_from_record_list - #39814

Merged
antiguru merged 2 commits into
sqlfunc-specific-output-typesfrom
sqlfunc-record-get-could-error
Oct 14, 2026
Merged

antiguru merged 2 commits into
sqlfunc-specific-output-typesfrom
sqlfunc-record-get-could-error

Conversation

@antiguru

@antiguru antiguru commented Oct 14, 2026 •

Copy link
Copy Markdown
Member

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

@antiguru
antiguru requested a review from a team as a code owner October 14, 2026 10:19
@antiguru
antiguru added this pull request to stack #38942 October 14, 2026 10:20
@antiguru
antiguru requested a review from mgree October 14, 2026 10:22
}
)]
fn record_get<'a>(&self, a: DatumList<'a>) -> Datum<'a> {
a.iter().nth(self.0).unwrap()

@antiguru antiguru Oct 14, 2026 •

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.

Claude, please file a linear issue for avoiding panics in function implementations to enforce invariants.

@antiguru antiguru Oct 14, 2026 •

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.

Filed as CPU-308. It covers this unwrap and the unreachable! in output_type_expr, the other panic sites under func/impls/, and the arity assert from #39061.

Posted by Claude Code.

@antiguru
antiguru force-pushed the sqlfunc-record-get-could-error branch from f74d198 to bf94aba Compare October 14, 2026 15:18
@antiguru
antiguru removed this pull request from stack #38942 October 14, 2026 15:45
@antiguru
antiguru force-pushed the sqlfunc-record-get-could-error branch from bf94aba to e94c12d Compare October 14, 2026 15:46
@antiguru
antiguru added this pull request to stack #39823 October 14, 2026 15:46
@antiguru antiguru changed the title expr: Mark RecordGet as unable to error expr: Derive the conservative properties of record_get and map_build_from_record_list Oct 14, 2026
@antiguru antiguru changed the title expr: Derive the conservative properties of record_get and map_build_from_record_list expr: Correct over-conservative properties of record_get and map_build_from_record_list Oct 14, 2026
@antiguru

Copy link
Copy Markdown
Member Author

This now also carries the map_build_from_record_list correction (e94c12d), which moved out of the map.rs conversion so that #39062 changes no declared properties. @ggevay, your approval predates it.

Posted by Claude Code.

@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. For posterity, https://linear.app/materializeinc/issue/CPU-308/avoid-panics-in-scalar-function-implementations-to-enforce-invariants documents the tension between panics and internal errors.

antiguru and others added 2 commits October 14, 2026 21:03
`record_get` returns a field of its input record and never produces an
`EvalError`, yet it declared `could_error = true`. The flag came over from
the hand-written implementation, whose `LazyUnaryFunc::could_error`
defaulted to true. Dropping it lets the macro infer `false` from the
non-`Result` output.

Registry: `record_get` changes `could_error` from true to false. The current
`LIR_VERSION` hasn't shipped, so the snapshots were regenerated in place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
`map_build_from_record_list` skips NULL records and NULL keys and always
returns a map, so it never returns NULL for a non-NULL input and never produces
an `EvalError`. It declared `introduces_nulls` and `could_error` true and a
nullable output anyway, because the hand-written impl did, and its conversion to
the sqlfunc macro kept those declarations. Dropping the pins lets the macro
derive both flags as false from the `DatumMap` output, and the
`output_type_expr` reports `nullable(false)`, leaving nullability to the unary
tail.

The registry reports `map_build_from_record_list` moving `introduces_nulls` and
`could_error` from true to false and its output type from nullable to
non-nullable. The current `LIR_VERSION` hasn't shipped, so the snapshots are
regenerated in place. The sqllogictest shards passed on an earlier version of
the stack that carried this change inside the `map.rs` conversion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@antiguru
antiguru force-pushed the sqlfunc-record-get-could-error branch from e94c12d to 0d5fecd Compare October 14, 2026 19:12
@antiguru
antiguru merged commit 2170a33 into main Oct 14, 2026
83 checks passed
@antiguru
antiguru deleted the sqlfunc-record-get-could-error branch October 14, 2026 19:50
antiguru added a commit that referenced this pull request Oct 14, 2026
…ro (#39062)

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](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@def-

def- commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review (Post Merge)

@antiguru — an automated review of commit 2170a3342c found the following potential MEDIUM+ issue(s) after this PR was merged.

1. HIGH -- Narrowing map_build's output to NOT NULL stalls existing Avro sinks on upgrade

src/expr/src/scalar/func/impls/map.rs:71

Changing map_build's declared output from .nullable(true) to .nullable(false) changes the persisted column type of every existing materialized view that calls map_build on a non-null list, for example the documented map_build(headers) pattern. On upgrade, Kafka sinks re-derive their Avro schema from that desc and publish a schema without the null branch. Confluent and Redpanda registries reject it under their default BACKWARD compatibility, so the sink stays stalled until someone intervenes by hand.

Details

At boot the MV desc is rebuilt from the plan (src/adapter/src/catalog/state.rs:1640), and the sink's Avro schema is regenerated from that desc (src/sql/src/plan/statement/ddl.rs:3341, src/interchange/src/json.rs:407). Persist accepts the narrowed desc because every Arrow column is nullable. That is why the break only shows up downstream, at the schema registry. Iceberg sinks follow the same path to the same failure: field_is_compatible requires required to match exactly (src/storage/src/sink/iceberg.rs:1000). The Iceberg case comes from reading the code. I did not run it.

Reproduced with plain docker, Redpanda's schema registry, and an in-place upgrade on a shared /mzdata:

CREATE TABLE t (k text NOT NULL, v text);
CREATE MATERIALIZED VIEW mv AS SELECT k, map_build(LIST[ROW('v', v)]) AS m FROM t;
CREATE SINK s FROM mv INTO KAFKA CONNECTION kc (TOPIC 'mzrv')
  FORMAT AVRO USING CONFLUENT SCHEMA REGISTRY CONNECTION csr ENVELOPE DEBEZIUM;
  • On v26.46.0, m is nullable and the registered field is ["null", {"type":"map",...}].
  • After upgrading to this commit's image, m is NOT NULL and mz_sink_statuses shows stalled: ... schema being registered is incompatible with an earlier schema. The sink retries every 5 s and keeps failing.
  • Control: the same upgrade to the parent commit's image (expr: Return specific output types instead of Datum #39105) leaves m nullable and the sink running.

Fix: keep this change's could_error = false, but leave map_build's declared output type nullable until MV column nullability is stable across upgrades. The record_get hunk does not change any output type and can stay as is.

     }
-    .nullable(false)
+    .nullable(true),
+    // NOTE: the body never returns NULL, but narrowing this changes the desc of existing
+    // materialized views, and Avro and Iceberg sinks on them reject the regenerated schema.
+    introduces_nulls = true
 )]

@antiguru

Copy link
Copy Markdown
Member Author

@def- Thanks, confirmed. #39843 restores the nullable map_build output and introduces_nulls = true, and keeps could_error = false and the record_get change.

Posted by Claude Code.

antiguru added a commit to antiguru/materialize that referenced this pull request Oct 15, 2026
because the body always returns a map. The output type becomes the column
type of every materialized view that calls `map_build`, and existing views
pick up the narrowed type when the catalog re-derives their description on
upgrade. A Kafka sink with Avro on such a view then regenerates its schema
without the `null` branch, and a schema registry under its default BACKWARD
compatibility rejects it, so the sink stalls. Iceberg sinks compare
nullability exactly and fail the same way.

This restores the nullable output and `introduces_nulls = true` and keeps
`could_error = false`, which changes no column type. The registry reports
`map_build_from_record_list` moving `introduces_nulls` back to true and its
output type back to nullable, the declarations before MaterializeInc#39814.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
antiguru added a commit that referenced this pull request Oct 15, 2026
#39814 narrowed `map_build`'s output type to NOT NULL. Existing
materialized views that call `map_build` pick up the narrowed column
type on upgrade, and a Kafka Avro sink on such a view then registers a
schema the registry rejects as incompatible, which stalls the sink. The
post-merge QA review on #39814 reproduced this against Redpanda's schema
registry.

This restores the nullable output and `introduces_nulls = true`, and
keeps `could_error = false`.

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

Co-authored-by: Claude Opus 5.5 <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.

4 participants