Repository navigation
Convert mz_columns to a builtin materialized view - #40001
Conversation
adf85f8 to
07eab27
Compare
07eab27 to
1bb5e56
Compare
QA LLM Review1. HIGH -- A column named
|
6363439 to
5eaa4e4
Compare
5eaa4e4 to
48e3b69
Compare
|
I've re-run three jobs from nightly 18550 on this head (889c34a), because DDL looked much slower there than at the previous head or on main:
The code change between the two heads is small, so this may be the agents, and the re-run on fresh agents should tell us: testdrive, SLT, workload replay. I'll follow up with the results in my review. |
aljoscha
left a comment
There was a problem hiding this comment.
Did a first pass, looks good, but would be good to have @ggevay take a look.
I did have some inline questions. Also, that change from a string "NULL" to SQL NULL, have to think about if we want to do that. It feels correct, but it's a change, so ... 🤷♂️
| def validate(self) -> Testdrive: | ||
| return Testdrive(dedent(""" | ||
| # On releases before 26.48 mz_columns is a table the adapter packs, | ||
| # which reports the string NULL for a table column without a default. |
There was a problem hiding this comment.
weird that we had this ...
There was a problem hiding this comment.
Yeah I thought so too! Re:
Also, that change from a string "NULL" to SQL NULL, have to think about if we want to do that. It feels correct, but it's a change, so ...
Gabor suggested changing it to a SQL NULL here https://linear.app/materializeinc/issue/SQL-494/convert-mz-columns-and-mz-index-columns-to-catalog-views#comment-839801c2, mainly to keep pg parity
| .. | ||
| } => { | ||
| let entry = self.get_entry(custom_id); | ||
| // NOTE(benesch): the `mz_columns.type text` field is |
There was a problem hiding this comment.
did we change anything about this issue here?
| // A user relation's columns are the `ItemColumn` rows its catalog item | ||
| // records. | ||
| // | ||
| // NOTE(benesch): the `mz_columns.type text` field is |
There was a problem hiding this comment.
ah okay, that resolves my earlier question ... 😅
| // we replace `mz_type_name` with a join on `mz_builtin_types`. | ||
| let sql = format!( | ||
| " | ||
| IN CLUSTER mz_catalog_server |
There was a problem hiding this comment.
now I'm out of my depth, might be good to have @ggevay take a look at the SQL at least, to make sure it's good, I'm still reviewing the rest of the PR
| query I | ||
| SELECT count(DISTINCT(1)) FROM pg_attrdef | ||
| ---- | ||
| 1 |
There was a problem hiding this comment.
what's up with these changes?
There was a problem hiding this comment.
Ah sorry, should've been more clear in the commit message. This is a result of replacing the string 'NULL' with a SQL NULL. The motivation was brought up by Gabor https://linear.app/materializeinc/issue/SQL-494/convert-mz-columns-and-mz-index-columns-to-catalog-views#comment-839801c2
| order by | ||
| attr.attnum; | ||
| ---- | ||
| c_int4 NULL false true false true 0 base int4 false |
There was a problem hiding this comment.
what's up with these change?
There was a problem hiding this comment.
Same motivation #40001 (comment). Here the "hasDefault" column turns to false which makes sense since if you look at the table definition, they have no column defaults.
3babd8d to
7a28b4a
Compare
QA LLM Review1. HIGH -- One wide temporary view OOM-loops mz_catalog_server, because mz_columns copies each relation's whole ItemColumns row onto every one of its columns
DetailsIn the plan in Repro: local Fix: filter and project before unnesting, and unnest the defaults separately so no per-column row carries an O(N) value: item_columns AS (
SELECT
mz_internal.parse_catalog_id(data->'key'->'id') AS id,
data->'value'->'columns' AS columns
FROM mz_internal.mz_catalog_raw
WHERE data->>'kind' = 'ItemColumns'
),
defaults AS (
SELECT i.id, d.ordinality, d.value AS "default"
FROM items AS i,
jsonb_array_elements_text(i.column_defaults) WITH ORDINALITY AS d(value, ordinality)
),
2. LOW --
|
7a28b4a to
e42ed13
Compare
35e5c82 to
9b17d9d
Compare
| Builtin::MaterializedView(&MZ_DATABASES), | ||
| Builtin::MaterializedView(&MZ_SCHEMAS), | ||
| Builtin::Table(&MZ_COLUMNS), | ||
| Builtin::MaterializedView(&MZ_COLUMNS), |
There was a problem hiding this comment.
mz_columns is in mz_catalog, so misc/python/materialize/checks/all_checks/builtin_version_pin.py should record the conversion: "mz_columns": MzVersion.parse_mz("v26.49.0"), or the release this ends up in. It's None there today. The same goes for mz_index_columns in #39427.
| user_columns AS ( | ||
| SELECT | ||
| c.id, | ||
| col.value->>'name' AS name, | ||
| -- WITH ORDINALITY numbers the list from one, which is what position reports | ||
| col.ordinality::uint8 AS position, | ||
| (col.value->>'nullable')::bool AS nullable, | ||
| coalesce(custom.name, mz_internal.mz_type_name((col.value->>'type_oid')::oid), 'unknown') AS type, | ||
| d.default, | ||
| coalesce(custom.oid, (col.value->>'type_oid')::oid) AS type_oid, | ||
| (col.value->>'type_mod')::int4 AS type_mod | ||
| FROM | ||
| item_columns AS c | ||
| JOIN items AS i ON i.id = c.id | ||
| CROSS JOIN LATERAL jsonb_array_elements(c.columns) WITH ORDINALITY AS col(value, ordinality) |
There was a problem hiding this comment.
On a synthetic catalog with 14k items and 324k columns, the mz_columns dataflow holds 165 MB of arrangements on a 1-worker mz_catalog_server. The old table's index was 12 MB, and it stays as mz_columns_ind. Most of the 165 MB is per-column state: the JSON value of every column is carried through the delta join's arrangements, and the defaults join adds more.
Extracting the scalar fields right after the unnest halves it. I ran this as a user MV over mz_catalog_raw next to an exact copy of the user branch here. The rows are identical and the size is 166 → 86 MB:
cols AS (
SELECT
c.id,
col.ordinality,
col.value->>'name' AS name,
(col.value->>'nullable')::bool AS nullable,
(col.value->>'type_oid')::oid AS type_oid,
(col.value->>'type_mod')::int4 AS type_mod,
CASE
WHEN jsonb_typeof(col.value->'custom_type') = 'object'
THEN mz_internal.parse_catalog_id(col.value->'custom_type')
END AS custom_type
FROM
item_columns AS c
CROSS JOIN LATERAL jsonb_array_elements(c.columns) WITH ORDINALITY AS col(value, ordinality)
)The rest of user_columns then joins cols to items, custom and defaults as it does today.
parse_catalog_create_sql reports a table's DEFAULT clauses by column name. If it doesn't have a default, we provide None which gets replaced with a SQL null. The behavior before was a string 'NULL' for tables and a SQL NULL for any other type of relation. This is actually different from postgres, thus we correct it now.
The generated view lists the columns of every builtin relation, so that catalog views can reference the column information of builtin items in SQL.
We use the durable catalog and mz_builtin_columns to derive mz_columns. We also remove the builtin table code.
We add SLTs surrounding mz_columns to ensure: - The columns report what we expect them to - We test a column of each different type -
The ItemColumns platform check creates relations with column and expression keys in every phase, adds columns with ALTER TABLE, and comments on columns. It then checks what mz_columns and mz_index_columns report and that the reported positions line up with the positions in mz_comments.
A table column without a default now reads as SQL NULL in mz_columns, so pg_attrdef has no rows in cockroach/select_search_path.slt. This is consistent with Postgresql
For column defaults, we were originally exposing it as a JSON map keyed by the column's name. However, this could cause an error if a user had a column named after the magic internal key `$serde_json::private::Number`. The magic internal key is what turns a string into a number. For example, {"numField": {"$serde_json::private::Number": "5"}} would say that numField has a numeric value of 5. However if we had { "column_defaults": {"$serde_json::private::Number": "5", "b": "'x'", "c": null}}, then we'd get an error "invalid length 3" and cause the catalog server to go into a panic loop.
We now make `column_defaults` an array and index on position to avoid the map entirely.
Addresses the QA review on PR #40001:
#40001 (comment)
The `ItemColumns` row of a relation holds its column list, so the view unnests it with `WITH ORDINALITY`, whose one-based numbering is the reported position, and the column defaults are read by that position.
- By doing all the JSONB extraction in a CTE, we join on the scalars from the json rather than the JSON values, halving the memory necessary for maintenance. This is because otherwise, the `value` JSON of every column was carried through the delta join's arrangement. - Also records the mz_columns conversion in the builtin version pin check.
9b17d9d to
ee15d8a
Compare
mz_columns.default reads as the string NULL for a table column without a default, as it did in every release through v26.48. #40001 made it SQL NULL, which the Terraform provider (every release through v0.11.12) compares against its declared default of "NULL": it then replaces the table on every apply and drops its rows (SQL-839). Builtin tables read the string too, so the contract holds for every table and not only for user tables. The PostgreSQL-compatible views keep the behavior #40001 gave them by mapping the string to no default: pg_attribute.atthasdef is false, pg_attrdef has no row and information_schema.columns.column_default is NULL for such a column. Closes: SQL-839 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Before, tables whose columns' defaults were undefined we'd store their `mz_columns` default value as the SQL string 'NULL' but would store it as NULL for other relation objects. In #40001 , we decided to unify the behavior and store it as NULL for tables too. However, this created a regression in our terraform provider where it'd drop and create tables with no defaults on their columns because of a schema mismatch. Thus we change it back to the SQL string `NULL`. ### Motivation Closes: [SQL-839](https://linear.app/materializeinc/issue/SQL-839) ### Verification We still get pg compatibility by just changing the definition of the information_schema / pg views. This is why we don't need to update any of the SLT tests related to those views. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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