Skip to main content

mz_adapter/catalog/
builtin_table_updates.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10mod notice;
11
12use bytesize::ByteSize;
13use ipnet::IpNet;
14use mz_adapter_types::compaction::CompactionWindow;
15use mz_audit_log::VersionedStorageUsage;
16use mz_catalog::SYSTEM_CONN_ID;
17use mz_catalog::builtin::{
18    BuiltinTable, MZ_AGGREGATES, MZ_ARRAY_TYPES, MZ_BASE_TYPES, MZ_CLUSTER_REPLICA_SIZE_INTERNAL,
19    MZ_CLUSTER_REPLICA_SIZES, MZ_COLUMNS, MZ_EGRESS_IPS, MZ_FUNCTIONS,
20    MZ_HISTORY_RETENTION_STRATEGIES, MZ_INDEX_COLUMNS, MZ_LICENSE_KEYS, MZ_LIST_TYPES,
21    MZ_MAP_TYPES, MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES, MZ_OBJECT_GLOBAL_IDS, MZ_OPERATORS,
22    MZ_PSEUDO_TYPES, MZ_REPLACEMENTS, MZ_ROLE_AUTH, MZ_SESSIONS, MZ_SOURCE_REFERENCES,
23    MZ_STORAGE_USAGE_BY_SHARD, MZ_SUBSCRIPTIONS, MZ_TYPE_PG_METADATA, MZ_TYPES,
24    MZ_WEBHOOKS_SOURCES,
25};
26use mz_catalog::durable::SourceReferences;
27use mz_catalog::memory::error::Error;
28use mz_catalog::memory::objects::{
29    CatalogEntry, CatalogItem, DataSourceDesc, Func, Index, MaterializedView, Table,
30    TableDataSource, Type,
31};
32use mz_expr::MirScalarExpr;
33use mz_license_keys::ValidatedLicenseKey;
34use mz_orchestrator::{CpuLimit, DiskLimit, MemoryLimit};
35use mz_ore::cast::CastFrom;
36use mz_ore::collections::CollectionExt;
37use mz_persist_client::batch::ProtoBatch;
38use mz_repr::adt::array::ArrayDimension;
39use mz_repr::adt::interval::Interval;
40use mz_repr::adt::jsonb::Jsonb;
41use mz_repr::adt::mz_acl_item::PrivilegeMap;
42use mz_repr::refresh_schedule::RefreshEvery;
43use mz_repr::role_id::RoleId;
44use mz_repr::{
45    CatalogItemId, Datum, Diff, GlobalId, ReprColumnType, Row, RowPacker, SqlScalarType, Timestamp,
46};
47use mz_sql::ast::{CreateIndexStatement, Statement};
48use mz_sql::catalog::{CatalogType, TypeCategory};
49use mz_sql::func::FuncImplCatalogDetails;
50use mz_sql::names::SchemaSpecifier;
51use mz_sql_parser::ast::display::AstDisplay;
52use mz_storage_client::client::TableData;
53use smallvec::smallvec;
54use uuid::Uuid;
55
56// DO NOT add any more imports from `crate` outside of `crate::catalog`.
57use crate::active_compute_sink::ActiveSubscribe;
58use crate::catalog::CatalogState;
59use crate::coord::ConnMeta;
60
61/// An update to a built-in table.
62#[derive(Debug, Clone)]
63pub struct BuiltinTableUpdate<T = CatalogItemId> {
64    /// The reference of the table to update.
65    pub id: T,
66    /// The data to put into the table.
67    pub data: TableData,
68}
69
70impl<T> BuiltinTableUpdate<T> {
71    /// Create a [`BuiltinTableUpdate`] from a [`Row`].
72    pub fn row(id: T, row: Row, diff: Diff) -> BuiltinTableUpdate<T> {
73        BuiltinTableUpdate {
74            id,
75            data: TableData::Rows(vec![(row, diff)]),
76        }
77    }
78
79    pub fn batch(id: T, batch: ProtoBatch) -> BuiltinTableUpdate<T> {
80        BuiltinTableUpdate {
81            id,
82            data: TableData::Batches(smallvec![batch]),
83        }
84    }
85}
86
87impl CatalogState {
88    pub fn resolve_builtin_table_updates(
89        &self,
90        builtin_table_update: Vec<BuiltinTableUpdate<&'static BuiltinTable>>,
91    ) -> Vec<BuiltinTableUpdate<CatalogItemId>> {
92        builtin_table_update
93            .into_iter()
94            .map(|builtin_table_update| self.resolve_builtin_table_update(builtin_table_update))
95            .collect()
96    }
97
98    pub fn resolve_builtin_table_update(
99        &self,
100        BuiltinTableUpdate { id, data }: BuiltinTableUpdate<&'static BuiltinTable>,
101    ) -> BuiltinTableUpdate<CatalogItemId> {
102        let id = self.resolve_builtin_table(id);
103        BuiltinTableUpdate { id, data }
104    }
105
106    pub(super) fn pack_role_auth_update(
107        &self,
108        id: RoleId,
109        diff: Diff,
110    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
111        let role_auth = self.get_role_auth(&id);
112        let role = self.get_role(&id);
113        BuiltinTableUpdate::row(
114            &*MZ_ROLE_AUTH,
115            Row::pack_slice(&[
116                Datum::String(&role_auth.role_id.to_string()),
117                Datum::UInt32(role.oid),
118                match &role_auth.password_hash {
119                    Some(hash) => Datum::String(hash),
120                    None => Datum::Null,
121                },
122                Datum::TimestampTz(
123                    mz_ore::now::to_datetime(role_auth.updated_at)
124                        .try_into()
125                        .expect("must fit"),
126                ),
127            ]),
128            diff,
129        )
130    }
131
132    pub(super) fn pack_item_update(
133        &self,
134        id: CatalogItemId,
135        diff: Diff,
136    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
137        let entry = self.get_entry(&id);
138        let oid = entry.oid();
139        let conn_id = entry.item().conn_id().unwrap_or(&SYSTEM_CONN_ID);
140        let schema_id = &self
141            .get_schema(
142                &entry.name().qualifiers.database_spec,
143                &entry.name().qualifiers.schema_spec,
144                conn_id,
145            )
146            .id;
147        let name = &entry.name().item;
148        let owner_id = entry.owner_id();
149        let privileges_row = self.pack_privilege_array_row(entry.privileges());
150        let privileges = privileges_row.unpack_first();
151        let mut updates = match entry.item() {
152            CatalogItem::Index(index) => self.pack_index_update(id, index, diff),
153            CatalogItem::Source(source) => {
154                match &source.data_source {
155                    DataSourceDesc::Webhook { .. } => {
156                        vec![self.pack_webhook_source_update(id, diff)]
157                    }
158                    // Old-syntax subsource metadata (mz_postgres/mysql/sql_server_source_tables)
159                    // is now derived from create_sql by materialized views over
160                    // mz_catalog_raw, so ingestion exports need no special packing.
161                    DataSourceDesc::Ingestion { .. }
162                    | DataSourceDesc::OldSyntaxIngestion { .. }
163                    | DataSourceDesc::IngestionExport { .. }
164                    | DataSourceDesc::Introspection(_)
165                    | DataSourceDesc::Progress
166                    | DataSourceDesc::Catalog => vec![],
167                }
168            }
169            CatalogItem::MaterializedView(mview) => {
170                self.pack_materialized_view_update(id, mview, diff)
171            }
172            // mz_sinks, mz_kafka_sinks and mz_iceberg_sinks read create_sql
173            // out of mz_catalog_raw, so there is nothing to pack here.
174            CatalogItem::Sink(_) => vec![],
175            CatalogItem::Type(ty) => {
176                self.pack_type_update(id, oid, schema_id, name, owner_id, privileges, ty, diff)
177            }
178            CatalogItem::Func(func) => {
179                self.pack_func_update(id, schema_id, name, owner_id, func, diff)
180            }
181            // Tables, views, and metric sinks are exposed through materialized
182            // views derived from `mz_catalog_raw`, and logs and secrets never
183            // had builtin-table rows, so none pack a row here.
184            CatalogItem::Table(_)
185            | CatalogItem::View(_)
186            | CatalogItem::Log(_)
187            | CatalogItem::Secret(_)
188            | CatalogItem::MetricSink(_) => vec![],
189            // Connection details (mz_kafka_connections, mz_ssh_tunnel_connections,
190            // mz_aws_connections, mz_aws_privatelink_connections) are now derived
191            // from the persisted create_sql by materialized views over
192            // mz_catalog_raw, so connections need no special packing here.
193            CatalogItem::Connection(_) => vec![],
194        };
195
196        // Always report the latest for an objects columns.
197        if let Some(desc) = entry.relation_desc_latest() {
198            let defaults = match entry.item() {
199                CatalogItem::Table(Table {
200                    data_source: TableDataSource::TableWrites { defaults },
201                    ..
202                }) => Some(defaults),
203                _ => None,
204            };
205            for (i, (column_name, column_type)) in desc.iter().enumerate() {
206                let default: Option<String> = defaults.map(|d| d[i].to_ast_string_stable());
207                let default: Datum = default
208                    .as_ref()
209                    .map(|d| Datum::String(d))
210                    .unwrap_or(Datum::Null);
211                let pgtype = mz_pgrepr::Type::from(&column_type.scalar_type);
212                let (type_name, type_oid) = match &column_type.scalar_type {
213                    SqlScalarType::List {
214                        custom_id: Some(custom_id),
215                        ..
216                    }
217                    | SqlScalarType::Map {
218                        custom_id: Some(custom_id),
219                        ..
220                    }
221                    | SqlScalarType::Record {
222                        custom_id: Some(custom_id),
223                        ..
224                    } => {
225                        let entry = self.get_entry(custom_id);
226                        // NOTE(benesch): the `mz_columns.type text` field is
227                        // wrong. Types do not have a name that can be
228                        // represented as a single textual field. There can be
229                        // multiple types with the same name in different
230                        // schemas and databases. We should eventually deprecate
231                        // the `type` field in favor of a new `type_id` field
232                        // that can be joined against `mz_types`.
233                        //
234                        // For now, in the interest of pragmatism, we just use
235                        // the type's item name, and accept that there may be
236                        // ambiguity if the same type name is used in multiple
237                        // schemas. The ambiguity is mitigated by the OID, which
238                        // can be joined against `mz_types.oid` to resolve the
239                        // ambiguity.
240                        let name = &*entry.name().item;
241                        let oid = entry.oid();
242                        (name, oid)
243                    }
244                    _ => (pgtype.name(), pgtype.oid()),
245                };
246                updates.push(BuiltinTableUpdate::row(
247                    &*MZ_COLUMNS,
248                    Row::pack_slice(&[
249                        Datum::String(&id.to_string()),
250                        Datum::String(column_name),
251                        Datum::UInt64(u64::cast_from(i + 1)),
252                        Datum::from(column_type.nullable),
253                        Datum::String(type_name),
254                        default,
255                        Datum::UInt32(type_oid),
256                        Datum::Int32(pgtype.typmod()),
257                    ]),
258                    diff,
259                ));
260            }
261        }
262
263        // Use initial lcw so that we can tell apart default from non-existent windows.
264        if let Some(cw) = entry.item().initial_logical_compaction_window() {
265            updates.push(self.pack_history_retention_strategy_update(id, cw, diff));
266        }
267
268        updates.extend(Self::pack_item_global_id_update(entry, diff));
269
270        updates
271    }
272
273    fn pack_item_global_id_update(
274        entry: &CatalogEntry,
275        diff: Diff,
276    ) -> impl Iterator<Item = BuiltinTableUpdate<&'static BuiltinTable>> + use<'_> {
277        let id = entry.id().to_string();
278        let global_ids = entry.global_ids();
279        global_ids.map(move |global_id| {
280            BuiltinTableUpdate::row(
281                &*MZ_OBJECT_GLOBAL_IDS,
282                Row::pack_slice(&[Datum::String(&id), Datum::String(&global_id.to_string())]),
283                diff,
284            )
285        })
286    }
287
288    fn pack_history_retention_strategy_update(
289        &self,
290        id: CatalogItemId,
291        cw: CompactionWindow,
292        diff: Diff,
293    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
294        let cw: u64 = cw.comparable_timestamp().into();
295        let cw = Jsonb::from_serde_json(serde_json::Value::Number(serde_json::Number::from(cw)))
296            .expect("must serialize");
297        BuiltinTableUpdate::row(
298            &*MZ_HISTORY_RETENTION_STRATEGIES,
299            Row::pack_slice(&[
300                Datum::String(&id.to_string()),
301                // FOR is the only strategy at the moment. We may introduce FROM or others later.
302                Datum::String("FOR"),
303                cw.into_row().into_element(),
304            ]),
305            diff,
306        )
307    }
308
309    fn pack_materialized_view_update(
310        &self,
311        id: CatalogItemId,
312        mview: &MaterializedView,
313        diff: Diff,
314    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
315        let mut updates = Vec::new();
316
317        if let Some(refresh_schedule) = &mview.refresh_schedule {
318            // This can't be `ON COMMIT`, because that is represented by a `None` instead of an
319            // empty `RefreshSchedule`.
320            assert!(!refresh_schedule.is_empty());
321            for RefreshEvery {
322                interval,
323                aligned_to,
324            } in refresh_schedule.everies.iter()
325            {
326                let aligned_to_dt = mz_ore::now::to_datetime(
327                    <&Timestamp as TryInto<u64>>::try_into(aligned_to).expect("undoes planning"),
328                );
329                updates.push(BuiltinTableUpdate::row(
330                    &*MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES,
331                    Row::pack_slice(&[
332                        Datum::String(&id.to_string()),
333                        Datum::String("every"),
334                        Datum::Interval(
335                            Interval::from_duration(interval).expect(
336                                "planning ensured that this is convertible back to Interval",
337                            ),
338                        ),
339                        Datum::TimestampTz(aligned_to_dt.try_into().expect("undoes planning")),
340                        Datum::Null,
341                    ]),
342                    diff,
343                ));
344            }
345            for at in refresh_schedule.ats.iter() {
346                let at_dt = mz_ore::now::to_datetime(
347                    <&Timestamp as TryInto<u64>>::try_into(at).expect("undoes planning"),
348                );
349                updates.push(BuiltinTableUpdate::row(
350                    &*MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES,
351                    Row::pack_slice(&[
352                        Datum::String(&id.to_string()),
353                        Datum::String("at"),
354                        Datum::Null,
355                        Datum::Null,
356                        Datum::TimestampTz(at_dt.try_into().expect("undoes planning")),
357                    ]),
358                    diff,
359                ));
360            }
361        } else {
362            updates.push(BuiltinTableUpdate::row(
363                &*MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES,
364                Row::pack_slice(&[
365                    Datum::String(&id.to_string()),
366                    Datum::String("on-commit"),
367                    Datum::Null,
368                    Datum::Null,
369                    Datum::Null,
370                ]),
371                diff,
372            ));
373        }
374
375        if let Some(target_id) = mview.replacement_target {
376            updates.push(BuiltinTableUpdate::row(
377                &*MZ_REPLACEMENTS,
378                Row::pack_slice(&[
379                    Datum::String(&id.to_string()),
380                    Datum::String(&target_id.to_string()),
381                ]),
382                diff,
383            ));
384        }
385
386        updates
387    }
388
389    fn pack_index_update(
390        &self,
391        id: CatalogItemId,
392        index: &Index,
393        diff: Diff,
394    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
395        let mut updates = vec![];
396
397        let create_stmt = mz_sql::parse::parse(&index.create_sql)
398            .unwrap_or_else(|e| {
399                panic!(
400                    "create_sql cannot be invalid: `{}` --- error: `{}`",
401                    index.create_sql, e
402                )
403            })
404            .into_element()
405            .ast;
406
407        let key_sqls = match &create_stmt {
408            Statement::CreateIndex(CreateIndexStatement { key_parts, .. }) => key_parts
409                .as_ref()
410                .expect("key_parts is filled in during planning"),
411            _ => unreachable!(),
412        };
413
414        let on_entry = self.get_entry_by_global_id(&index.on);
415        let on_desc = on_entry
416            .relation_desc()
417            .expect("can only create indexes on items with a valid description");
418        let repr_col_types: Vec<ReprColumnType> = on_desc
419            .typ()
420            .column_types
421            .iter()
422            .map(ReprColumnType::from)
423            .collect();
424        for (i, key) in index.keys.iter().enumerate() {
425            let nullable = key.typ(&repr_col_types).nullable;
426            let seq_in_index = u64::cast_from(i + 1);
427            let key_sql = key_sqls
428                .get(i)
429                .expect("missing sql information for index key")
430                .to_ast_string_simple();
431            let (field_number, expression) = match key {
432                MirScalarExpr::Column(col, _) => {
433                    (Datum::UInt64(u64::cast_from(*col + 1)), Datum::Null)
434                }
435                _ => (Datum::Null, Datum::String(&key_sql)),
436            };
437            updates.push(BuiltinTableUpdate::row(
438                &*MZ_INDEX_COLUMNS,
439                Row::pack_slice(&[
440                    Datum::String(&id.to_string()),
441                    Datum::UInt64(seq_in_index),
442                    field_number,
443                    expression,
444                    Datum::from(nullable),
445                ]),
446                diff,
447            ));
448        }
449
450        updates
451    }
452
453    fn pack_type_update(
454        &self,
455        id: CatalogItemId,
456        oid: u32,
457        schema_id: &SchemaSpecifier,
458        name: &str,
459        owner_id: &RoleId,
460        privileges: Datum,
461        typ: &Type,
462        diff: Diff,
463    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
464        let mut out = vec![];
465
466        let redacted = typ.create_sql.as_ref().map(|create_sql| {
467            mz_sql::parse::parse(create_sql)
468                .unwrap_or_else(|_| panic!("create_sql cannot be invalid: {}", create_sql))
469                .into_element()
470                .ast
471                .to_ast_string_redacted()
472        });
473
474        out.push(BuiltinTableUpdate::row(
475            &*MZ_TYPES,
476            Row::pack_slice(&[
477                Datum::String(&id.to_string()),
478                Datum::UInt32(oid),
479                Datum::String(&schema_id.to_string()),
480                Datum::String(name),
481                Datum::String(&TypeCategory::from_catalog_type(&typ.details.typ).to_string()),
482                Datum::String(&owner_id.to_string()),
483                privileges,
484                if let Some(create_sql) = &typ.create_sql {
485                    Datum::String(create_sql)
486                } else {
487                    Datum::Null
488                },
489                if let Some(redacted) = &redacted {
490                    Datum::String(redacted)
491                } else {
492                    Datum::Null
493                },
494            ]),
495            diff,
496        ));
497
498        let mut row = Row::default();
499        let mut packer = row.packer();
500
501        fn append_modifier(packer: &mut RowPacker<'_>, mods: &[i64]) {
502            if mods.is_empty() {
503                packer.push(Datum::Null);
504            } else {
505                packer.push_list(mods.iter().map(|m| Datum::Int64(*m)));
506            }
507        }
508
509        let index_id = match &typ.details.typ {
510            CatalogType::Array {
511                element_reference: element_id,
512            } => {
513                packer.push(Datum::String(&id.to_string()));
514                packer.push(Datum::String(&element_id.to_string()));
515                &MZ_ARRAY_TYPES
516            }
517            CatalogType::List {
518                element_reference: element_id,
519                element_modifiers,
520            } => {
521                packer.push(Datum::String(&id.to_string()));
522                packer.push(Datum::String(&element_id.to_string()));
523                append_modifier(&mut packer, element_modifiers);
524                &MZ_LIST_TYPES
525            }
526            CatalogType::Map {
527                key_reference: key_id,
528                value_reference: value_id,
529                key_modifiers,
530                value_modifiers,
531            } => {
532                packer.push(Datum::String(&id.to_string()));
533                packer.push(Datum::String(&key_id.to_string()));
534                packer.push(Datum::String(&value_id.to_string()));
535                append_modifier(&mut packer, key_modifiers);
536                append_modifier(&mut packer, value_modifiers);
537                &MZ_MAP_TYPES
538            }
539            CatalogType::Pseudo => {
540                packer.push(Datum::String(&id.to_string()));
541                &MZ_PSEUDO_TYPES
542            }
543            _ => {
544                packer.push(Datum::String(&id.to_string()));
545                &MZ_BASE_TYPES
546            }
547        };
548        out.push(BuiltinTableUpdate::row(index_id, row, diff));
549
550        if let Some(pg_metadata) = &typ.details.pg_metadata {
551            out.push(BuiltinTableUpdate::row(
552                &*MZ_TYPE_PG_METADATA,
553                Row::pack_slice(&[
554                    Datum::String(&id.to_string()),
555                    Datum::UInt32(pg_metadata.typinput_oid),
556                    Datum::UInt32(pg_metadata.typreceive_oid),
557                    Datum::UInt32(pg_metadata.typsend_oid),
558                ]),
559                diff,
560            ));
561        }
562
563        out
564    }
565
566    fn pack_func_update(
567        &self,
568        id: CatalogItemId,
569        schema_id: &SchemaSpecifier,
570        name: &str,
571        owner_id: &RoleId,
572        func: &Func,
573        diff: Diff,
574    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
575        let mut updates = vec![];
576        for func_impl_details in func.inner.func_impls() {
577            let arg_type_ids = func_impl_details
578                .arg_typs
579                .iter()
580                .map(|typ| self.get_system_type(typ).id().to_string())
581                .collect::<Vec<_>>();
582
583            let mut row = Row::default();
584            row.packer()
585                .try_push_array(
586                    &[ArrayDimension {
587                        lower_bound: 1,
588                        length: arg_type_ids.len(),
589                    }],
590                    arg_type_ids.iter().map(|id| Datum::String(id)),
591                )
592                .expect(
593                    "arg_type_ids is 1 dimensional, and its length is used for the array length",
594                );
595            let arg_type_ids = row.unpack_first();
596
597            updates.push(BuiltinTableUpdate::row(
598                &*MZ_FUNCTIONS,
599                Row::pack_slice(&[
600                    Datum::String(&id.to_string()),
601                    Datum::UInt32(func_impl_details.oid),
602                    Datum::String(&schema_id.to_string()),
603                    Datum::String(name),
604                    arg_type_ids,
605                    Datum::from(
606                        func_impl_details
607                            .variadic_typ
608                            .map(|typ| self.get_system_type(typ).id().to_string())
609                            .as_deref(),
610                    ),
611                    Datum::from(
612                        func_impl_details
613                            .return_typ
614                            .map(|typ| self.get_system_type(typ).id().to_string())
615                            .as_deref(),
616                    ),
617                    func_impl_details.return_is_set.into(),
618                    Datum::String(&owner_id.to_string()),
619                ]),
620                diff,
621            ));
622
623            if let mz_sql::func::Func::Aggregate(_) = func.inner {
624                updates.push(BuiltinTableUpdate::row(
625                    &*MZ_AGGREGATES,
626                    Row::pack_slice(&[
627                        Datum::UInt32(func_impl_details.oid),
628                        // TODO(database-issues#1064): Support ordered-set aggregate functions.
629                        Datum::String("n"),
630                        Datum::Int16(0),
631                    ]),
632                    diff,
633                ));
634            }
635        }
636        updates
637    }
638
639    pub fn pack_op_update(
640        &self,
641        operator: &str,
642        func_impl_details: FuncImplCatalogDetails,
643        diff: Diff,
644    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
645        let arg_type_ids = func_impl_details
646            .arg_typs
647            .iter()
648            .map(|typ| self.get_system_type(typ).id().to_string())
649            .collect::<Vec<_>>();
650
651        let mut row = Row::default();
652        row.packer()
653            .try_push_array(
654                &[ArrayDimension {
655                    lower_bound: 1,
656                    length: arg_type_ids.len(),
657                }],
658                arg_type_ids.iter().map(|id| Datum::String(id)),
659            )
660            .expect("arg_type_ids is 1 dimensional, and its length is used for the array length");
661        let arg_type_ids = row.unpack_first();
662
663        BuiltinTableUpdate::row(
664            &*MZ_OPERATORS,
665            Row::pack_slice(&[
666                Datum::UInt32(func_impl_details.oid),
667                Datum::String(operator),
668                arg_type_ids,
669                Datum::from(
670                    func_impl_details
671                        .return_typ
672                        .map(|typ| self.get_system_type(typ).id().to_string())
673                        .as_deref(),
674                ),
675            ]),
676            diff,
677        )
678    }
679
680    pub fn pack_storage_usage_update(
681        &self,
682        VersionedStorageUsage::V1(event): VersionedStorageUsage,
683        diff: Diff,
684    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
685        let id = &MZ_STORAGE_USAGE_BY_SHARD;
686        let row = Row::pack_slice(&[
687            Datum::UInt64(event.id),
688            Datum::from(event.shard_id.as_deref()),
689            Datum::UInt64(event.size_bytes),
690            Datum::TimestampTz(
691                mz_ore::now::to_datetime(event.collection_timestamp)
692                    .try_into()
693                    .expect("must fit"),
694            ),
695        ]);
696        BuiltinTableUpdate::row(id, row, diff)
697    }
698
699    pub fn pack_egress_ip_update(
700        &self,
701        ip: &IpNet,
702    ) -> Result<BuiltinTableUpdate<&'static BuiltinTable>, Error> {
703        let id = &MZ_EGRESS_IPS;
704        let addr = ip.network();
705        let row = Row::pack_slice(&[
706            Datum::String(&addr.to_string()),
707            Datum::Int32(ip.prefix_len().into()),
708            Datum::String(&format!("{}/{}", addr, ip.prefix_len())),
709        ]);
710        Ok(BuiltinTableUpdate::row(id, row, Diff::ONE))
711    }
712
713    pub fn pack_license_key_update(
714        &self,
715        license_key: &ValidatedLicenseKey,
716    ) -> Result<BuiltinTableUpdate<&'static BuiltinTable>, Error> {
717        let id = &MZ_LICENSE_KEYS;
718        let row = Row::pack_slice(&[
719            Datum::String(&license_key.id),
720            Datum::String(&license_key.organization),
721            Datum::String(&license_key.environment_id),
722            Datum::TimestampTz(
723                mz_ore::now::to_datetime(license_key.expiration * 1000)
724                    .try_into()
725                    .expect("must fit"),
726            ),
727            Datum::TimestampTz(
728                mz_ore::now::to_datetime(license_key.not_before * 1000)
729                    .try_into()
730                    .expect("must fit"),
731            ),
732        ]);
733        Ok(BuiltinTableUpdate::row(id, row, Diff::ONE))
734    }
735
736    pub fn pack_all_replica_size_updates(&self) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
737        let mut updates = Vec::new();
738        for (size, alloc) in &self.cluster_replica_sizes.0 {
739            // Just invent something when the limits are `None`, which only happens in non-prod
740            // environments (tests, process orchestrator, etc.)
741            let DiskLimit(ByteSize(disk_bytes)) =
742                (alloc.disk_limit).unwrap_or(DiskLimit::ARBITRARY);
743
744            // The disk column of mz_clusters / mz_cluster_replicas MVs needs
745            // `swap_enabled` and `disk_bytes`; expose them through a parallel
746            // internal table. Unlike the public sizes table below, we write
747            // here unconditionally — `cluster_replica_size_has_disk` previously
748            // indexed the in-memory map without checking `disabled`, so a
749            // managed cluster pinned to a disabled size still resolved its
750            // `disk` column from the real allocation. Writing disabled rows
751            // here preserves that behavior.
752            let internal_row = Row::pack_slice(&[
753                size.as_str().into(),
754                Datum::from(alloc.swap_enabled),
755                disk_bytes.into(),
756            ]);
757            updates.push(BuiltinTableUpdate::row(
758                &*MZ_CLUSTER_REPLICA_SIZE_INTERNAL,
759                internal_row,
760                Diff::ONE,
761            ));
762
763            if alloc.disabled {
764                continue;
765            }
766
767            let cpu_limit = alloc.cpu_limit.unwrap_or(CpuLimit::MAX);
768            let MemoryLimit(ByteSize(memory_bytes)) =
769                (alloc.memory_limit).unwrap_or(MemoryLimit::MAX);
770
771            let row = Row::pack_slice(&[
772                size.as_str().into(),
773                u64::cast_from(alloc.scale).into(),
774                u64::cast_from(alloc.workers).into(),
775                cpu_limit.as_nanocpus().into(),
776                memory_bytes.into(),
777                disk_bytes.into(),
778                (alloc.credits_per_hour).into(),
779            ]);
780
781            updates.push(BuiltinTableUpdate::row(
782                &*MZ_CLUSTER_REPLICA_SIZES,
783                row,
784                Diff::ONE,
785            ));
786        }
787
788        updates
789    }
790
791    pub fn pack_subscribe_update(
792        &self,
793        id: GlobalId,
794        subscribe: &ActiveSubscribe,
795        session_uuid: Uuid,
796        diff: Diff,
797    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
798        let mut row = Row::default();
799        let mut packer = row.packer();
800        packer.push(Datum::String(&id.to_string()));
801        packer.push(Datum::Uuid(session_uuid));
802        packer.push(Datum::String(&subscribe.cluster_id.to_string()));
803
804        let start_dt = mz_ore::now::to_datetime(subscribe.start_time);
805        packer.push(Datum::TimestampTz(start_dt.try_into().expect("must fit")));
806
807        let depends_on: Vec<_> = subscribe
808            .depends_on
809            .iter()
810            .map(|id| id.to_string())
811            .collect();
812        packer.push_list(depends_on.iter().map(|s| Datum::String(s)));
813
814        BuiltinTableUpdate::row(&*MZ_SUBSCRIPTIONS, row, diff)
815    }
816
817    pub fn pack_session_update(
818        &self,
819        conn: &ConnMeta,
820        diff: Diff,
821    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
822        let connect_dt = mz_ore::now::to_datetime(conn.connected_at());
823        BuiltinTableUpdate::row(
824            &*MZ_SESSIONS,
825            Row::pack_slice(&[
826                Datum::Uuid(conn.uuid()),
827                Datum::UInt32(conn.conn_id().unhandled()),
828                Datum::String(&conn.authenticated_role_id().to_string()),
829                Datum::from(conn.client_ip().map(|ip| ip.to_string()).as_deref()),
830                Datum::TimestampTz(connect_dt.try_into().expect("must fit")),
831            ]),
832            diff,
833        )
834    }
835
836    fn pack_privilege_array_row(&self, privileges: &PrivilegeMap) -> Row {
837        let mut row = Row::default();
838        let flat_privileges: Vec<_> = privileges.all_values_owned().collect();
839        row.packer()
840            .try_push_array(
841                &[ArrayDimension {
842                    lower_bound: 1,
843                    length: flat_privileges.len(),
844                }],
845                flat_privileges
846                    .into_iter()
847                    .map(|mz_acl_item| Datum::MzAclItem(mz_acl_item.clone())),
848            )
849            .expect("privileges is 1 dimensional, and its length is used for the array length");
850        row
851    }
852
853    pub fn pack_webhook_source_update(
854        &self,
855        item_id: CatalogItemId,
856        diff: Diff,
857    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
858        let url = self
859            .try_get_webhook_url(&item_id)
860            .expect("webhook source should exist");
861        let url = url.to_string();
862        let name = &self.get_entry(&item_id).name().item;
863        let id_str = item_id.to_string();
864
865        BuiltinTableUpdate::row(
866            &*MZ_WEBHOOKS_SOURCES,
867            Row::pack_slice(&[
868                Datum::String(&id_str),
869                Datum::String(name),
870                Datum::String(&url),
871            ]),
872            diff,
873        )
874    }
875
876    pub fn pack_source_references_update(
877        &self,
878        source_references: &SourceReferences,
879        diff: Diff,
880    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
881        let source_id = source_references.source_id.to_string();
882        let updated_at = &source_references.updated_at;
883        source_references
884            .references
885            .iter()
886            .map(|reference| {
887                let mut row = Row::default();
888                let mut packer = row.packer();
889                packer.extend([
890                    Datum::String(&source_id),
891                    reference
892                        .namespace
893                        .as_ref()
894                        .map(|s| Datum::String(s))
895                        .unwrap_or(Datum::Null),
896                    Datum::String(&reference.name),
897                    Datum::TimestampTz(
898                        mz_ore::now::to_datetime(*updated_at)
899                            .try_into()
900                            .expect("must fit"),
901                    ),
902                ]);
903                if reference.columns.len() > 0 {
904                    packer
905                        .try_push_array(
906                            &[ArrayDimension {
907                                lower_bound: 1,
908                                length: reference.columns.len(),
909                            }],
910                            reference.columns.iter().map(|col| Datum::String(col)),
911                        )
912                        .expect(
913                            "columns is 1 dimensional, and its length is used for the array length",
914                        );
915                } else {
916                    packer.push(Datum::Null);
917                }
918
919                BuiltinTableUpdate::row(&*MZ_SOURCE_REFERENCES, row, diff)
920            })
921            .collect()
922    }
923}