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_AWS_CONNECTIONS,
19    MZ_AWS_PRIVATELINK_CONNECTIONS, MZ_BASE_TYPES, MZ_CLUSTER_REPLICA_SIZE_INTERNAL,
20    MZ_CLUSTER_REPLICA_SIZES, MZ_COLUMNS, MZ_EGRESS_IPS, MZ_FUNCTIONS,
21    MZ_HISTORY_RETENTION_STRATEGIES, MZ_ICEBERG_SINKS, MZ_INDEX_COLUMNS, MZ_KAFKA_CONNECTIONS,
22    MZ_KAFKA_SINKS, MZ_KAFKA_SOURCE_TABLES, MZ_LICENSE_KEYS, MZ_LIST_TYPES, MZ_MAP_TYPES,
23    MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES, MZ_MYSQL_SOURCE_TABLES, MZ_OBJECT_DEPENDENCIES,
24    MZ_OBJECT_GLOBAL_IDS, MZ_OPERATORS, MZ_POSTGRES_SOURCE_TABLES, MZ_PSEUDO_TYPES,
25    MZ_REPLACEMENTS, MZ_ROLE_AUTH, MZ_SESSIONS, MZ_SINKS, MZ_SOURCE_REFERENCES,
26    MZ_SQL_SERVER_SOURCE_TABLES, MZ_SSH_TUNNEL_CONNECTIONS, MZ_STORAGE_USAGE_BY_SHARD,
27    MZ_SUBSCRIPTIONS, MZ_TABLES, MZ_TYPE_PG_METADATA, MZ_TYPES, MZ_VIEWS, MZ_WEBHOOKS_SOURCES,
28};
29use mz_catalog::config::AwsPrincipalContext;
30use mz_catalog::durable::SourceReferences;
31use mz_catalog::memory::error::Error;
32use mz_catalog::memory::objects::{
33    CatalogEntry, CatalogItem, Connection, DataSourceDesc, Func, Index, MaterializedView, Sink,
34    Table, TableDataSource, Type, View,
35};
36use mz_expr::MirScalarExpr;
37use mz_license_keys::ValidatedLicenseKey;
38use mz_orchestrator::{CpuLimit, DiskLimit, MemoryLimit};
39use mz_ore::cast::CastFrom;
40use mz_ore::collections::CollectionExt;
41use mz_persist_client::batch::ProtoBatch;
42use mz_repr::adt::array::ArrayDimension;
43use mz_repr::adt::interval::Interval;
44use mz_repr::adt::jsonb::Jsonb;
45use mz_repr::adt::mz_acl_item::PrivilegeMap;
46use mz_repr::refresh_schedule::RefreshEvery;
47use mz_repr::role_id::RoleId;
48use mz_repr::{
49    CatalogItemId, Datum, Diff, GlobalId, ReprColumnType, Row, RowPacker, SqlScalarType, Timestamp,
50};
51use mz_sql::ast::{CreateIndexStatement, Statement, UnresolvedItemName};
52use mz_sql::catalog::{CatalogType, TypeCategory};
53use mz_sql::func::FuncImplCatalogDetails;
54use mz_sql::names::SchemaSpecifier;
55use mz_sql::plan::{ConnectionDetails, SshKey};
56use mz_sql_parser::ast::display::AstDisplay;
57use mz_storage_client::client::TableData;
58use mz_storage_types::connections::KafkaConnection;
59use mz_storage_types::connections::aws::{AwsAuth, AwsConnection};
60use mz_storage_types::connections::inline::ReferencedConnection;
61use mz_storage_types::connections::string_or_secret::StringOrSecret;
62use mz_storage_types::sinks::{IcebergSinkConnection, KafkaSinkConnection, StorageSinkConnection};
63use mz_storage_types::sources::SourceConnection;
64use smallvec::smallvec;
65
66// DO NOT add any more imports from `crate` outside of `crate::catalog`.
67use crate::active_compute_sink::ActiveSubscribe;
68use crate::catalog::CatalogState;
69use crate::coord::ConnMeta;
70
71/// An update to a built-in table.
72#[derive(Debug, Clone)]
73pub struct BuiltinTableUpdate<T = CatalogItemId> {
74    /// The reference of the table to update.
75    pub id: T,
76    /// The data to put into the table.
77    pub data: TableData,
78}
79
80impl<T> BuiltinTableUpdate<T> {
81    /// Create a [`BuiltinTableUpdate`] from a [`Row`].
82    pub fn row(id: T, row: Row, diff: Diff) -> BuiltinTableUpdate<T> {
83        BuiltinTableUpdate {
84            id,
85            data: TableData::Rows(vec![(row, diff)]),
86        }
87    }
88
89    pub fn batch(id: T, batch: ProtoBatch) -> BuiltinTableUpdate<T> {
90        BuiltinTableUpdate {
91            id,
92            data: TableData::Batches(smallvec![batch]),
93        }
94    }
95}
96
97impl CatalogState {
98    pub fn resolve_builtin_table_updates(
99        &self,
100        builtin_table_update: Vec<BuiltinTableUpdate<&'static BuiltinTable>>,
101    ) -> Vec<BuiltinTableUpdate<CatalogItemId>> {
102        builtin_table_update
103            .into_iter()
104            .map(|builtin_table_update| self.resolve_builtin_table_update(builtin_table_update))
105            .collect()
106    }
107
108    pub fn resolve_builtin_table_update(
109        &self,
110        BuiltinTableUpdate { id, data }: BuiltinTableUpdate<&'static BuiltinTable>,
111    ) -> BuiltinTableUpdate<CatalogItemId> {
112        let id = self.resolve_builtin_table(id);
113        BuiltinTableUpdate { id, data }
114    }
115
116    pub fn pack_depends_update(
117        &self,
118        depender: CatalogItemId,
119        dependee: CatalogItemId,
120        diff: Diff,
121    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
122        let row = Row::pack_slice(&[
123            Datum::String(&depender.to_string()),
124            Datum::String(&dependee.to_string()),
125        ]);
126        BuiltinTableUpdate::row(&*MZ_OBJECT_DEPENDENCIES, row, diff)
127    }
128
129    pub(super) fn pack_role_auth_update(
130        &self,
131        id: RoleId,
132        diff: Diff,
133    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
134        let role_auth = self.get_role_auth(&id);
135        let role = self.get_role(&id);
136        BuiltinTableUpdate::row(
137            &*MZ_ROLE_AUTH,
138            Row::pack_slice(&[
139                Datum::String(&role_auth.role_id.to_string()),
140                Datum::UInt32(role.oid),
141                match &role_auth.password_hash {
142                    Some(hash) => Datum::String(hash),
143                    None => Datum::Null,
144                },
145                Datum::TimestampTz(
146                    mz_ore::now::to_datetime(role_auth.updated_at)
147                        .try_into()
148                        .expect("must fit"),
149                ),
150            ]),
151            diff,
152        )
153    }
154
155    pub(super) fn pack_item_update(
156        &self,
157        id: CatalogItemId,
158        diff: Diff,
159    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
160        let entry = self.get_entry(&id);
161        let oid = entry.oid();
162        let conn_id = entry.item().conn_id().unwrap_or(&SYSTEM_CONN_ID);
163        let schema_id = &self
164            .get_schema(
165                &entry.name().qualifiers.database_spec,
166                &entry.name().qualifiers.schema_spec,
167                conn_id,
168            )
169            .id;
170        let name = &entry.name().item;
171        let owner_id = entry.owner_id();
172        let privileges_row = self.pack_privilege_array_row(entry.privileges());
173        let privileges = privileges_row.unpack_first();
174        let mut updates = match entry.item() {
175            CatalogItem::Index(index) => self.pack_index_update(id, index, diff),
176            CatalogItem::Table(table) => {
177                let mut updates = self
178                    .pack_table_update(id, oid, schema_id, name, owner_id, privileges, diff, table);
179
180                if let TableDataSource::DataSource {
181                    desc: data_source,
182                    timeline: _,
183                } = &table.data_source
184                {
185                    updates.extend(match data_source {
186                        DataSourceDesc::IngestionExport {
187                            ingestion_id,
188                            external_reference: UnresolvedItemName(external_reference),
189                            details: _,
190                            data_config: _,
191                        } => {
192                            let ingestion_entry = self
193                                .get_entry(ingestion_id)
194                                .source_desc()
195                                .expect("primary source exists")
196                                .expect("primary source is a source");
197
198                            match ingestion_entry.connection.name() {
199                                "postgres" => {
200                                    mz_ore::soft_assert_eq_no_log!(external_reference.len(), 3);
201                                    // The left-most qualification of Postgres
202                                    // tables is the database, but this
203                                    // information is redundant because each
204                                    // Postgres connection connects to only one
205                                    // database.
206                                    let schema_name = external_reference[1].as_str();
207                                    let table_name = external_reference[2].as_str();
208
209                                    self.pack_postgres_source_tables_update(
210                                        id,
211                                        schema_name,
212                                        table_name,
213                                        diff,
214                                    )
215                                }
216                                "mysql" => {
217                                    mz_ore::soft_assert_eq_no_log!(external_reference.len(), 2);
218                                    let schema_name = external_reference[0].as_str();
219                                    let table_name = external_reference[1].as_str();
220
221                                    self.pack_mysql_source_tables_update(
222                                        id,
223                                        schema_name,
224                                        table_name,
225                                        diff,
226                                    )
227                                }
228                                "sql-server" => {
229                                    mz_ore::soft_assert_eq_no_log!(external_reference.len(), 3);
230                                    // The left-most qualification of SQL Server tables is
231                                    // the database, but this information is redundant
232                                    // because each SQL Server connection connects to
233                                    // only one database.
234                                    let schema_name = external_reference[1].as_str();
235                                    let table_name = external_reference[2].as_str();
236
237                                    self.pack_sql_server_source_table_update(
238                                        id,
239                                        schema_name,
240                                        table_name,
241                                        diff,
242                                    )
243                                }
244                                // Load generator sources don't have any special
245                                // updates.
246                                "load-generator" => vec![],
247                                "kafka" => {
248                                    mz_ore::soft_assert_eq_no_log!(external_reference.len(), 1);
249                                    let topic = external_reference[0].as_str();
250                                    let envelope = data_source.envelope();
251                                    let (key_format, value_format) = data_source.formats();
252
253                                    self.pack_kafka_source_tables_update(
254                                        id,
255                                        topic,
256                                        envelope,
257                                        key_format,
258                                        value_format,
259                                        diff,
260                                    )
261                                }
262                                s => unreachable!("{s} sources do not have tables"),
263                            }
264                        }
265                        DataSourceDesc::Ingestion { .. }
266                        | DataSourceDesc::OldSyntaxIngestion { .. }
267                        | DataSourceDesc::Introspection(_)
268                        | DataSourceDesc::Progress
269                        | DataSourceDesc::Webhook { .. }
270                        | DataSourceDesc::Catalog => vec![],
271                    });
272                }
273
274                updates
275            }
276            CatalogItem::Source(source) => {
277                match &source.data_source {
278                    DataSourceDesc::Ingestion { .. }
279                    | DataSourceDesc::OldSyntaxIngestion { .. } => vec![],
280                    DataSourceDesc::IngestionExport {
281                        ingestion_id,
282                        external_reference: UnresolvedItemName(external_reference),
283                        details: _,
284                        data_config: _,
285                    } => {
286                        let ingestion_entry = self
287                            .get_entry(ingestion_id)
288                            .source_desc()
289                            .expect("primary source exists")
290                            .expect("primary source is a source");
291
292                        match ingestion_entry.connection.name() {
293                            "postgres" => {
294                                mz_ore::soft_assert_eq_no_log!(external_reference.len(), 3);
295                                // The left-most qualification of Postgres
296                                // tables is the database, but this
297                                // information is redundant because each
298                                // Postgres connection connects to only one
299                                // database.
300                                let schema_name = external_reference[1].as_str();
301                                let table_name = external_reference[2].as_str();
302
303                                self.pack_postgres_source_tables_update(
304                                    id,
305                                    schema_name,
306                                    table_name,
307                                    diff,
308                                )
309                            }
310                            "mysql" => {
311                                mz_ore::soft_assert_eq_no_log!(external_reference.len(), 2);
312                                let schema_name = external_reference[0].as_str();
313                                let table_name = external_reference[1].as_str();
314
315                                self.pack_mysql_source_tables_update(
316                                    id,
317                                    schema_name,
318                                    table_name,
319                                    diff,
320                                )
321                            }
322                            "sql-server" => {
323                                mz_ore::soft_assert_eq_no_log!(external_reference.len(), 3);
324                                // The left-most qualification of SQL Server tables is
325                                // the database, but this information is redundant
326                                // because each SQL Server connection connects to
327                                // only one database.
328                                let schema_name = external_reference[1].as_str();
329                                let table_name = external_reference[2].as_str();
330
331                                self.pack_sql_server_source_table_update(
332                                    id,
333                                    schema_name,
334                                    table_name,
335                                    diff,
336                                )
337                            }
338                            // Load generator sources don't have any special
339                            // updates.
340                            "load-generator" => vec![],
341                            s => unreachable!("{s} sources do not have subsources"),
342                        }
343                    }
344                    DataSourceDesc::Webhook { .. } => {
345                        vec![self.pack_webhook_source_update(id, diff)]
346                    }
347                    DataSourceDesc::Introspection(_)
348                    | DataSourceDesc::Progress
349                    | DataSourceDesc::Catalog => vec![],
350                }
351            }
352            CatalogItem::View(view) => {
353                self.pack_view_update(id, oid, schema_id, name, owner_id, privileges, view, diff)
354            }
355            CatalogItem::MaterializedView(mview) => {
356                self.pack_materialized_view_update(id, mview, diff)
357            }
358            CatalogItem::Sink(sink) => {
359                self.pack_sink_update(id, oid, schema_id, name, owner_id, sink, diff)
360            }
361            CatalogItem::Type(ty) => {
362                self.pack_type_update(id, oid, schema_id, name, owner_id, privileges, ty, diff)
363            }
364            CatalogItem::Func(func) => {
365                self.pack_func_update(id, schema_id, name, owner_id, func, diff)
366            }
367            CatalogItem::Log(_) | CatalogItem::Secret(_) => vec![],
368            CatalogItem::Connection(connection) => {
369                self.pack_connection_update(id, connection, diff)
370            }
371        };
372
373        if !entry.item().is_temporary() {
374            // Populate or clean up the `mz_object_dependencies` table.
375            // TODO(jkosh44) Unclear if this table wants to include all uses or only references.
376            for dependee in entry.item().references().items() {
377                updates.push(self.pack_depends_update(id, *dependee, diff))
378            }
379        }
380
381        // Always report the latest for an objects columns.
382        if let Some(desc) = entry.relation_desc_latest() {
383            let defaults = match entry.item() {
384                CatalogItem::Table(Table {
385                    data_source: TableDataSource::TableWrites { defaults },
386                    ..
387                }) => Some(defaults),
388                _ => None,
389            };
390            for (i, (column_name, column_type)) in desc.iter().enumerate() {
391                let default: Option<String> = defaults.map(|d| d[i].to_ast_string_stable());
392                let default: Datum = default
393                    .as_ref()
394                    .map(|d| Datum::String(d))
395                    .unwrap_or(Datum::Null);
396                let pgtype = mz_pgrepr::Type::from(&column_type.scalar_type);
397                let (type_name, type_oid) = match &column_type.scalar_type {
398                    SqlScalarType::List {
399                        custom_id: Some(custom_id),
400                        ..
401                    }
402                    | SqlScalarType::Map {
403                        custom_id: Some(custom_id),
404                        ..
405                    }
406                    | SqlScalarType::Record {
407                        custom_id: Some(custom_id),
408                        ..
409                    } => {
410                        let entry = self.get_entry(custom_id);
411                        // NOTE(benesch): the `mz_columns.type text` field is
412                        // wrong. Types do not have a name that can be
413                        // represented as a single textual field. There can be
414                        // multiple types with the same name in different
415                        // schemas and databases. We should eventually deprecate
416                        // the `type` field in favor of a new `type_id` field
417                        // that can be joined against `mz_types`.
418                        //
419                        // For now, in the interest of pragmatism, we just use
420                        // the type's item name, and accept that there may be
421                        // ambiguity if the same type name is used in multiple
422                        // schemas. The ambiguity is mitigated by the OID, which
423                        // can be joined against `mz_types.oid` to resolve the
424                        // ambiguity.
425                        let name = &*entry.name().item;
426                        let oid = entry.oid();
427                        (name, oid)
428                    }
429                    _ => (pgtype.name(), pgtype.oid()),
430                };
431                updates.push(BuiltinTableUpdate::row(
432                    &*MZ_COLUMNS,
433                    Row::pack_slice(&[
434                        Datum::String(&id.to_string()),
435                        Datum::String(column_name),
436                        Datum::UInt64(u64::cast_from(i + 1)),
437                        Datum::from(column_type.nullable),
438                        Datum::String(type_name),
439                        default,
440                        Datum::UInt32(type_oid),
441                        Datum::Int32(pgtype.typmod()),
442                    ]),
443                    diff,
444                ));
445            }
446        }
447
448        // Use initial lcw so that we can tell apart default from non-existent windows.
449        if let Some(cw) = entry.item().initial_logical_compaction_window() {
450            updates.push(self.pack_history_retention_strategy_update(id, cw, diff));
451        }
452
453        updates.extend(Self::pack_item_global_id_update(entry, diff));
454
455        updates
456    }
457
458    fn pack_item_global_id_update(
459        entry: &CatalogEntry,
460        diff: Diff,
461    ) -> impl Iterator<Item = BuiltinTableUpdate<&'static BuiltinTable>> + use<'_> {
462        let id = entry.id().to_string();
463        let global_ids = entry.global_ids();
464        global_ids.map(move |global_id| {
465            BuiltinTableUpdate::row(
466                &*MZ_OBJECT_GLOBAL_IDS,
467                Row::pack_slice(&[Datum::String(&id), Datum::String(&global_id.to_string())]),
468                diff,
469            )
470        })
471    }
472
473    fn pack_history_retention_strategy_update(
474        &self,
475        id: CatalogItemId,
476        cw: CompactionWindow,
477        diff: Diff,
478    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
479        let cw: u64 = cw.comparable_timestamp().into();
480        let cw = Jsonb::from_serde_json(serde_json::Value::Number(serde_json::Number::from(cw)))
481            .expect("must serialize");
482        BuiltinTableUpdate::row(
483            &*MZ_HISTORY_RETENTION_STRATEGIES,
484            Row::pack_slice(&[
485                Datum::String(&id.to_string()),
486                // FOR is the only strategy at the moment. We may introduce FROM or others later.
487                Datum::String("FOR"),
488                cw.into_row().into_element(),
489            ]),
490            diff,
491        )
492    }
493
494    fn pack_table_update(
495        &self,
496        id: CatalogItemId,
497        oid: u32,
498        schema_id: &SchemaSpecifier,
499        name: &str,
500        owner_id: &RoleId,
501        privileges: Datum,
502        diff: Diff,
503        table: &Table,
504    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
505        let redacted = table.create_sql.as_ref().map(|create_sql| {
506            mz_sql::parse::parse(create_sql)
507                .unwrap_or_else(|_| panic!("create_sql cannot be invalid: {}", create_sql))
508                .into_element()
509                .ast
510                .to_ast_string_redacted()
511        });
512        let source_id = if let TableDataSource::DataSource {
513            desc: DataSourceDesc::IngestionExport { ingestion_id, .. },
514            ..
515        } = &table.data_source
516        {
517            Some(ingestion_id.to_string())
518        } else {
519            None
520        };
521
522        vec![BuiltinTableUpdate::row(
523            &*MZ_TABLES,
524            Row::pack_slice(&[
525                Datum::String(&id.to_string()),
526                Datum::UInt32(oid),
527                Datum::String(&schema_id.to_string()),
528                Datum::String(name),
529                Datum::String(&owner_id.to_string()),
530                privileges,
531                if let Some(create_sql) = &table.create_sql {
532                    Datum::String(create_sql)
533                } else {
534                    Datum::Null
535                },
536                if let Some(redacted) = &redacted {
537                    Datum::String(redacted)
538                } else {
539                    Datum::Null
540                },
541                if let Some(source_id) = source_id.as_ref() {
542                    Datum::String(source_id)
543                } else {
544                    Datum::Null
545                },
546            ]),
547            diff,
548        )]
549    }
550
551    fn pack_postgres_source_tables_update(
552        &self,
553        id: CatalogItemId,
554        schema_name: &str,
555        table_name: &str,
556        diff: Diff,
557    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
558        vec![BuiltinTableUpdate::row(
559            &*MZ_POSTGRES_SOURCE_TABLES,
560            Row::pack_slice(&[
561                Datum::String(&id.to_string()),
562                Datum::String(schema_name),
563                Datum::String(table_name),
564            ]),
565            diff,
566        )]
567    }
568
569    fn pack_mysql_source_tables_update(
570        &self,
571        id: CatalogItemId,
572        schema_name: &str,
573        table_name: &str,
574        diff: Diff,
575    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
576        vec![BuiltinTableUpdate::row(
577            &*MZ_MYSQL_SOURCE_TABLES,
578            Row::pack_slice(&[
579                Datum::String(&id.to_string()),
580                Datum::String(schema_name),
581                Datum::String(table_name),
582            ]),
583            diff,
584        )]
585    }
586
587    fn pack_sql_server_source_table_update(
588        &self,
589        id: CatalogItemId,
590        schema_name: &str,
591        table_name: &str,
592        diff: Diff,
593    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
594        vec![BuiltinTableUpdate::row(
595            &*MZ_SQL_SERVER_SOURCE_TABLES,
596            Row::pack_slice(&[
597                Datum::String(&id.to_string()),
598                Datum::String(schema_name),
599                Datum::String(table_name),
600            ]),
601            diff,
602        )]
603    }
604
605    fn pack_kafka_source_tables_update(
606        &self,
607        id: CatalogItemId,
608        topic: &str,
609        envelope: Option<&str>,
610        key_format: Option<&str>,
611        value_format: Option<&str>,
612        diff: Diff,
613    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
614        vec![BuiltinTableUpdate::row(
615            &*MZ_KAFKA_SOURCE_TABLES,
616            Row::pack_slice(&[
617                Datum::String(&id.to_string()),
618                Datum::String(topic),
619                Datum::from(envelope),
620                Datum::from(key_format),
621                Datum::from(value_format),
622            ]),
623            diff,
624        )]
625    }
626
627    fn pack_connection_update(
628        &self,
629        id: CatalogItemId,
630        connection: &Connection,
631        diff: Diff,
632    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
633        let mut updates = vec![];
634        match connection.details {
635            ConnectionDetails::Kafka(ref kafka) => {
636                updates.extend(self.pack_kafka_connection_update(id, kafka, diff));
637            }
638            ConnectionDetails::Aws(ref aws_config) => {
639                match self.pack_aws_connection_update(id, aws_config, diff) {
640                    Ok(update) => {
641                        updates.push(update);
642                    }
643                    Err(e) => {
644                        tracing::error!(%id, %e, "failed writing row to mz_aws_connections table");
645                    }
646                }
647            }
648            ConnectionDetails::AwsPrivatelink(_) => {
649                if let Some(aws_principal_context) = self.aws_principal_context.as_ref() {
650                    updates.push(self.pack_aws_privatelink_connection_update(
651                        id,
652                        aws_principal_context,
653                        diff,
654                    ));
655                } else {
656                    tracing::error!(%id, "missing AWS principal context; cannot write row to mz_aws_privatelink_connections table");
657                }
658            }
659            ConnectionDetails::Ssh {
660                ref key_1,
661                ref key_2,
662                ..
663            } => {
664                updates.push(self.pack_ssh_tunnel_connection_update(id, key_1, key_2, diff));
665            }
666            ConnectionDetails::Csr(_)
667            | ConnectionDetails::GlueSchemaRegistry(_)
668            | ConnectionDetails::Gcp(_)
669            | ConnectionDetails::Postgres(_)
670            | ConnectionDetails::MySql(_)
671            | ConnectionDetails::SqlServer(_)
672            | ConnectionDetails::IcebergCatalog(_) => (),
673        };
674        updates
675    }
676
677    pub(crate) fn pack_ssh_tunnel_connection_update(
678        &self,
679        id: CatalogItemId,
680        key_1: &SshKey,
681        key_2: &SshKey,
682        diff: Diff,
683    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
684        BuiltinTableUpdate::row(
685            &*MZ_SSH_TUNNEL_CONNECTIONS,
686            Row::pack_slice(&[
687                Datum::String(&id.to_string()),
688                Datum::String(key_1.public_key().as_str()),
689                Datum::String(key_2.public_key().as_str()),
690            ]),
691            diff,
692        )
693    }
694
695    fn pack_kafka_connection_update(
696        &self,
697        id: CatalogItemId,
698        kafka: &KafkaConnection<ReferencedConnection>,
699        diff: Diff,
700    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
701        let progress_topic = kafka.progress_topic(&self.config.connection_context, id);
702        let mut row = Row::default();
703        row.packer()
704            .try_push_array(
705                &[ArrayDimension {
706                    lower_bound: 1,
707                    length: kafka.brokers.len(),
708                }],
709                kafka
710                    .brokers
711                    .iter()
712                    .map(|broker| Datum::String(&broker.address)),
713            )
714            .expect("kafka.brokers is 1 dimensional, and its length is used for the array length");
715        let brokers = row.unpack_first();
716        vec![BuiltinTableUpdate::row(
717            &*MZ_KAFKA_CONNECTIONS,
718            Row::pack_slice(&[
719                Datum::String(&id.to_string()),
720                brokers,
721                Datum::String(&progress_topic),
722            ]),
723            diff,
724        )]
725    }
726
727    pub fn pack_aws_privatelink_connection_update(
728        &self,
729        connection_id: CatalogItemId,
730        aws_principal_context: &AwsPrincipalContext,
731        diff: Diff,
732    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
733        let id = &MZ_AWS_PRIVATELINK_CONNECTIONS;
734        let row = Row::pack_slice(&[
735            Datum::String(&connection_id.to_string()),
736            Datum::String(&aws_principal_context.to_principal_string(connection_id)),
737        ]);
738        BuiltinTableUpdate::row(id, row, diff)
739    }
740
741    pub fn pack_aws_connection_update(
742        &self,
743        connection_id: CatalogItemId,
744        aws_config: &AwsConnection,
745        diff: Diff,
746    ) -> Result<BuiltinTableUpdate<&'static BuiltinTable>, anyhow::Error> {
747        let id = &MZ_AWS_CONNECTIONS;
748
749        let mut access_key_id = None;
750        let mut access_key_id_secret_id = None;
751        let mut secret_access_key_secret_id = None;
752        let mut session_token = None;
753        let mut session_token_secret_id = None;
754        let mut assume_role_arn = None;
755        let mut assume_role_session_name = None;
756        let mut principal = None;
757        let mut external_id = None;
758        let mut example_trust_policy = None;
759        match &aws_config.auth {
760            AwsAuth::Credentials(credentials) => {
761                match &credentials.access_key_id {
762                    StringOrSecret::String(s) => access_key_id = Some(s.as_str()),
763                    StringOrSecret::Secret(s) => access_key_id_secret_id = Some(s.to_string()),
764                }
765                secret_access_key_secret_id = Some(credentials.secret_access_key.to_string());
766                match credentials.session_token.as_ref() {
767                    None => (),
768                    Some(StringOrSecret::String(s)) => session_token = Some(s.as_str()),
769                    Some(StringOrSecret::Secret(s)) => {
770                        session_token_secret_id = Some(s.to_string())
771                    }
772                }
773            }
774            AwsAuth::AssumeRole(assume_role) => {
775                assume_role_arn = Some(assume_role.arn.as_str());
776                assume_role_session_name = assume_role.session_name.as_deref();
777                principal = self
778                    .config
779                    .connection_context
780                    .aws_connection_role_arn
781                    .as_deref();
782                external_id =
783                    Some(assume_role.external_id(&self.config.connection_context, connection_id)?);
784                example_trust_policy = {
785                    let policy = assume_role
786                        .example_trust_policy(&self.config.connection_context, connection_id)?;
787                    let policy = Jsonb::from_serde_json(policy).expect("valid json");
788                    Some(policy.into_row())
789                };
790            }
791        }
792
793        let row = Row::pack_slice(&[
794            Datum::String(&connection_id.to_string()),
795            Datum::from(aws_config.endpoint.as_deref()),
796            Datum::from(aws_config.region.as_deref()),
797            Datum::from(access_key_id),
798            Datum::from(access_key_id_secret_id.as_deref()),
799            Datum::from(secret_access_key_secret_id.as_deref()),
800            Datum::from(session_token),
801            Datum::from(session_token_secret_id.as_deref()),
802            Datum::from(assume_role_arn),
803            Datum::from(assume_role_session_name),
804            Datum::from(principal),
805            Datum::from(external_id.as_deref()),
806            Datum::from(example_trust_policy.as_ref().map(|p| p.into_element())),
807        ]);
808
809        Ok(BuiltinTableUpdate::row(id, row, diff))
810    }
811
812    fn pack_view_update(
813        &self,
814        id: CatalogItemId,
815        oid: u32,
816        schema_id: &SchemaSpecifier,
817        name: &str,
818        owner_id: &RoleId,
819        privileges: Datum,
820        view: &View,
821        diff: Diff,
822    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
823        let create_stmt = mz_sql::parse::parse(&view.create_sql)
824            .unwrap_or_else(|e| {
825                panic!(
826                    "create_sql cannot be invalid: `{}` --- error: `{}`",
827                    view.create_sql, e
828                )
829            })
830            .into_element()
831            .ast;
832        let query = match &create_stmt {
833            Statement::CreateView(stmt) => &stmt.definition.query,
834            _ => unreachable!(),
835        };
836
837        let mut query_string = query.to_ast_string_stable();
838        // PostgreSQL appends a semicolon in `pg_views.definition`, we
839        // do the same for compatibility's sake.
840        query_string.push(';');
841
842        vec![BuiltinTableUpdate::row(
843            &*MZ_VIEWS,
844            Row::pack_slice(&[
845                Datum::String(&id.to_string()),
846                Datum::UInt32(oid),
847                Datum::String(&schema_id.to_string()),
848                Datum::String(name),
849                Datum::String(&query_string),
850                Datum::String(&owner_id.to_string()),
851                privileges,
852                Datum::String(&view.create_sql),
853                Datum::String(&create_stmt.to_ast_string_redacted()),
854            ]),
855            diff,
856        )]
857    }
858
859    fn pack_materialized_view_update(
860        &self,
861        id: CatalogItemId,
862        mview: &MaterializedView,
863        diff: Diff,
864    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
865        let mut updates = Vec::new();
866
867        if let Some(refresh_schedule) = &mview.refresh_schedule {
868            // This can't be `ON COMMIT`, because that is represented by a `None` instead of an
869            // empty `RefreshSchedule`.
870            assert!(!refresh_schedule.is_empty());
871            for RefreshEvery {
872                interval,
873                aligned_to,
874            } in refresh_schedule.everies.iter()
875            {
876                let aligned_to_dt = mz_ore::now::to_datetime(
877                    <&Timestamp as TryInto<u64>>::try_into(aligned_to).expect("undoes planning"),
878                );
879                updates.push(BuiltinTableUpdate::row(
880                    &*MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES,
881                    Row::pack_slice(&[
882                        Datum::String(&id.to_string()),
883                        Datum::String("every"),
884                        Datum::Interval(
885                            Interval::from_duration(interval).expect(
886                                "planning ensured that this is convertible back to Interval",
887                            ),
888                        ),
889                        Datum::TimestampTz(aligned_to_dt.try_into().expect("undoes planning")),
890                        Datum::Null,
891                    ]),
892                    diff,
893                ));
894            }
895            for at in refresh_schedule.ats.iter() {
896                let at_dt = mz_ore::now::to_datetime(
897                    <&Timestamp as TryInto<u64>>::try_into(at).expect("undoes planning"),
898                );
899                updates.push(BuiltinTableUpdate::row(
900                    &*MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES,
901                    Row::pack_slice(&[
902                        Datum::String(&id.to_string()),
903                        Datum::String("at"),
904                        Datum::Null,
905                        Datum::Null,
906                        Datum::TimestampTz(at_dt.try_into().expect("undoes planning")),
907                    ]),
908                    diff,
909                ));
910            }
911        } else {
912            updates.push(BuiltinTableUpdate::row(
913                &*MZ_MATERIALIZED_VIEW_REFRESH_STRATEGIES,
914                Row::pack_slice(&[
915                    Datum::String(&id.to_string()),
916                    Datum::String("on-commit"),
917                    Datum::Null,
918                    Datum::Null,
919                    Datum::Null,
920                ]),
921                diff,
922            ));
923        }
924
925        if let Some(target_id) = mview.replacement_target {
926            updates.push(BuiltinTableUpdate::row(
927                &*MZ_REPLACEMENTS,
928                Row::pack_slice(&[
929                    Datum::String(&id.to_string()),
930                    Datum::String(&target_id.to_string()),
931                ]),
932                diff,
933            ));
934        }
935
936        updates
937    }
938
939    fn pack_sink_update(
940        &self,
941        id: CatalogItemId,
942        oid: u32,
943        schema_id: &SchemaSpecifier,
944        name: &str,
945        owner_id: &RoleId,
946        sink: &Sink,
947        diff: Diff,
948    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
949        let mut updates = vec![];
950        match &sink.connection {
951            StorageSinkConnection::Kafka(KafkaSinkConnection {
952                topic: topic_name, ..
953            }) => {
954                updates.push(BuiltinTableUpdate::row(
955                    &*MZ_KAFKA_SINKS,
956                    Row::pack_slice(&[
957                        Datum::String(&id.to_string()),
958                        Datum::String(topic_name.as_str()),
959                    ]),
960                    diff,
961                ));
962            }
963            StorageSinkConnection::Iceberg(IcebergSinkConnection {
964                namespace, table, ..
965            }) => {
966                updates.push(BuiltinTableUpdate::row(
967                    &*MZ_ICEBERG_SINKS,
968                    Row::pack_slice(&[
969                        Datum::String(&id.to_string()),
970                        Datum::String(namespace.as_str()),
971                        Datum::String(table.as_str()),
972                    ]),
973                    diff,
974                ));
975            }
976        };
977
978        let create_stmt = mz_sql::parse::parse(&sink.create_sql)
979            .unwrap_or_else(|_| panic!("create_sql cannot be invalid: {}", sink.create_sql))
980            .into_element()
981            .ast;
982
983        let envelope = sink.envelope();
984
985        // The combined format string is used for the deprecated `format` column.
986        let combined_format = sink.combined_format();
987        let (key_format, value_format) = match sink.formats() {
988            Some((key_format, value_format)) => (key_format, Some(value_format)),
989            None => (None, None),
990        };
991
992        updates.push(BuiltinTableUpdate::row(
993            &*MZ_SINKS,
994            Row::pack_slice(&[
995                Datum::String(&id.to_string()),
996                Datum::UInt32(oid),
997                Datum::String(&schema_id.to_string()),
998                Datum::String(name),
999                Datum::String(sink.connection.name()),
1000                Datum::from(sink.connection_id().map(|id| id.to_string()).as_deref()),
1001                // size column now deprecated w/o linked clusters
1002                Datum::Null,
1003                Datum::from(envelope),
1004                // FIXME: These key/value formats are kinda leaky! Should probably live in
1005                // the kafka sink table.
1006                Datum::from(combined_format.as_ref().map(|f| f.as_ref())),
1007                Datum::from(key_format),
1008                Datum::from(value_format),
1009                Datum::String(&sink.cluster_id.to_string()),
1010                Datum::String(&owner_id.to_string()),
1011                Datum::String(&sink.create_sql),
1012                Datum::String(&create_stmt.to_ast_string_redacted()),
1013            ]),
1014            diff,
1015        ));
1016
1017        updates
1018    }
1019
1020    fn pack_index_update(
1021        &self,
1022        id: CatalogItemId,
1023        index: &Index,
1024        diff: Diff,
1025    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
1026        let mut updates = vec![];
1027
1028        let create_stmt = mz_sql::parse::parse(&index.create_sql)
1029            .unwrap_or_else(|e| {
1030                panic!(
1031                    "create_sql cannot be invalid: `{}` --- error: `{}`",
1032                    index.create_sql, e
1033                )
1034            })
1035            .into_element()
1036            .ast;
1037
1038        let key_sqls = match &create_stmt {
1039            Statement::CreateIndex(CreateIndexStatement { key_parts, .. }) => key_parts
1040                .as_ref()
1041                .expect("key_parts is filled in during planning"),
1042            _ => unreachable!(),
1043        };
1044
1045        let on_entry = self.get_entry_by_global_id(&index.on);
1046        let on_desc = on_entry
1047            .relation_desc()
1048            .expect("can only create indexes on items with a valid description");
1049        let repr_col_types: Vec<ReprColumnType> = on_desc
1050            .typ()
1051            .column_types
1052            .iter()
1053            .map(ReprColumnType::from)
1054            .collect();
1055        for (i, key) in index.keys.iter().enumerate() {
1056            let nullable = key.typ(&repr_col_types).nullable;
1057            let seq_in_index = u64::cast_from(i + 1);
1058            let key_sql = key_sqls
1059                .get(i)
1060                .expect("missing sql information for index key")
1061                .to_ast_string_simple();
1062            let (field_number, expression) = match key {
1063                MirScalarExpr::Column(col, _) => {
1064                    (Datum::UInt64(u64::cast_from(*col + 1)), Datum::Null)
1065                }
1066                _ => (Datum::Null, Datum::String(&key_sql)),
1067            };
1068            updates.push(BuiltinTableUpdate::row(
1069                &*MZ_INDEX_COLUMNS,
1070                Row::pack_slice(&[
1071                    Datum::String(&id.to_string()),
1072                    Datum::UInt64(seq_in_index),
1073                    field_number,
1074                    expression,
1075                    Datum::from(nullable),
1076                ]),
1077                diff,
1078            ));
1079        }
1080
1081        updates
1082    }
1083
1084    fn pack_type_update(
1085        &self,
1086        id: CatalogItemId,
1087        oid: u32,
1088        schema_id: &SchemaSpecifier,
1089        name: &str,
1090        owner_id: &RoleId,
1091        privileges: Datum,
1092        typ: &Type,
1093        diff: Diff,
1094    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
1095        let mut out = vec![];
1096
1097        let redacted = typ.create_sql.as_ref().map(|create_sql| {
1098            mz_sql::parse::parse(create_sql)
1099                .unwrap_or_else(|_| panic!("create_sql cannot be invalid: {}", create_sql))
1100                .into_element()
1101                .ast
1102                .to_ast_string_redacted()
1103        });
1104
1105        out.push(BuiltinTableUpdate::row(
1106            &*MZ_TYPES,
1107            Row::pack_slice(&[
1108                Datum::String(&id.to_string()),
1109                Datum::UInt32(oid),
1110                Datum::String(&schema_id.to_string()),
1111                Datum::String(name),
1112                Datum::String(&TypeCategory::from_catalog_type(&typ.details.typ).to_string()),
1113                Datum::String(&owner_id.to_string()),
1114                privileges,
1115                if let Some(create_sql) = &typ.create_sql {
1116                    Datum::String(create_sql)
1117                } else {
1118                    Datum::Null
1119                },
1120                if let Some(redacted) = &redacted {
1121                    Datum::String(redacted)
1122                } else {
1123                    Datum::Null
1124                },
1125            ]),
1126            diff,
1127        ));
1128
1129        let mut row = Row::default();
1130        let mut packer = row.packer();
1131
1132        fn append_modifier(packer: &mut RowPacker<'_>, mods: &[i64]) {
1133            if mods.is_empty() {
1134                packer.push(Datum::Null);
1135            } else {
1136                packer.push_list(mods.iter().map(|m| Datum::Int64(*m)));
1137            }
1138        }
1139
1140        let index_id = match &typ.details.typ {
1141            CatalogType::Array {
1142                element_reference: element_id,
1143            } => {
1144                packer.push(Datum::String(&id.to_string()));
1145                packer.push(Datum::String(&element_id.to_string()));
1146                &MZ_ARRAY_TYPES
1147            }
1148            CatalogType::List {
1149                element_reference: element_id,
1150                element_modifiers,
1151            } => {
1152                packer.push(Datum::String(&id.to_string()));
1153                packer.push(Datum::String(&element_id.to_string()));
1154                append_modifier(&mut packer, element_modifiers);
1155                &MZ_LIST_TYPES
1156            }
1157            CatalogType::Map {
1158                key_reference: key_id,
1159                value_reference: value_id,
1160                key_modifiers,
1161                value_modifiers,
1162            } => {
1163                packer.push(Datum::String(&id.to_string()));
1164                packer.push(Datum::String(&key_id.to_string()));
1165                packer.push(Datum::String(&value_id.to_string()));
1166                append_modifier(&mut packer, key_modifiers);
1167                append_modifier(&mut packer, value_modifiers);
1168                &MZ_MAP_TYPES
1169            }
1170            CatalogType::Pseudo => {
1171                packer.push(Datum::String(&id.to_string()));
1172                &MZ_PSEUDO_TYPES
1173            }
1174            _ => {
1175                packer.push(Datum::String(&id.to_string()));
1176                &MZ_BASE_TYPES
1177            }
1178        };
1179        out.push(BuiltinTableUpdate::row(index_id, row, diff));
1180
1181        if let Some(pg_metadata) = &typ.details.pg_metadata {
1182            out.push(BuiltinTableUpdate::row(
1183                &*MZ_TYPE_PG_METADATA,
1184                Row::pack_slice(&[
1185                    Datum::String(&id.to_string()),
1186                    Datum::UInt32(pg_metadata.typinput_oid),
1187                    Datum::UInt32(pg_metadata.typreceive_oid),
1188                ]),
1189                diff,
1190            ));
1191        }
1192
1193        out
1194    }
1195
1196    fn pack_func_update(
1197        &self,
1198        id: CatalogItemId,
1199        schema_id: &SchemaSpecifier,
1200        name: &str,
1201        owner_id: &RoleId,
1202        func: &Func,
1203        diff: Diff,
1204    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
1205        let mut updates = vec![];
1206        for func_impl_details in func.inner.func_impls() {
1207            let arg_type_ids = func_impl_details
1208                .arg_typs
1209                .iter()
1210                .map(|typ| self.get_system_type(typ).id().to_string())
1211                .collect::<Vec<_>>();
1212
1213            let mut row = Row::default();
1214            row.packer()
1215                .try_push_array(
1216                    &[ArrayDimension {
1217                        lower_bound: 1,
1218                        length: arg_type_ids.len(),
1219                    }],
1220                    arg_type_ids.iter().map(|id| Datum::String(id)),
1221                )
1222                .expect(
1223                    "arg_type_ids is 1 dimensional, and its length is used for the array length",
1224                );
1225            let arg_type_ids = row.unpack_first();
1226
1227            updates.push(BuiltinTableUpdate::row(
1228                &*MZ_FUNCTIONS,
1229                Row::pack_slice(&[
1230                    Datum::String(&id.to_string()),
1231                    Datum::UInt32(func_impl_details.oid),
1232                    Datum::String(&schema_id.to_string()),
1233                    Datum::String(name),
1234                    arg_type_ids,
1235                    Datum::from(
1236                        func_impl_details
1237                            .variadic_typ
1238                            .map(|typ| self.get_system_type(typ).id().to_string())
1239                            .as_deref(),
1240                    ),
1241                    Datum::from(
1242                        func_impl_details
1243                            .return_typ
1244                            .map(|typ| self.get_system_type(typ).id().to_string())
1245                            .as_deref(),
1246                    ),
1247                    func_impl_details.return_is_set.into(),
1248                    Datum::String(&owner_id.to_string()),
1249                ]),
1250                diff,
1251            ));
1252
1253            if let mz_sql::func::Func::Aggregate(_) = func.inner {
1254                updates.push(BuiltinTableUpdate::row(
1255                    &*MZ_AGGREGATES,
1256                    Row::pack_slice(&[
1257                        Datum::UInt32(func_impl_details.oid),
1258                        // TODO(database-issues#1064): Support ordered-set aggregate functions.
1259                        Datum::String("n"),
1260                        Datum::Int16(0),
1261                    ]),
1262                    diff,
1263                ));
1264            }
1265        }
1266        updates
1267    }
1268
1269    pub fn pack_op_update(
1270        &self,
1271        operator: &str,
1272        func_impl_details: FuncImplCatalogDetails,
1273        diff: Diff,
1274    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
1275        let arg_type_ids = func_impl_details
1276            .arg_typs
1277            .iter()
1278            .map(|typ| self.get_system_type(typ).id().to_string())
1279            .collect::<Vec<_>>();
1280
1281        let mut row = Row::default();
1282        row.packer()
1283            .try_push_array(
1284                &[ArrayDimension {
1285                    lower_bound: 1,
1286                    length: arg_type_ids.len(),
1287                }],
1288                arg_type_ids.iter().map(|id| Datum::String(id)),
1289            )
1290            .expect("arg_type_ids is 1 dimensional, and its length is used for the array length");
1291        let arg_type_ids = row.unpack_first();
1292
1293        BuiltinTableUpdate::row(
1294            &*MZ_OPERATORS,
1295            Row::pack_slice(&[
1296                Datum::UInt32(func_impl_details.oid),
1297                Datum::String(operator),
1298                arg_type_ids,
1299                Datum::from(
1300                    func_impl_details
1301                        .return_typ
1302                        .map(|typ| self.get_system_type(typ).id().to_string())
1303                        .as_deref(),
1304                ),
1305            ]),
1306            diff,
1307        )
1308    }
1309
1310    pub fn pack_storage_usage_update(
1311        &self,
1312        VersionedStorageUsage::V1(event): VersionedStorageUsage,
1313        diff: Diff,
1314    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
1315        let id = &MZ_STORAGE_USAGE_BY_SHARD;
1316        let row = Row::pack_slice(&[
1317            Datum::UInt64(event.id),
1318            Datum::from(event.shard_id.as_deref()),
1319            Datum::UInt64(event.size_bytes),
1320            Datum::TimestampTz(
1321                mz_ore::now::to_datetime(event.collection_timestamp)
1322                    .try_into()
1323                    .expect("must fit"),
1324            ),
1325        ]);
1326        BuiltinTableUpdate::row(id, row, diff)
1327    }
1328
1329    pub fn pack_egress_ip_update(
1330        &self,
1331        ip: &IpNet,
1332    ) -> Result<BuiltinTableUpdate<&'static BuiltinTable>, Error> {
1333        let id = &MZ_EGRESS_IPS;
1334        let addr = ip.network();
1335        let row = Row::pack_slice(&[
1336            Datum::String(&addr.to_string()),
1337            Datum::Int32(ip.prefix_len().into()),
1338            Datum::String(&format!("{}/{}", addr, ip.prefix_len())),
1339        ]);
1340        Ok(BuiltinTableUpdate::row(id, row, Diff::ONE))
1341    }
1342
1343    pub fn pack_license_key_update(
1344        &self,
1345        license_key: &ValidatedLicenseKey,
1346    ) -> Result<BuiltinTableUpdate<&'static BuiltinTable>, Error> {
1347        let id = &MZ_LICENSE_KEYS;
1348        let row = Row::pack_slice(&[
1349            Datum::String(&license_key.id),
1350            Datum::String(&license_key.organization),
1351            Datum::String(&license_key.environment_id),
1352            Datum::TimestampTz(
1353                mz_ore::now::to_datetime(license_key.expiration * 1000)
1354                    .try_into()
1355                    .expect("must fit"),
1356            ),
1357            Datum::TimestampTz(
1358                mz_ore::now::to_datetime(license_key.not_before * 1000)
1359                    .try_into()
1360                    .expect("must fit"),
1361            ),
1362        ]);
1363        Ok(BuiltinTableUpdate::row(id, row, Diff::ONE))
1364    }
1365
1366    pub fn pack_all_replica_size_updates(&self) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
1367        let mut updates = Vec::new();
1368        for (size, alloc) in &self.cluster_replica_sizes.0 {
1369            // Just invent something when the limits are `None`, which only happens in non-prod
1370            // environments (tests, process orchestrator, etc.)
1371            let DiskLimit(ByteSize(disk_bytes)) =
1372                (alloc.disk_limit).unwrap_or(DiskLimit::ARBITRARY);
1373
1374            // The disk column of mz_clusters / mz_cluster_replicas MVs needs
1375            // `swap_enabled` and `disk_bytes`; expose them through a parallel
1376            // internal table. Unlike the public sizes table below, we write
1377            // here unconditionally — `cluster_replica_size_has_disk` previously
1378            // indexed the in-memory map without checking `disabled`, so a
1379            // managed cluster pinned to a disabled size still resolved its
1380            // `disk` column from the real allocation. Writing disabled rows
1381            // here preserves that behavior.
1382            let internal_row = Row::pack_slice(&[
1383                size.as_str().into(),
1384                Datum::from(alloc.swap_enabled),
1385                disk_bytes.into(),
1386            ]);
1387            updates.push(BuiltinTableUpdate::row(
1388                &*MZ_CLUSTER_REPLICA_SIZE_INTERNAL,
1389                internal_row,
1390                Diff::ONE,
1391            ));
1392
1393            if alloc.disabled {
1394                continue;
1395            }
1396
1397            let cpu_limit = alloc.cpu_limit.unwrap_or(CpuLimit::MAX);
1398            let MemoryLimit(ByteSize(memory_bytes)) =
1399                (alloc.memory_limit).unwrap_or(MemoryLimit::MAX);
1400
1401            let row = Row::pack_slice(&[
1402                size.as_str().into(),
1403                u64::cast_from(alloc.scale).into(),
1404                u64::cast_from(alloc.workers).into(),
1405                cpu_limit.as_nanocpus().into(),
1406                memory_bytes.into(),
1407                disk_bytes.into(),
1408                (alloc.credits_per_hour).into(),
1409            ]);
1410
1411            updates.push(BuiltinTableUpdate::row(
1412                &*MZ_CLUSTER_REPLICA_SIZES,
1413                row,
1414                Diff::ONE,
1415            ));
1416        }
1417
1418        updates
1419    }
1420
1421    pub fn pack_subscribe_update(
1422        &self,
1423        id: GlobalId,
1424        subscribe: &ActiveSubscribe,
1425        diff: Diff,
1426    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
1427        let mut row = Row::default();
1428        let mut packer = row.packer();
1429        packer.push(Datum::String(&id.to_string()));
1430        packer.push(Datum::Uuid(subscribe.session_uuid));
1431        packer.push(Datum::String(&subscribe.cluster_id.to_string()));
1432
1433        let start_dt = mz_ore::now::to_datetime(subscribe.start_time);
1434        packer.push(Datum::TimestampTz(start_dt.try_into().expect("must fit")));
1435
1436        let depends_on: Vec<_> = subscribe
1437            .depends_on
1438            .iter()
1439            .map(|id| id.to_string())
1440            .collect();
1441        packer.push_list(depends_on.iter().map(|s| Datum::String(s)));
1442
1443        BuiltinTableUpdate::row(&*MZ_SUBSCRIPTIONS, row, diff)
1444    }
1445
1446    pub fn pack_session_update(
1447        &self,
1448        conn: &ConnMeta,
1449        diff: Diff,
1450    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
1451        let connect_dt = mz_ore::now::to_datetime(conn.connected_at());
1452        BuiltinTableUpdate::row(
1453            &*MZ_SESSIONS,
1454            Row::pack_slice(&[
1455                Datum::Uuid(conn.uuid()),
1456                Datum::UInt32(conn.conn_id().unhandled()),
1457                Datum::String(&conn.authenticated_role_id().to_string()),
1458                Datum::from(conn.client_ip().map(|ip| ip.to_string()).as_deref()),
1459                Datum::TimestampTz(connect_dt.try_into().expect("must fit")),
1460            ]),
1461            diff,
1462        )
1463    }
1464
1465    fn pack_privilege_array_row(&self, privileges: &PrivilegeMap) -> Row {
1466        let mut row = Row::default();
1467        let flat_privileges: Vec<_> = privileges.all_values_owned().collect();
1468        row.packer()
1469            .try_push_array(
1470                &[ArrayDimension {
1471                    lower_bound: 1,
1472                    length: flat_privileges.len(),
1473                }],
1474                flat_privileges
1475                    .into_iter()
1476                    .map(|mz_acl_item| Datum::MzAclItem(mz_acl_item.clone())),
1477            )
1478            .expect("privileges is 1 dimensional, and its length is used for the array length");
1479        row
1480    }
1481
1482    pub fn pack_webhook_source_update(
1483        &self,
1484        item_id: CatalogItemId,
1485        diff: Diff,
1486    ) -> BuiltinTableUpdate<&'static BuiltinTable> {
1487        let url = self
1488            .try_get_webhook_url(&item_id)
1489            .expect("webhook source should exist");
1490        let url = url.to_string();
1491        let name = &self.get_entry(&item_id).name().item;
1492        let id_str = item_id.to_string();
1493
1494        BuiltinTableUpdate::row(
1495            &*MZ_WEBHOOKS_SOURCES,
1496            Row::pack_slice(&[
1497                Datum::String(&id_str),
1498                Datum::String(name),
1499                Datum::String(&url),
1500            ]),
1501            diff,
1502        )
1503    }
1504
1505    pub fn pack_source_references_update(
1506        &self,
1507        source_references: &SourceReferences,
1508        diff: Diff,
1509    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
1510        let source_id = source_references.source_id.to_string();
1511        let updated_at = &source_references.updated_at;
1512        source_references
1513            .references
1514            .iter()
1515            .map(|reference| {
1516                let mut row = Row::default();
1517                let mut packer = row.packer();
1518                packer.extend([
1519                    Datum::String(&source_id),
1520                    reference
1521                        .namespace
1522                        .as_ref()
1523                        .map(|s| Datum::String(s))
1524                        .unwrap_or(Datum::Null),
1525                    Datum::String(&reference.name),
1526                    Datum::TimestampTz(
1527                        mz_ore::now::to_datetime(*updated_at)
1528                            .try_into()
1529                            .expect("must fit"),
1530                    ),
1531                ]);
1532                if reference.columns.len() > 0 {
1533                    packer
1534                        .try_push_array(
1535                            &[ArrayDimension {
1536                                lower_bound: 1,
1537                                length: reference.columns.len(),
1538                            }],
1539                            reference.columns.iter().map(|col| Datum::String(col)),
1540                        )
1541                        .expect(
1542                            "columns is 1 dimensional, and its length is used for the array length",
1543                        );
1544                } else {
1545                    packer.push(Datum::Null);
1546                }
1547
1548                BuiltinTableUpdate::row(&*MZ_SOURCE_REFERENCES, row, diff)
1549            })
1550            .collect()
1551    }
1552}