Skip to content

Refactor out inlined values in builtin materialized views - #39468

Merged
SangJunBak merged 8 commits into
mainfrom
jun/sql-653-static-builtin-values-in-mv-fingerprint
Oct 14, 2026
Merged

SangJunBak merged 8 commits into
mainfrom
jun/sql-653-static-builtin-values-in-mv-fingerprint

Conversation

@SangJunBak

Copy link
Copy Markdown
Contributor

I'd recommend this PR commit by commit. All of this is entirely a refactor and most is just moving code around.

Motivation

Closes sql-653

Description

Should have no customer facing changes. The biggest change is instead of inlining builtin values into the fingerprint, we can use our original pattern of using upstream "mz_builtin_*" views. Initially we thought we had to inline the builtin VALUES into the materialized view, otherwise a silent change to an upstream view (i.e. mz_builtin_indexes) would silently create an error in the materialized view. However we've since realized that as long as the schema of the upstream view stays the same, any updates to it will simply sink into the materialized view. If the schema were to change however, e.g. we ASSERT NOT NULL on the mv but have the upstream view provide a NULL value, then the mv would error when queried. However, we have tests that actually query each builtin so this regression would be caught.

Verification

A proof point of this working is mz_object_dependencies. It is an mv that references an upstream view mz_object_dependencies_raw that's constantly changing. This has been in production for a while but things are healthy due to what I stated prior.

@SangJunBak
SangJunBak added this pull request to stack #38849 October 2, 2026 00:20
@SangJunBak
SangJunBak requested a review from a team as a code owner October 2, 2026 00:20
@linear-code

linear-code Bot commented Oct 2, 2026

Copy link
Copy Markdown

SQL-653

@SangJunBak SangJunBak changed the title catalog: extract builtin index rows of mz_indexes into mz_builtin_ind… Refactor out inlined values in builtin materialized views Oct 2, 2026
@SangJunBak
SangJunBak requested review from mtabebe and removed request for mtabebe October 2, 2026 00:21
@SangJunBak SangJunBak added the ci-nightly PR CI control: also trigger Nightly label Oct 2, 2026
@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch from 3db1f61 to 03701c4 Compare October 2, 2026 03:32
@SangJunBak
SangJunBak requested a review from a team as a code owner October 2, 2026 03:32
@SangJunBak SangJunBak removed the ci-nightly PR CI control: also trigger Nightly label Oct 2, 2026
@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch from 03701c4 to 631ddf2 Compare October 2, 2026 04:40
@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch 2 times, most recently from 0ce5fa8 to a3430b2 Compare October 5, 2026 16:13
Base automatically changed from jun/sql-499-convert-item-metadata-tables-to-catalog-views to main October 6, 2026 03:01
@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch from a3430b2 to b3e9ac9 Compare October 6, 2026 03:02
@mtabebe
mtabebe self-requested a review October 6, 2026 14:10
@SangJunBak
SangJunBak removed request for a team October 6, 2026 16:52

@mtabebe mtabebe 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.

I want to make sure I understand this right.

The argument is that we don't need to add the VALUES into the fingerprint of the materialized view because (1) if the schema stays the same we have self correcting behaviour and (2) if the schema does change we may have errors and we will catch them in tests.

The (2) feels fuzzy to me. We will catch them in tests based on our catalog, but what if the customers have different catalogs? Can we be stronger in anyway?

Or are you saying the only errors possible through (2) are schema definition errors which do not depend on the data in the catalog?

Or maybe it is a different argument all together, that we are just moving around definitions?

I guess I'd just like some clarity.

let materialized_views: &'static BuiltinView =
Box::leak(Box::new(make_builtin_materialized_views(mv_iter)));
let tables: &'static BuiltinView = Box::leak(Box::new(make_builtin_tables(table_iter)));
Box::leak(Box::new(make_builtin_materialized_views(builtin_items)));

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.

Nice that we can simplify

FROM (VALUES {values}) AS v(oid, schema_name, name, type, privileges)"
FROM (VALUES {source_values}) AS v(oid, schema_name, name, type, privileges)
UNION ALL
SELECT oid, schema_name, name, 'log', privileges

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.

Nice 👍

@mtabebe

mtabebe commented Oct 7, 2026

Copy link
Copy Markdown
Contributor

I went through the 7 commits and the code does look good, I'm just trying to make sure my understanding of the error case is correct.

I also think this is a change where we should do a nightly run?

@SangJunBak SangJunBak added the ci-nightly PR CI control: also trigger Nightly label Oct 7, 2026
@SangJunBak

Copy link
Copy Markdown
Contributor Author

Or are you saying the only errors possible through (2) are schema definition errors which do not depend on the data in the catalog?

I was thinking this!

So the cases where I can see a regression happening are:

  1. An upstream view removes a referenced column

We would fail on boot which CI should catch when planning the builtins.

  1. An upstream view adds a referenced column

This would change the SQL of the mv, so the fingerprint mismatch in the 0dt smoke test would guide us to creating a shard migration.

  1. An upstream view changes the type of a referenced column

If the new type works with the original SQL of the mv but the byte representation changes (e.g. int32 to int64), then persist fails to register the RelationDesc and the upgrade fails.

However, we have a test called verify_builtin_descs that opens the catalog in debug mode and plans all our builtins then asserts nullability/types are the same as we declared them.. This would alert the user about type/nullability regressions when touching upstream objects and would force the user to update the desc of the builtin mv.

  1. An upstream view changes the nullability of a referenced column

If we said a column was not nullable in its desc but then an upstream view emits null, then the replica would panic on write after registration from the assert in src/repr/src/row/encode.rs:151:

assert!(
    !datum.is_null() || self.nullable,
    "tried pushing Null into non-nullable column"
);

There's also the case I said in a previous commit message:

e.g. we ASSERT NOT NULL on the mv but have the upstream view provide a NULL value, then the mv would error when queried. However, we have tests that actually query each builtin so this regression would be caught.

I've since realized though that we're not guaranteed to query all builtins in our SLTs. However, verify_builtin_descs would still catch this case.

  1. If a mv is defined as SELECT * from upstream_view and the order / values of the columns change

Because persist interprets columns by position and not name (repr/src/row/encode.rs:1516), if types of the column have a different encoding, we fail to register. However if it has the same encoding (e.g. we change timestamp to timestamptz, both 16-bytes), then we silently succeed.

Proposed solution:

Although verify_builtin_descs covers many of the cases, a developer could just change the desc of the builtin then not create a shard migration for the builtin mv, causing the errors decribed above. I think we should expand the assertion message in verify_builtin_descs where if it fails for a shard-based object, we assert that they should also create a migration for the object.

And in a follow up PR (my preference), we can change the fingerprint scheme for a builtin MV to be its desc rather than its SQL similar to BuiltinSource. I'd do it in this PR but because we're changing the fingerprint scheme, we're going to get a fingerprint mismatch for all MVs in the new update so we'll need to do a shard migration for all.

I can also change the commit message:

Initially we thought we had to inline the builtin VALUES into the materialized view, otherwise a silent change to an upstream view (i.e. mz_builtin_indexes) would silently create an error in the materialized view. However we've since realized that as long as the schema of the upstream view stays the same, any updates to it will simply sink into the materialized view.

to better explain why this is acceptable!

@SangJunBak
SangJunBak requested a review from mtabebe October 8, 2026 23:09
@mtabebe

mtabebe commented Oct 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for laying out these cases, can we put this comment somewhere in the code too?

A follow up PR for the fix seems fine to me.

@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch 3 times, most recently from 5fd4d3e to 1ecc7db Compare October 9, 2026 15:34

@mtabebe mtabebe 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.

I just looked at the last commit (1ecc7db) thanks for adding the comment 👍

@ggevay ggevay 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, two comment nits. The Nightly reds are pre-existing: the three orchestratord jobs are CLO-315 (red on main and the release tags too), and Parallel Workload (kill) hit SS-346's bootstrap panic after a kill, which also shows up on main.

Comment thread src/catalog/src/builtin.rs Outdated
Comment on lines +770 to +773
// 4. The upstream relation changes a referenced column's nullability: as in 3,
// `verify_builtin_descs` fails until `desc` is updated. If the shard keeps a
// non-nullable column and the relation emits NULL, the replica panics in
// `DatumEncoder::push` on write.

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.

Nit: this case can't reach DatumEncoder::push. The sink opens its writer with the planned desc, which becomes nullable as soon as the upstream emits NULL, and every boot evolves the MV's shard to it (evolve_nullability_for_bootstrap, coord.rs:3486). So widening needs no MigrationStep, and narrowing terminates envd at boot:

Suggested change
// 4. The upstream relation changes a referenced column's nullability: as in 3,
// `verify_builtin_descs` fails until `desc` is updated. If the shard keeps a
// non-nullable column and the relation emits NULL, the replica panics in
// `DatumEncoder::push` on write.
// 4. The upstream relation changes a referenced column's nullability: as in 3,
// `verify_builtin_descs` fails until `desc` is updated. Boot then evolves the
// shard to the new desc (`evolve_nullability_for_bootstrap`), which persist
// accepts for widening and rejects for narrowing, terminating envd.

Comment thread src/catalog/src/builtin/builtin.rs Outdated
Comment on lines +305 to +306
/// `mz_builtin_sources` reads the privileges for its log rows. See
/// `MZ_INDEXES` for what that means for migrations.

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.

Nit: MZ_INDEXES has no migration text; the explanation lives on the Fingerprint impl for BuiltinMaterializedView.

Suggested change
/// `mz_builtin_sources` reads the privileges for its log rows. See
/// `MZ_INDEXES` for what that means for migrations.
/// `mz_builtin_sources` reads the privileges for its log rows. See the
/// `Fingerprint` impl for `BuiltinMaterializedView` for what that means for migrations.

@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch from 1ecc7db to cb7dbd0 Compare October 13, 2026 17:38
@def-

def- commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- "Only nullability changed" exemption skips a needed MigrationStep when a column becomes non-nullable

src/adapter/src/catalog.rs:3080

The new verify_builtin_descs hint says a shard-backed builtin needs no MigrationStep if only nullability changed. The Fingerprint comment at src/catalog/src/builtin.rs:777 and :784 says the same. That holds when a column becomes nullable, but not when a column becomes non-nullable while the existing shard still holds NULLs in it. A developer who follows the hint in that case ships an upgrade that panics the mz_catalog_server replica in a loop.

Details

How it fails: suppose an upstream relation stops emitting NULL in a column, for example NULL AS x becomes a non-null expression. The planned MV desc turns non-nullable and the test prints the hint. Without a MigrationStep the old shard is reused. evolve_nullability_for_bootstrap accepts the new desc, because every top-level Arrow field is encoded as nullable (src/repr/src/row/encode.rs:1509), so the schema check sees no difference. The MV's self-correcting persist sink then retracts the old NULL rows. Its writer encodes with the new desc (src/compute/src/sink/materialized_view.rs:372), and DatumEncoder::push hits a hard assert! on NULL in a non-nullable column (src/repr/src/row/encode.rs:150). This happens after promotion, on every restart of the replica.

The fix is to limit the exemption to columns that become nullable. A column that becomes non-nullable still needs a Replacement step, because that is the only way to drop rows that would fail the new desc.

@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch from cb7dbd0 to 6638c19 Compare October 14, 2026 08:55
SangJunBak and others added 8 commits October 14, 2026 04:56
…exes

mz_indexes is a stable mz_catalog relation, so later changes to how
builtin indexes are reported must not touch its schema. Moving the
builtin index rows into their own mz_internal.mz_builtin_indexes view
lets that view be extended and reused while mz_indexes reads it like any
other upstream relation. The SQL of mz_indexes changes, so it gets a
replacement MigrationStep.

Reading the rows from a view instead of inlining them as VALUES means
the mz_indexes fingerprint, which is its SQL, no longer changes when the
set of builtin indexes changes. That is fine: adding or removing a
builtin index changes the rows of mz_builtin_indexes but not its schema,
and row changes flow through the mz_indexes dataflow like any other
input. What the fingerprint can no longer see is a change to the shape
of the upstream view. The ways that can regress a builtin MV, and what
catches each:

1. The upstream view drops a referenced column: planning the MV fails at
   catalog open, which every test that opens a catalog hits.
2. The upstream view adds a column that the MV is changed to reference:
   the SQL changes, so the fingerprint changes, and update_fingerprints
   panics at upgrade until a MigrationStep is added.
3. The upstream view changes a referenced column's type: the planned
   desc changes and verify_builtin_descs fails until the declared desc
   is updated. The fingerprint does not change, so without a
   MigrationStep the coordinator evolves the existing shard to the new
   desc at bootstrap (evolve_nullability_for_bootstrap). Persist refuses
   when the Arrow encoding differs (int32 to int64), which terminates
   environmentd, and cannot tell when the encoding is shared (timestamp
   to timestamptz), which silently reinterprets the stored bytes.
4. The upstream view changes a referenced column's nullability. For a
   column under ASSERT NOT NULL, which covers every column of
   mz_indexes, the planner keeps it non-nullable, so verify_builtin_descs
   passes and the MV instead errors at runtime with MustNotBeNull once a
   NULL arrives. Only a test that queries the MV catches that. For other
   columns verify_builtin_descs fails until the declared desc is
   updated, and the shard then evolves at bootstrap without a
   MigrationStep.
5. The MV is SELECT * over the upstream view, which reorders or swaps
   columns: persist matches columns by position, so this is case 3 for
   each moved column.

verify_builtin_descs plans every builtin against a debug catalog and
compares the result with the declared desc, and its failure message
names the MigrationStep requirement for shard-backed builtins.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…indexes

- Because we don't need to inline VALUES into the DDLs of our builtin materialized views, we can create a new mz_builtin_log_indexes such that both mz_sources and mz_indexes can query it rather than get sent an iterator of the builtin logs.
- We simply extract inline VALUES code into a new builtin view called mz_builtin_logs then reference it in mz_builtin_indexes
- This comes with the nice benefit of not having to dynamically generate each mz_indexes and define the order statically.
- We can also turn make_mz_indexes into a static MZ_INDEXES like the rest of our builtins
- In a later commit, we'll making the same refactor for mz_sources
- In mz_sources, rather than inline the builtin sources and logs as a VALUES list, read them from mz_builtin_sources
- mz_builtin_sources already existed inside "make_builtin_sources", however it was just never used and was dead code. This is why we delete code in make_mz_sources but don't see a positive diff in this commit. However if you were to look inside make_builtin_sources, the same code we deleted exists there.
…tin_log_indexes

- Move `privileges` to `mz_builtin_log_indexes` from `mz_builtin_sources` 
- Instead of embedding the builtin logs to mz_sources, just join on `mz_builtin_log_indexes`
Simply move make_mz_sources to a static mz_sources to remove an unnecessary function call.
Given we're not sharing iterators across these view generators, we can simplify the code further.
A builtin materialized view's fingerprint is its SQL, so a change to the
shape of a relation it reads is not seen by the fingerprint check at
upgrade. Record the ways that can regress a builtin MV, and what catches
each, at the Fingerprint impl. Notably a nullability change in a column
under ASSERT NOT NULL is invisible to verify_builtin_descs, because the
planner keeps the asserted column non-nullable, and only shows up as a
runtime MustNotBeNull error on the MV.

verify_builtin_descs is the check that catches the type and column-order
cases, but a developer who fixes the declared desc it complains about
still has to add a MigrationStep for a shard-backed builtin. Say so in
its failure messages for tables, sources and materialized views, and
check the column count before zipping so a SELECT * that picked up a new
column fails with the same guidance instead of a bare zip_eq panic. A
keys-only mismatch gets its own message without the hint, since keys
never reach the shard schema.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@SangJunBak
SangJunBak force-pushed the jun/sql-653-static-builtin-values-in-mv-fingerprint branch from 6638c19 to e5236b6 Compare October 14, 2026 08:56
@SangJunBak
SangJunBak merged commit 1ff0254 into main Oct 14, 2026
342 of 347 checks passed
@SangJunBak
SangJunBak deleted the jun/sql-653-static-builtin-values-in-mv-fingerprint branch October 14, 2026 10:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-nightly PR CI control: also trigger Nightly

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants