Skip to content

Add ItemColumn and IndexColumn durable collections - #39818

Merged
SangJunBak merged 8 commits into
mainfrom
jun/durable-column
Oct 28, 2026
Merged

SangJunBak merged 8 commits into
mainfrom
jun/durable-column

Conversation

@SangJunBak

@SangJunBak SangJunBak commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor
  • Adds durable catalog code needed for replacing mz_columns and mz_index_columns. The next PR in the stack will be defining these views using the durable state.
  • See commit messages for details

Motivation

Part of SQL-494

Verification

  • Created rust unit tests and also a platform check test across upgrades in a followup PR

@SangJunBak
SangJunBak added this pull request to stack #39819 October 14, 2026 14:41
@linear-code

linear-code Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

SQL-494

SQL-703

@SangJunBak
SangJunBak marked this pull request as ready for review October 14, 2026 14:46
@SangJunBak
SangJunBak requested a review from a team as a code owner October 14, 2026 14:46
@def-

def- commented Oct 14, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Bootstrap refresh records columns from state before the boot's item migrations are applied to it

src/adapter/src/catalog/migrate.rs:260

refresh_durable_item_metadata(state, tx) runs after rewrite_sources_to_tables / rewrite_items have staged item rewrites in tx, but before state.apply_updates(op_item_updates, ..) (line 264) loads those rewrites into state. Any item whose definition a migration changes on this boot therefore gets its pre-migration columns written durably. The rows stay wrong until the next restart, which breaks the contract in the ItemColumn doc (src/catalog/src/durable/objects.rs:655) that bootstrap leaves the rows matching the running planner.

Details

A path that hits this exists today. With force_source_table_syntax on, rewrite_sources_to_tables turns each old-style multi-output source (Postgres, MySQL, SQL Server, and the TPCH/Auction/Marketing load generators) into a CREATE VIEW over the progress collection under the same CatalogItemId. The function's own comment says this "changes the schema of the object with source_item.id". At line 260, state still holds the old source with its empty desc, so set_item_metadata records zero columns for that id, while the in-memory catalog plans the view with the progress columns.

For a 0dt upgrade, the follower's savepoint run doesn't persist anything. The promoted leader then reboots writable, runs the migration again, and writes the stale rows durably. That process then serves until the next restart. Once the follow-up serves mz_columns from these rows, the migrated objects show no columns for that whole window. The call also sits right under "Add whole-catalog migrations below", so any new migration that changes an item's columns in that block or in rewrite_items will hit the same staleness.

Fix: move the call after the state.apply_updates(op_item_updates, local_expr_cache) at line 264, so it reads the post-migration state. The writes have no in-memory counterpart, so they need no further apply. They are committed with the rest of the bootstrap transaction in open.rs.

Comment on lines +302 to +305
pub nullable: bool,
pub type_oid: u32,
pub type_mod: i32,
pub custom_type: Option<CatalogItemId>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Rather than the SQLScalarType, I decided to put the computed derived state since I didn't want to embed all of SQLScalarType into the durable catalog

@SangJunBak
SangJunBak removed this pull request from stack #39819 October 15, 2026 19:41
@SangJunBak
SangJunBak added this pull request to stack #39878 October 15, 2026 19:42
@SangJunBak
SangJunBak force-pushed the jun/durable-column branch 2 times, most recently from 1d28288 to ce75aca Compare October 16, 2026 20:28
@aljoscha

Copy link
Copy Markdown
Contributor

Here's a couple comments from an LLM review. I haven't looked at the code yet, but the suggestion make sense if the current behavior or coverage is actually as they describe. 😅

-- AI below --

Blocking: I found no concrete correctness blocker in the current diff. The existing review comment about refreshing pre-migration state is addressed by the current placement in open.rs.

Strong suggestion: test stale metadata, not just missing metadata. The bootstrap test deletes two rows and verifies they are restored. It does not exercise the important upgrade promise: an existing row has the wrong nullability/type, or the new planner produces fewer columns. The durable-layer test covers replacement and truncation separately, but a bootstrap test with deliberately stale rows would connect those pieces.

Nit: truncate_item_metadata relies on undocumented contiguous positions. It deletes consecutive positions until the first missing key. That works for the current adapter producer, which uses enumerate(), but set_item_metadata accepts arbitrary positions without stating that constraint. With rows at positions 0 and 2, dropping the item leaves position 2 behind. I would document or assert the contiguous-position contract rather than treat this as a demonstrated normal-operation bug.

@SangJunBak
SangJunBak requested a review from aljoscha October 19, 2026 19:00
@SangJunBak

Copy link
Copy Markdown
Contributor Author

@SangJunBak
SangJunBak removed this pull request from stack #39878 October 19, 2026 22:15
@SangJunBak
SangJunBak added this pull request to stack #40002 October 19, 2026 22:15
@SangJunBak SangJunBak changed the title Add ItemColumn and IndexColumn durable collections sql-703: Convert mz_columns to a builtin materialized view Oct 19, 2026
@SangJunBak SangJunBak changed the title sql-703: Convert mz_columns to a builtin materialized view Add ItemColumn and IndexColumn durable collections Oct 19, 2026
@SangJunBak
SangJunBak removed this pull request from stack #40002 October 20, 2026 20:51
@SangJunBak
SangJunBak added this pull request to stack #40040 October 20, 2026 20:51
@def-

def- commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

QA LLM Review

1. MEDIUM -- Every catalog transaction now rebuilds and decodes one entry per column of every relation

src/catalog/src/durable/transaction.rs:240

Each catalog transaction rebuilds the full proto Snapshot from the in-memory trace and converts it to Rust types, so its cost scales with the number of catalog entries. This PR adds an entry for every column of every relation and every index key, which is typically several times the number of items, and every DDL statement pays the extra cost on the coordinator loop. Measured with the optimized profile on 20k items with 10 columns each, transaction() goes from 6.7 ms to 66 ms and a minimal transaction plus commit from 9 ms to 100 ms.

Details

PersistHandle::with_snapshot (src/catalog/src/durable/persist.rs:761) walks the entire trace and clones every key and value into fresh BTreeMaps. Transaction::new then runs from_proto over each collection, which for item_columns means allocating a ColumnName per row. Catalog::transact opens one such transaction per DDL (src/adapter/src/catalog/transact.rs:740), and each statement inside an explicit DDL transaction also deep-clones the snapshot (src/adapter/src/coord/ddl.rs:312). None of these paths reads the new rows, except set_item_metadata, which only needs the rows of the item being written.

Other measurements, all optimized: at 5k items × 20 columns, transaction() goes from 1.5 ms to 22 ms and transaction plus commit from 1.9 ms to 38 ms. At 5k × 10 they are 1.4 → 10 ms and 1.9 → 19 ms. That works out to about 0.3 µs of transaction() time per column row, plus about as much again on commit. The probe's items have short create_sql and no privileges, so with real items the ratio would be smaller, but the added time per DDL would be the same.

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

The shape of this looks good, but what Dennis found here is concerning: #39818 (comment). Can we fix that?

@SangJunBak

Copy link
Copy Markdown
Contributor Author

@aljoscha @def- Implemented a version where instead of keying on the object and position, we key on just the object then store a vec for the columns. That way it should scale with the number of objects. Repeated the same benchmark and found:

┌─────────────┬──────────────────┬───────────────┬──────────────┬─────────────┐
│  Items ×    │      Metric      │   Old (no     │ Per-column   │  Per-item   │
│   columns   │                  │ column rows)  │     rows     │    rows     │
├─────────────┼──────────────────┼───────────────┼──────────────┼─────────────┤
│ 20k × 10    │ transaction()    │ 4.07 ms       │ 27.5 ms      │ 9.10 ms     │
│             │                  │               │ (6.8x)       │ (2.2x)      │
├─────────────┼──────────────────┼───────────────┼──────────────┼─────────────┤
│ 20k × 10    │ transaction +    │ 6.08 ms       │ 54.5 ms      │ 14.1 ms     │
│             │ commit           │               │ (9.0x)       │ (2.3x)      │
├─────────────┼──────────────────┼───────────────┼──────────────┼─────────────┤
│ 5k × 20     │ transaction()    │ 0.87 ms       │ 14.4 ms      │ 2.96 ms     │
│             │                  │               │ (16.6x)      │ (3.4x)      │
├─────────────┼──────────────────┼───────────────┼──────────────┼─────────────┤
│ 5k × 20     │ transaction +    │ 1.34 ms       │ 26.4 ms      │ 4.58 ms     │
│             │ commit           │               │ (19.7x)      │ (3.4x)      │
├─────────────┼──────────────────┼───────────────┼──────────────┼─────────────┤
│ 5k × 10     │ transaction()    │ 0.87 ms       │ 6.64 ms      │ 2.22 ms     │
│             │                  │               │ (7.6x)       │ (2.6x)      │
├─────────────┼──────────────────┼───────────────┼──────────────┼─────────────┤
│ 5k × 10     │ transaction +    │ 1.34 ms       │ 11.5 ms      │ 3.55 ms     │
│             │ commit           │               │ (8.6x)       │ (2.6x)      │
└─────────────┴──────────────────┴───────────────┴──────────────┴─────────────┘

Still worse, but maybe a price we pay? I'll push the commits with the schema changes after this comment.

@aljoscha

Copy link
Copy Markdown
Contributor

Yeah, I think that' okay. I think we have to independently tackle that problem that we do touch all this catalog content for a snapshot/transaction, which is a pre-existing problem.

@SangJunBak
SangJunBak force-pushed the jun/durable-column branch 2 times, most recently from ec2e0b2 to e0b0f9e Compare October 23, 2026 11:43
@SangJunBak

Copy link
Copy Markdown
Contributor Author

@aljoscha Ah sorry forgot to push! Also rebased off main too. Here are the PRs and the commits that do the change (they should each be on top).

PR #39818, Add ItemColumn and IndexColumn durable collections: #39818

  • catalog: remove position from the key of item columns and index keys

PR #40001, Convert mz_columns to a builtin materialized view: #40001

  • catalog: read item columns from one row per item in mz_columns
  • test: update expected outputs for item columns stored one row per item

PR #39427, Convert mz_index_columns to a builtin materialized view: #39427

  • catalog: read index keys from one row per index in mz_index_columns
  • test: update expected outputs for index keys stored one row per index

@SangJunBak
SangJunBak requested a review from aljoscha October 23, 2026 11:49

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

sounds good then!

@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

SangJunBak and others added 8 commits October 27, 2026 16:11
All of this commit is boilerplate. The durable catalog gains two collections: ItemColumn, one row per column of a
relation, and IndexColumn, one row per key of an index. Both will be used by mz_columns and mz_index_columns. We cannot easily derive these values from each object's DDL statements, thus we store its column information durably/
For columns and index columns, we implement a setter `set_item_metadata` and a cleanup method `truncate_item_metadata` such that for every update to an item, we update an item's column information durably. The goal is to remove the code we have in builtin_table_updates for our builtin column relations.
initialize_state records the columns and index keys of every item as the
running build's planner produces them, right after our builtin table updates.

We introduce a getter `is_read_only` such that the catalog in debug mode doesn't fail when we refresh the column information. We can't use config.read_only since this variable actually is intended for the read only 0dt follower process, which opens the catalog in savepoint mode.
The rows are written with the item, replaced as a whole, and dropped with
it by either removal path.
We'll need this for a future that tests stale metadata
Although we tested before for missing column information, we don't test if an existing row has the wrong nullability/type, or if the new planner produces fewer columns. Thus we create a new test for these cases.
truncate_item_metadata would assume that column positions are contiguous but we never explicitly call out this contract. Thus we document this contract in truncate_item_metadata and soft assert on callers of it.
Because transactions scale with the number of keys in the catalog, by having the column position in the key, we negatively impacted performance by a fair bit. By storing a vector of the columns and keying by the catalog ID, we notice only a ~2x regression on transactions rather than a 9-15x regression. 

Much of this commit is boilerplate/updating tests. I did make some changes to change the voice of some of the comments to my own. I also simplified the truncation code since now we can just remove by the catalog id rather than iterating over (catalog_id, position) and deleting column information that way.
@SangJunBak
SangJunBak merged commit 3ad0896 into main Oct 28, 2026
85 checks passed
@SangJunBak
SangJunBak deleted the jun/durable-column branch October 28, 2026 01:31
SangJunBak added a commit that referenced this pull request Oct 28, 2026
From the durable column information we recorded in
#39818, we derive the
column information mz_columns and convert it to a builtin mv.

See commit messages for more details

### Motivation

Part of sql-494

### Description

We report the column defaults from a create_sql statement, generate the
mz_builtin_columns for our builtin collections since they don't exist in
our catalog, then we convert mz_columns to a builtin mv using a
combination of mz_builtin_columns and mz_catalog_raw.

### Verification
- Expanded the existing SLT with a more thorough test suite that asserts
against a column of each type in our system.
- Diffed this relation using the prod analytics workload replay against
main and this branch.
SangJunBak added a commit that referenced this pull request Oct 28, 2026
From the durable index key information we recorded in
#39818, we derive the
index key information from it and convert mz_index_columns to a builtin
mv.

See commit messages for more details

### Motivation

Part of sql-494

### Verification
- Diffed this relation using the prod analytics workload replay against
main and this branch.
- Created an SLT for index columns that assert expected behavior as well
as behavior for indexes where the keys are varying expressions
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