Skip to main content

mz_adapter/catalog/
apply.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
10//! Logic related to applying updates from a [`mz_catalog::durable::DurableCatalogState`] to a
11//! [`CatalogState`].
12
13use std::collections::{BTreeMap, BTreeSet, VecDeque};
14use std::fmt::Debug;
15use std::iter;
16use std::str::FromStr;
17use std::sync::Arc;
18
19use differential_dataflow::consolidation::consolidate_updates;
20use futures::future;
21use itertools::{Either, Itertools};
22use mz_adapter_types::connection::ConnectionId;
23use mz_catalog::SYSTEM_CONN_ID;
24use mz_catalog::builtin::{
25    BUILTIN_LOG_LOOKUP, BUILTIN_LOOKUP, Builtin, BuiltinLog, BuiltinTable, BuiltinView,
26};
27use mz_catalog::durable::objects::{
28    ClusterKey, DatabaseKey, DurableType, ItemKey, NetworkPolicyKey, RoleAuthKey, RoleKey,
29    SchemaKey,
30};
31use mz_catalog::durable::{CatalogError, SystemObjectMapping};
32use mz_catalog::memory::error::{Error, ErrorKind};
33use mz_catalog::memory::objects::{
34    CatalogEntry, CatalogItem, Cluster, ClusterReplica, Database, Func, Index, Log, NetworkPolicy,
35    Role, RoleAuth, Schema, Source, StateDiff, StateUpdate, StateUpdateKind, Table,
36    TableDataSource, Type, UpdateFrom,
37};
38use mz_compute_types::config::ComputeReplicaConfig;
39use mz_compute_types::dataflows::DataflowDescription;
40use mz_controller::clusters::{ReplicaConfig, ReplicaLogging};
41use mz_controller_types::ClusterId;
42use mz_expr::MirScalarExpr;
43use mz_ore::collections::CollectionExt;
44use mz_ore::tracing::OpenTelemetryContext;
45use mz_ore::{
46    instrument, soft_assert_eq_or_log, soft_assert_no_log, soft_assert_or_log, soft_panic_or_log,
47};
48use mz_pgrepr::oid::INVALID_OID;
49use mz_repr::adt::mz_acl_item::{MzAclItem, PrivilegeMap};
50use mz_repr::role_id::RoleId;
51use mz_repr::{CatalogItemId, Diff, GlobalId, RelationVersion, Timestamp, VersionedRelationDesc};
52use mz_sql::catalog::CatalogError as SqlCatalogError;
53use mz_sql::catalog::{CatalogItem as SqlCatalogItem, CatalogItemType, CatalogSchema, CatalogType};
54use mz_sql::names::{
55    FullItemName, ItemQualifiers, QualifiedItemName, RawDatabaseSpecifier,
56    ResolvedDatabaseSpecifier, ResolvedIds, SchemaSpecifier,
57};
58use mz_sql::session::user::MZ_SYSTEM_ROLE_ID;
59use mz_sql::session::vars::{VarError, VarInput};
60use mz_sql::{plan, rbac};
61use mz_sql_parser::ast::Expr;
62use mz_storage_types::sources::Timeline;
63use mz_transform::dataflow::DataflowMetainfo;
64use mz_transform::notice::OptimizerNotice;
65use tracing::{info_span, warn};
66
67use crate::AdapterError;
68use crate::catalog::state::LocalExpressionCache;
69use crate::catalog::{BuiltinTableUpdate, CatalogState};
70use crate::coord::catalog_implications::parsed_state_updates::{self, ParsedStateUpdate};
71use crate::util::{index_sql, sort_topological};
72
73/// Maintains the state of retractions while applying catalog state updates for a single timestamp.
74/// [`CatalogState`] maintains denormalized state for certain catalog objects. Updating an object
75/// results in applying a retraction for that object followed by applying an addition for that
76/// object. When applying those additions it can be extremely expensive to re-build that
77/// denormalized state from scratch. To avoid that issue we stash the denormalized state from
78/// retractions, so it can be used during additions.
79///
80/// Not all objects maintain denormalized state, so we only stash the retractions for the subset of
81/// objects that maintain denormalized state.
82// TODO(jkosh44) It might be simpler or more future proof to include all object types here, even if
83// the update step is a no-op for certain types.
84#[derive(Debug, Clone, Default)]
85struct InProgressRetractions {
86    roles: BTreeMap<RoleKey, Role>,
87    role_auths: BTreeMap<RoleAuthKey, RoleAuth>,
88    databases: BTreeMap<DatabaseKey, Database>,
89    schemas: BTreeMap<SchemaKey, Schema>,
90    clusters: BTreeMap<ClusterKey, Cluster>,
91    network_policies: BTreeMap<NetworkPolicyKey, NetworkPolicy>,
92    items: BTreeMap<ItemKey, CatalogEntry>,
93    introspection_source_indexes: BTreeMap<CatalogItemId, CatalogEntry>,
94    system_object_mappings: BTreeMap<CatalogItemId, CatalogEntry>,
95}
96
97impl CatalogState {
98    /// Update in-memory catalog state from a list of updates made to the durable catalog state.
99    ///
100    /// Returns builtin table updates corresponding to the changes to catalog state.
101    #[must_use]
102    #[instrument]
103    pub(crate) async fn apply_updates(
104        &mut self,
105        updates: Vec<StateUpdate>,
106        local_expression_cache: &mut LocalExpressionCache,
107    ) -> (
108        Vec<BuiltinTableUpdate<&'static BuiltinTable>>,
109        Vec<ParsedStateUpdate>,
110    ) {
111        let mut builtin_table_updates = Vec::with_capacity(updates.len());
112        let mut catalog_updates = Vec::with_capacity(updates.len());
113
114        // First, consolidate updates. The code that applies parsed state
115        // updates _requires_ that the given updates are consolidated. There
116        // must be at most one addition and/or one retraction for a given item,
117        // as identified by that items ID type.
118        let updates = Self::consolidate_updates(updates);
119
120        // Apply updates in groups, according to their timestamps.
121        let mut groups: Vec<Vec<_>> = Vec::new();
122        for (_, updates) in &updates.into_iter().chunk_by(|update| update.ts) {
123            // Bring the updates into the pseudo-topological order that we need
124            // for updating our in-memory state and generating builtin table
125            // updates.
126            let updates = sort_updates(updates.collect());
127            groups.push(updates);
128        }
129
130        for updates in groups {
131            let mut apply_state = ApplyState::Updates(Vec::new());
132            let mut retractions = InProgressRetractions::default();
133
134            for update in updates {
135                let (next_apply_state, (builtin_table_update, catalog_update)) = apply_state
136                    .step(
137                        ApplyState::new(update),
138                        self,
139                        &mut retractions,
140                        local_expression_cache,
141                    )
142                    .await;
143                apply_state = next_apply_state;
144                builtin_table_updates.extend(builtin_table_update);
145                catalog_updates.extend(catalog_update);
146            }
147
148            // Apply remaining state.
149            let (builtin_table_update, catalog_update) = apply_state
150                .apply(self, &mut retractions, local_expression_cache)
151                .await;
152            builtin_table_updates.extend(builtin_table_update);
153            catalog_updates.extend(catalog_update);
154
155            // Clean up plans and optimizer notices for items that
156            // were retracted but not replaced (i.e., truly dropped).
157            let dropped_entries: Vec<CatalogEntry> = retractions.items.into_values().collect();
158            if !dropped_entries.is_empty() {
159                let dropped_notices = self.drop_optimizer_notices(dropped_entries);
160                if self.system_config().enable_mz_notices() {
161                    self.pack_optimizer_notice_updates(
162                        &mut builtin_table_updates,
163                        dropped_notices.iter(),
164                        Diff::MINUS_ONE,
165                    );
166                }
167            }
168        }
169
170        (builtin_table_updates, catalog_updates)
171    }
172
173    /// It can happen that the sequencing logic creates "fluctuating" updates
174    /// for a given catalog ID. For example, when doing a `DROP OWNED BY ...`,
175    /// for a table, there will be a retraction of the original table state,
176    /// then an addition for the same table but stripped of some of the roles
177    /// and access things, and then a retraction for that intermediate table
178    /// state. By consolidating, the intermediate state addition/retraction will
179    /// cancel out and we'll only see the retraction for the original state.
180    fn consolidate_updates(updates: Vec<StateUpdate>) -> Vec<StateUpdate> {
181        let mut updates: Vec<(StateUpdateKind, Timestamp, mz_repr::Diff)> = updates
182            .into_iter()
183            .map(|update| (update.kind, update.ts, update.diff.into()))
184            .collect_vec();
185
186        consolidate_updates(&mut updates);
187
188        updates
189            .into_iter()
190            .map(|(kind, ts, diff)| StateUpdate {
191                kind,
192                ts,
193                diff: diff
194                    .try_into()
195                    .expect("catalog state cannot have diff other than -1 or 1"),
196            })
197            .collect_vec()
198    }
199
200    #[instrument(level = "debug")]
201    fn apply_updates_inner(
202        &mut self,
203        updates: Vec<StateUpdate>,
204        retractions: &mut InProgressRetractions,
205        local_expression_cache: &mut LocalExpressionCache,
206    ) -> Result<
207        (
208            Vec<BuiltinTableUpdate<&'static BuiltinTable>>,
209            Vec<ParsedStateUpdate>,
210        ),
211        CatalogError,
212    > {
213        soft_assert_no_log!(
214            updates.iter().map(|update| update.ts).all_equal(),
215            "all timestamps should be equal: {updates:?}"
216        );
217
218        let mut update_system_config = false;
219
220        let mut builtin_table_updates = Vec::with_capacity(updates.len());
221        let mut catalog_updates = Vec::new();
222
223        for state_update in updates {
224            // Do not apply temporary item updates from other processes, since
225            // temporary items are scoped per session, which must be in the
226            // same process.
227            if self.is_nonlocal_ephemeral_item_update(&state_update) {
228                tracing::debug!(
229                    ?state_update,
230                    "skipping ephemeral item update for non-local session"
231                );
232                continue;
233            }
234
235            if matches!(state_update.kind, StateUpdateKind::SystemConfiguration(_)) {
236                update_system_config = true;
237            }
238
239            match state_update.diff {
240                StateDiff::Retraction => {
241                    // We want the parsed catalog updates to match the state of
242                    // the catalog _before_ applying a retraction. So that we can
243                    // still have useful in-memory state to work with.
244                    if let Some(update) =
245                        parsed_state_updates::parse_state_update(self, state_update.clone())
246                    {
247                        catalog_updates.push(update);
248                    }
249
250                    // We want the builtin table retraction to match the state of the catalog
251                    // before applying the update.
252                    builtin_table_updates.extend(self.generate_builtin_table_update(
253                        state_update.kind.clone(),
254                        state_update.diff,
255                    ));
256                    self.apply_update(
257                        state_update.kind,
258                        state_update.diff,
259                        retractions,
260                        local_expression_cache,
261                    )?;
262                }
263                StateDiff::Addition => {
264                    self.apply_update(
265                        state_update.kind.clone(),
266                        state_update.diff,
267                        retractions,
268                        local_expression_cache,
269                    )?;
270                    // We want the builtin table addition to match the state of
271                    // the catalog after applying the update. So that we already
272                    // have useful in-memory state to work with.
273                    builtin_table_updates.extend(self.generate_builtin_table_update(
274                        state_update.kind.clone(),
275                        state_update.diff,
276                    ));
277
278                    // We want the parsed catalog updates to match the state of
279                    // the catalog _after_ applying an addition.
280                    if let Some(update) =
281                        parsed_state_updates::parse_state_update(self, state_update.clone())
282                    {
283                        catalog_updates.push(update);
284                    }
285                }
286            }
287        }
288
289        if update_system_config {
290            self.system_configuration.sync_dyncfgs();
291        }
292
293        Ok((builtin_table_updates, catalog_updates))
294    }
295
296    #[instrument(level = "debug")]
297    fn apply_update(
298        &mut self,
299        kind: StateUpdateKind,
300        diff: StateDiff,
301        retractions: &mut InProgressRetractions,
302        local_expression_cache: &mut LocalExpressionCache,
303    ) -> Result<(), CatalogError> {
304        match kind {
305            StateUpdateKind::Role(role) => {
306                self.apply_role_update(role, diff, retractions);
307            }
308            StateUpdateKind::RoleAuth(role_auth) => {
309                self.apply_role_auth_update(role_auth, diff, retractions);
310            }
311            StateUpdateKind::Database(database) => {
312                self.apply_database_update(database, diff, retractions);
313            }
314            StateUpdateKind::Schema(schema) => {
315                self.apply_schema_update(schema, diff, retractions);
316            }
317            StateUpdateKind::DefaultPrivilege(default_privilege) => {
318                self.apply_default_privilege_update(default_privilege, diff, retractions);
319            }
320            StateUpdateKind::SystemPrivilege(system_privilege) => {
321                self.apply_system_privilege_update(system_privilege, diff, retractions);
322            }
323            StateUpdateKind::SystemConfiguration(system_configuration) => {
324                self.apply_system_configuration_update(system_configuration, diff, retractions);
325            }
326            StateUpdateKind::ClusterSystemConfiguration(cfg) => {
327                Self::apply_scoped_system_configuration_update(
328                    &mut self.scoped_system_parameters.cluster,
329                    cfg.cluster_id,
330                    cfg.name,
331                    cfg.value,
332                    diff,
333                );
334            }
335            StateUpdateKind::ReplicaSystemConfiguration(cfg) => {
336                Self::apply_scoped_system_configuration_update(
337                    &mut self.scoped_system_parameters.replica,
338                    cfg.replica_id,
339                    cfg.name,
340                    cfg.value,
341                    diff,
342                );
343            }
344            StateUpdateKind::Cluster(cluster) => {
345                self.apply_cluster_update(cluster, diff, retractions);
346            }
347            StateUpdateKind::NetworkPolicy(network_policy) => {
348                self.apply_network_policy_update(network_policy, diff, retractions);
349            }
350            StateUpdateKind::ClusterReplicaSize(size) => {
351                self.apply_cluster_replica_size_update(size, diff);
352            }
353            StateUpdateKind::IntrospectionSourceIndex(introspection_source_index) => {
354                self.apply_introspection_source_index_update(
355                    introspection_source_index,
356                    diff,
357                    retractions,
358                );
359            }
360            StateUpdateKind::ClusterReplica(cluster_replica) => {
361                self.apply_cluster_replica_update(cluster_replica, diff, retractions);
362            }
363            StateUpdateKind::SystemObjectMapping(system_object_mapping) => {
364                self.apply_system_object_mapping_update(
365                    system_object_mapping,
366                    diff,
367                    retractions,
368                    local_expression_cache,
369                );
370            }
371            StateUpdateKind::Item(item) => {
372                self.apply_item_update(item, diff, retractions, local_expression_cache)?;
373            }
374            StateUpdateKind::Comment(comment) => {
375                self.apply_comment_update(comment, diff, retractions);
376            }
377            StateUpdateKind::SourceReferences(source_reference) => {
378                self.apply_source_references_update(source_reference, diff, retractions);
379            }
380            StateUpdateKind::AuditLog(_audit_log) => {
381                // Audit logs are not stored in-memory.
382            }
383            StateUpdateKind::StorageCollectionMetadata(storage_collection_metadata) => {
384                self.apply_storage_collection_metadata_update(
385                    storage_collection_metadata,
386                    diff,
387                    retractions,
388                );
389            }
390            StateUpdateKind::UnfinalizedShard(unfinalized_shard) => {
391                self.apply_unfinalized_shard_update(unfinalized_shard, diff, retractions);
392            }
393        }
394
395        Ok(())
396    }
397
398    #[instrument(level = "debug")]
399    fn apply_role_auth_update(
400        &mut self,
401        role_auth: mz_catalog::durable::RoleAuth,
402        diff: StateDiff,
403        retractions: &mut InProgressRetractions,
404    ) {
405        apply_with_update(
406            &mut self.role_auth_by_id,
407            role_auth,
408            |role_auth| role_auth.role_id,
409            diff,
410            &mut retractions.role_auths,
411        );
412    }
413
414    #[instrument(level = "debug")]
415    fn apply_role_update(
416        &mut self,
417        role: mz_catalog::durable::Role,
418        diff: StateDiff,
419        retractions: &mut InProgressRetractions,
420    ) {
421        apply_inverted_lookup(&mut self.roles_by_name, &role.name, role.id, diff);
422        apply_with_update(
423            &mut self.roles_by_id,
424            role,
425            |role| role.id,
426            diff,
427            &mut retractions.roles,
428        );
429    }
430
431    #[instrument(level = "debug")]
432    fn apply_database_update(
433        &mut self,
434        database: mz_catalog::durable::Database,
435        diff: StateDiff,
436        retractions: &mut InProgressRetractions,
437    ) {
438        apply_inverted_lookup(
439            &mut self.database_by_name,
440            &database.name,
441            database.id,
442            diff,
443        );
444        apply_with_update(
445            &mut self.database_by_id,
446            database,
447            |database| database.id,
448            diff,
449            &mut retractions.databases,
450        );
451    }
452
453    #[instrument(level = "debug")]
454    fn apply_schema_update(
455        &mut self,
456        schema: mz_catalog::durable::Schema,
457        diff: StateDiff,
458        retractions: &mut InProgressRetractions,
459    ) {
460        match &schema.database_id {
461            Some(database_id) => {
462                let db = self
463                    .database_by_id
464                    .get_mut(database_id)
465                    .expect("catalog out of sync");
466                apply_inverted_lookup(&mut db.schemas_by_name, &schema.name, schema.id, diff);
467                apply_with_update(
468                    &mut db.schemas_by_id,
469                    schema,
470                    |schema| schema.id,
471                    diff,
472                    &mut retractions.schemas,
473                );
474            }
475            None => {
476                apply_inverted_lookup(
477                    &mut self.ambient_schemas_by_name,
478                    &schema.name,
479                    schema.id,
480                    diff,
481                );
482                apply_with_update(
483                    &mut self.ambient_schemas_by_id,
484                    schema,
485                    |schema| schema.id,
486                    diff,
487                    &mut retractions.schemas,
488                );
489            }
490        }
491    }
492
493    #[instrument(level = "debug")]
494    fn apply_default_privilege_update(
495        &mut self,
496        default_privilege: mz_catalog::durable::DefaultPrivilege,
497        diff: StateDiff,
498        _retractions: &mut InProgressRetractions,
499    ) {
500        match diff {
501            StateDiff::Addition => Arc::make_mut(&mut self.default_privileges)
502                .grant(default_privilege.object, default_privilege.acl_item),
503            StateDiff::Retraction => Arc::make_mut(&mut self.default_privileges)
504                .revoke(&default_privilege.object, &default_privilege.acl_item),
505        }
506    }
507
508    #[instrument(level = "debug")]
509    fn apply_system_privilege_update(
510        &mut self,
511        system_privilege: MzAclItem,
512        diff: StateDiff,
513        _retractions: &mut InProgressRetractions,
514    ) {
515        match diff {
516            StateDiff::Addition => {
517                Arc::make_mut(&mut self.system_privileges).grant(system_privilege)
518            }
519            StateDiff::Retraction => {
520                Arc::make_mut(&mut self.system_privileges).revoke(&system_privilege)
521            }
522        }
523    }
524
525    #[instrument(level = "debug")]
526    fn apply_system_configuration_update(
527        &mut self,
528        system_configuration: mz_catalog::durable::SystemConfiguration,
529        diff: StateDiff,
530        _retractions: &mut InProgressRetractions,
531    ) {
532        let res = match diff {
533            StateDiff::Addition => self.insert_system_configuration(
534                &system_configuration.name,
535                VarInput::Flat(&system_configuration.value),
536            ),
537            StateDiff::Retraction => self.remove_system_configuration(&system_configuration.name),
538        };
539        match res {
540            Ok(_) => (),
541            // When system variables are deleted, nothing deletes them from the underlying
542            // durable catalog, which isn't great. Still, we need to be able to ignore
543            // unknown variables.
544            Err(Error {
545                kind: ErrorKind::VarError(VarError::UnknownParameter(name)),
546            }) => {
547                warn!(%name, "unknown system parameter from catalog storage");
548            }
549            Err(e) => panic!("unable to update system variable: {e:?}"),
550        }
551    }
552
553    /// Applies a durable update to one of the in-memory scoped-parameter working
554    /// copies (`scoped_system_parameters.{cluster,replica}`), keyed by object
555    /// id. Mirrors the durable `{cluster,replica}_system_configurations`
556    /// collections; the cluster copy is consumed at plan time via
557    /// [`CatalogState::cluster_scoped_optimizer_overrides`], the replica copy at
558    /// the compute controller's per-replica dyncfg push.
559    ///
560    /// Retraction is conditional on the value matching, so a value change
561    /// (retraction of the old value + addition of the new) is correct regardless
562    /// of the order the two updates are applied in. See the scoped feature flags
563    /// design.
564    ///
565    /// [`CatalogState::cluster_scoped_optimizer_overrides`]: crate::catalog::CatalogState::cluster_scoped_optimizer_overrides
566    fn apply_scoped_system_configuration_update<Id: Ord>(
567        map: &mut BTreeMap<Id, BTreeMap<String, String>>,
568        id: Id,
569        name: String,
570        value: String,
571        diff: StateDiff,
572    ) {
573        match diff {
574            StateDiff::Addition => {
575                map.entry(id).or_default().insert(name, value);
576            }
577            StateDiff::Retraction => {
578                if let Some(values) = map.get_mut(&id) {
579                    if values.get(&name) == Some(&value) {
580                        values.remove(&name);
581                        if values.is_empty() {
582                            map.remove(&id);
583                        }
584                    }
585                }
586            }
587        }
588    }
589
590    #[instrument(level = "debug")]
591    fn apply_cluster_update(
592        &mut self,
593        cluster: mz_catalog::durable::Cluster,
594        diff: StateDiff,
595        retractions: &mut InProgressRetractions,
596    ) {
597        // Soft signal for a managed cluster whose replica size isn't in
598        // the in-memory size map. The `mz_clusters` MV LEFT JOINs the size
599        // table and resolves `disk` to false when the size is missing, so
600        // this case no longer crashes a managed cluster with RF=0.
601        // A managed cluster with at least one running replica still panics
602        // via `concretize_replica_location` in `apply_cluster_replica_update`.
603        if matches!(diff, StateDiff::Addition) {
604            if let mz_catalog::durable::ClusterVariant::Managed(managed) = &cluster.config.variant {
605                if !self.cluster_replica_sizes.0.contains_key(&managed.size) {
606                    soft_panic_or_log!(
607                        "managed cluster {} ({}) references unknown replica size {:?}; \
608                         mz_clusters.disk will resolve to false",
609                        cluster.name,
610                        cluster.id,
611                        managed.size,
612                    );
613                }
614            }
615        }
616        apply_inverted_lookup(&mut self.clusters_by_name, &cluster.name, cluster.id, diff);
617        apply_with_update(
618            &mut self.clusters_by_id,
619            cluster,
620            |cluster| cluster.id,
621            diff,
622            &mut retractions.clusters,
623        );
624    }
625
626    #[instrument(level = "debug")]
627    fn apply_network_policy_update(
628        &mut self,
629        policy: mz_catalog::durable::NetworkPolicy,
630        diff: StateDiff,
631        retractions: &mut InProgressRetractions,
632    ) {
633        apply_inverted_lookup(
634            &mut self.network_policies_by_name,
635            &policy.name,
636            policy.id,
637            diff,
638        );
639        apply_with_update(
640            &mut self.network_policies_by_id,
641            policy,
642            |policy| policy.id,
643            diff,
644            &mut retractions.network_policies,
645        );
646    }
647
648    #[instrument(level = "debug")]
649    fn apply_cluster_replica_size_update(
650        &mut self,
651        size: mz_catalog::durable::ClusterReplicaSize,
652        diff: StateDiff,
653    ) {
654        let sizes = &mut self.cluster_replica_sizes.0;
655        match diff {
656            StateDiff::Addition => {
657                sizes.insert(size.name, size.allocation);
658            }
659            StateDiff::Retraction => {
660                sizes.remove(&size.name);
661            }
662        }
663    }
664
665    #[instrument(level = "debug")]
666    fn apply_introspection_source_index_update(
667        &mut self,
668        introspection_source_index: mz_catalog::durable::IntrospectionSourceIndex,
669        diff: StateDiff,
670        retractions: &mut InProgressRetractions,
671    ) {
672        let cluster = self
673            .clusters_by_id
674            .get_mut(&introspection_source_index.cluster_id)
675            .expect("catalog out of sync");
676        let log = BUILTIN_LOG_LOOKUP
677            .get(introspection_source_index.name.as_str())
678            .expect("missing log");
679        apply_inverted_lookup(
680            &mut cluster.log_indexes,
681            &log.variant,
682            introspection_source_index.index_id,
683            diff,
684        );
685
686        match diff {
687            StateDiff::Addition => {
688                if let Some(mut entry) = retractions
689                    .introspection_source_indexes
690                    .remove(&introspection_source_index.item_id)
691                {
692                    // This should only happen during startup as a result of builtin migrations. We
693                    // create a new index item and replace the old one with it.
694                    let (index_name, index) = self.create_introspection_source_index(
695                        introspection_source_index.cluster_id,
696                        log,
697                        introspection_source_index.index_id,
698                    );
699                    assert_eq!(entry.id, introspection_source_index.item_id);
700                    assert_eq!(entry.oid, introspection_source_index.oid);
701                    assert_eq!(entry.name, index_name);
702                    entry.item = index;
703                    self.insert_entry(entry);
704                } else {
705                    self.insert_introspection_source_index(
706                        introspection_source_index.cluster_id,
707                        log,
708                        introspection_source_index.item_id,
709                        introspection_source_index.index_id,
710                        introspection_source_index.oid,
711                    );
712                }
713            }
714            StateDiff::Retraction => {
715                let entry = self.drop_item(introspection_source_index.item_id);
716                retractions
717                    .introspection_source_indexes
718                    .insert(entry.id, entry);
719            }
720        }
721    }
722
723    #[instrument(level = "debug")]
724    fn apply_cluster_replica_update(
725        &mut self,
726        cluster_replica: mz_catalog::durable::ClusterReplica,
727        diff: StateDiff,
728        _retractions: &mut InProgressRetractions,
729    ) {
730        let cluster = self
731            .clusters_by_id
732            .get(&cluster_replica.cluster_id)
733            .expect("catalog out of sync");
734
735        // Mirror the cluster-side soft signal: if a managed replica's size
736        // is no longer in the in-memory size map, skip in-memory
737        // registration rather than panicking. The mz_cluster_replicas MV
738        // resolves `disk` from mz_cluster_replica_size_internal (which
739        // retains rows for disabled sizes), so SQL still returns sensible
740        // results. We tolerate disabled sizes here (allow_disabled=true)
741        // because an existing replica must remain queryable even if the
742        // operator has since disabled its size.
743        if let mz_catalog::durable::ReplicaLocation::Managed { size, .. } =
744            &cluster_replica.config.location
745        {
746            if !self.cluster_replica_sizes.0.contains_key(size) {
747                soft_panic_or_log!(
748                    "cluster replica {}.{} ({}) references unknown replica size {:?}; \
749                     skipping in-memory registration",
750                    cluster.name,
751                    cluster_replica.name,
752                    cluster_replica.replica_id,
753                    size,
754                );
755                return;
756            }
757        }
758
759        // Pass no availability-zone override: a rebuild from the durable record
760        // keeps the AZ list the replica was provisioned under, which may differ
761        // from the cluster's current pool while a graceful reconfiguration is in
762        // flight — the cluster controller tells realized- from target-shape
763        // replicas by exactly this list.
764        let location = self
765            .concretize_replica_location(cluster_replica.config.location, &vec![], None, true)
766            .expect("catalog in unexpected state");
767        let cluster = self
768            .clusters_by_id
769            .get_mut(&cluster_replica.cluster_id)
770            .expect("catalog out of sync");
771        apply_inverted_lookup(
772            &mut cluster.replica_id_by_name_,
773            &cluster_replica.name,
774            cluster_replica.replica_id,
775            diff,
776        );
777        match diff {
778            StateDiff::Retraction => {
779                let prev = cluster.replicas_by_id_.remove(&cluster_replica.replica_id);
780                assert!(
781                    prev.is_some(),
782                    "retraction does not match existing value: {:?}",
783                    cluster_replica.replica_id
784                );
785            }
786            StateDiff::Addition => {
787                let logging = ReplicaLogging {
788                    log_logging: cluster_replica.config.logging.log_logging,
789                    interval: cluster_replica.config.logging.interval,
790                };
791                let config = ReplicaConfig {
792                    location,
793                    compute: ComputeReplicaConfig {
794                        logging,
795                        arrangement_compression: cluster_replica.config.arrangement_compression,
796                    },
797                };
798                let mem_cluster_replica = ClusterReplica {
799                    name: cluster_replica.name.clone(),
800                    cluster_id: cluster_replica.cluster_id,
801                    replica_id: cluster_replica.replica_id,
802                    config,
803                    owner_id: cluster_replica.owner_id,
804                };
805                let prev = cluster
806                    .replicas_by_id_
807                    .insert(cluster_replica.replica_id, mem_cluster_replica);
808                assert_eq!(
809                    prev, None,
810                    "values must be explicitly retracted before inserting a new value: {:?}",
811                    cluster_replica.replica_id
812                );
813            }
814        }
815    }
816
817    #[instrument(level = "debug")]
818    fn apply_system_object_mapping_update(
819        &mut self,
820        system_object_mapping: mz_catalog::durable::SystemObjectMapping,
821        diff: StateDiff,
822        retractions: &mut InProgressRetractions,
823        local_expression_cache: &mut LocalExpressionCache,
824    ) {
825        let item_id = system_object_mapping.unique_identifier.catalog_id;
826        let global_id = system_object_mapping.unique_identifier.global_id;
827
828        if system_object_mapping.unique_identifier.runtime_alterable() {
829            // Runtime-alterable system objects have real entries in the items
830            // collection and so get handled through the normal `insert_item`
831            // and `drop_item` code paths.
832            return;
833        }
834
835        if let StateDiff::Retraction = diff {
836            let entry = self.drop_item(item_id);
837            retractions.system_object_mappings.insert(item_id, entry);
838            return;
839        }
840
841        if let Some(entry) = retractions.system_object_mappings.remove(&item_id) {
842            // This implies that we updated the fingerprint for some builtin item. The retraction
843            // was parsed, planned, and optimized using the compiled in definition, not the
844            // definition from a previous version. So we can just stick the old entry back into the
845            // catalog.
846            self.insert_entry(entry);
847            return;
848        }
849
850        let builtin = BUILTIN_LOOKUP
851            .get(&system_object_mapping.description)
852            .expect("missing builtin")
853            .1;
854        let schema_name = builtin.schema();
855        let schema_id = self
856            .ambient_schemas_by_name
857            .get(schema_name)
858            .unwrap_or_else(|| panic!("unknown ambient schema: {schema_name}"));
859        let name = QualifiedItemName {
860            qualifiers: ItemQualifiers {
861                database_spec: ResolvedDatabaseSpecifier::Ambient,
862                schema_spec: SchemaSpecifier::Id(*schema_id),
863            },
864            item: builtin.name().into(),
865        };
866        match builtin {
867            Builtin::Log(log) => {
868                let mut acl_items = vec![rbac::owner_privilege(
869                    mz_sql::catalog::ObjectType::Source,
870                    MZ_SYSTEM_ROLE_ID,
871                )];
872                acl_items.extend_from_slice(&log.access);
873                self.insert_item(
874                    item_id,
875                    log.oid,
876                    name.clone(),
877                    CatalogItem::Log(Log {
878                        variant: log.variant,
879                        global_id,
880                    }),
881                    MZ_SYSTEM_ROLE_ID,
882                    PrivilegeMap::from_mz_acl_items(acl_items),
883                );
884            }
885
886            Builtin::Table(table) => {
887                let mut acl_items = vec![rbac::owner_privilege(
888                    mz_sql::catalog::ObjectType::Table,
889                    MZ_SYSTEM_ROLE_ID,
890                )];
891                acl_items.extend_from_slice(&table.access);
892
893                self.insert_item(
894                    item_id,
895                    table.oid,
896                    name.clone(),
897                    CatalogItem::Table(Table {
898                        create_sql: None,
899                        desc: VersionedRelationDesc::new(table.desc.clone()),
900                        collections: [(RelationVersion::root(), global_id)].into_iter().collect(),
901                        conn_id: None,
902                        resolved_ids: ResolvedIds::empty(),
903                        custom_logical_compaction_window: table.is_retained_metrics_object.then(
904                            || {
905                                self.system_config()
906                                    .metrics_retention()
907                                    .try_into()
908                                    .expect("invalid metrics retention")
909                            },
910                        ),
911                        is_retained_metrics_object: table.is_retained_metrics_object,
912                        data_source: TableDataSource::TableWrites {
913                            defaults: vec![Expr::null(); table.desc.arity()],
914                        },
915                    }),
916                    MZ_SYSTEM_ROLE_ID,
917                    PrivilegeMap::from_mz_acl_items(acl_items),
918                );
919            }
920            Builtin::Index(index) => {
921                let custom_logical_compaction_window =
922                    index.is_retained_metrics_object.then(|| {
923                        self.system_config()
924                            .metrics_retention()
925                            .try_into()
926                            .expect("invalid metrics retention")
927                    });
928                // Indexes can't be versioned.
929                let versions = BTreeMap::new();
930
931                let item = self
932                    .parse_item(
933                        global_id,
934                        &index.create_sql(),
935                        &versions,
936                        None,
937                        index.is_retained_metrics_object,
938                        custom_logical_compaction_window,
939                        local_expression_cache,
940                        None,
941                    )
942                    .unwrap_or_else(|e| {
943                        panic!(
944                            "internal error: failed to load bootstrap index:\n\
945                                    {}\n\
946                                    error:\n\
947                                    {:?}\n\n\
948                                    make sure that the schema name is specified in the builtin index's create sql statement.",
949                            index.name, e
950                        )
951                    });
952                let CatalogItem::Index(_) = item else {
953                    panic!(
954                        "internal error: builtin index {}'s SQL does not begin with \"CREATE INDEX\".",
955                        index.name
956                    );
957                };
958
959                self.insert_item(
960                    item_id,
961                    index.oid,
962                    name,
963                    item,
964                    MZ_SYSTEM_ROLE_ID,
965                    PrivilegeMap::default(),
966                );
967            }
968            Builtin::View(_) => {
969                // parse_views is responsible for inserting all builtin views.
970                unreachable!("views added elsewhere");
971            }
972
973            // Note: Element types must be loaded before array types.
974            Builtin::Type(typ) => {
975                let typ = self.resolve_builtin_type_references(typ);
976                if let CatalogType::Array { element_reference } = typ.details.typ {
977                    let entry = self.get_entry_mut(&element_reference);
978                    let item_type = match &mut entry.item {
979                        CatalogItem::Type(item_type) => item_type,
980                        _ => unreachable!("types can only reference other types"),
981                    };
982                    item_type.details.array_id = Some(item_id);
983                }
984
985                let schema_id = self.resolve_system_schema(typ.schema);
986
987                self.insert_item(
988                    item_id,
989                    typ.oid,
990                    QualifiedItemName {
991                        qualifiers: ItemQualifiers {
992                            database_spec: ResolvedDatabaseSpecifier::Ambient,
993                            schema_spec: SchemaSpecifier::Id(schema_id),
994                        },
995                        item: typ.name.to_owned(),
996                    },
997                    CatalogItem::Type(Type {
998                        create_sql: None,
999                        global_id,
1000                        details: typ.details.clone(),
1001                        resolved_ids: ResolvedIds::empty(),
1002                    }),
1003                    MZ_SYSTEM_ROLE_ID,
1004                    PrivilegeMap::from_mz_acl_items(vec![
1005                        rbac::default_builtin_object_privilege(mz_sql::catalog::ObjectType::Type),
1006                        rbac::owner_privilege(mz_sql::catalog::ObjectType::Type, MZ_SYSTEM_ROLE_ID),
1007                    ]),
1008                );
1009            }
1010
1011            Builtin::Func(func) => {
1012                // This OID is never used. `func` has a `Vec` of implementations and
1013                // each implementation has its own OID. Those are the OIDs that are
1014                // actually used by the system.
1015                let oid = INVALID_OID;
1016                self.insert_item(
1017                    item_id,
1018                    oid,
1019                    name.clone(),
1020                    CatalogItem::Func(Func {
1021                        inner: func.inner,
1022                        global_id,
1023                    }),
1024                    MZ_SYSTEM_ROLE_ID,
1025                    PrivilegeMap::default(),
1026                );
1027            }
1028
1029            Builtin::Source(coll) => {
1030                let mut acl_items = vec![rbac::owner_privilege(
1031                    mz_sql::catalog::ObjectType::Source,
1032                    MZ_SYSTEM_ROLE_ID,
1033                )];
1034                acl_items.extend_from_slice(&coll.access);
1035
1036                self.insert_item(
1037                    item_id,
1038                    coll.oid,
1039                    name.clone(),
1040                    CatalogItem::Source(Source {
1041                        create_sql: None,
1042                        data_source: coll.data_source.clone(),
1043                        desc: coll.desc.clone(),
1044                        global_id,
1045                        timeline: Timeline::EpochMilliseconds,
1046                        resolved_ids: ResolvedIds::empty(),
1047                        custom_logical_compaction_window: coll.is_retained_metrics_object.then(
1048                            || {
1049                                self.system_config()
1050                                    .metrics_retention()
1051                                    .try_into()
1052                                    .expect("invalid metrics retention")
1053                            },
1054                        ),
1055                        is_retained_metrics_object: coll.is_retained_metrics_object,
1056                    }),
1057                    MZ_SYSTEM_ROLE_ID,
1058                    PrivilegeMap::from_mz_acl_items(acl_items),
1059                );
1060            }
1061            Builtin::MaterializedView(mv) => {
1062                let mut acl_items = vec![rbac::owner_privilege(
1063                    mz_sql::catalog::ObjectType::MaterializedView,
1064                    MZ_SYSTEM_ROLE_ID,
1065                )];
1066                acl_items.extend_from_slice(&mv.access);
1067
1068                let custom_logical_compaction_window = mv.is_retained_metrics_object.then(|| {
1069                    self.system_config()
1070                        .metrics_retention()
1071                        .try_into()
1072                        .expect("invalid metrics retention")
1073                });
1074
1075                // Builtin materialized views can't be versioned.
1076                let versions = BTreeMap::new();
1077
1078                let mut item = self
1079                    .parse_item(
1080                        global_id,
1081                        &mv.create_sql(),
1082                        &versions,
1083                        None,
1084                        mv.is_retained_metrics_object,
1085                        custom_logical_compaction_window,
1086                        local_expression_cache,
1087                        None,
1088                    )
1089                    .unwrap_or_else(|e| {
1090                        panic!(
1091                            "internal error: failed to load bootstrap materialized view:\n\
1092                             {}\n\
1093                             error:\n\
1094                             {e:?}\n\n\
1095                             make sure that the schema name is specified in the builtin \
1096                             materialized view's create sql statement.",
1097                            mv.name,
1098                        )
1099                    });
1100                let CatalogItem::MaterializedView(catalog_mv) = &mut item else {
1101                    panic!(
1102                        "internal error: builtin materialized view {}'s SQL does not begin \
1103                         with \"CREATE MATERIALIZED VIEW\".",
1104                        mv.name,
1105                    );
1106                };
1107
1108                // The optimizer can only infer keys from MV definitions, but cannot infer
1109                // uniqueness present in the input data. Extend with the keys declared in the
1110                // builtin definition, to allow supplying additional key knowledge.
1111                let mut desc = catalog_mv.desc.latest();
1112                for key in &mv.desc.typ().keys {
1113                    desc = desc.with_key(key.clone());
1114                }
1115                catalog_mv.desc = VersionedRelationDesc::new(desc);
1116
1117                self.insert_item(
1118                    item_id,
1119                    mv.oid,
1120                    name,
1121                    item,
1122                    MZ_SYSTEM_ROLE_ID,
1123                    PrivilegeMap::from_mz_acl_items(acl_items),
1124                );
1125            }
1126            Builtin::Connection(connection) => {
1127                // Connections can't be versioned.
1128                let versions = BTreeMap::new();
1129                let mut item = self
1130                    .parse_item(
1131                        global_id,
1132                        connection.sql,
1133                        &versions,
1134                        None,
1135                        false,
1136                        None,
1137                        local_expression_cache,
1138                        None,
1139                    )
1140                    .unwrap_or_else(|e| {
1141                        panic!(
1142                            "internal error: failed to load bootstrap connection:\n\
1143                                    {}\n\
1144                                    error:\n\
1145                                    {:?}\n\n\
1146                                    make sure that the schema name is specified in the builtin connection's create sql statement.",
1147                            connection.name, e
1148                        )
1149                    });
1150                let CatalogItem::Connection(_) = &mut item else {
1151                    panic!(
1152                        "internal error: builtin connection {}'s SQL does not begin with \"CREATE CONNECTION\".",
1153                        connection.name
1154                    );
1155                };
1156
1157                let mut acl_items = vec![rbac::owner_privilege(
1158                    mz_sql::catalog::ObjectType::Connection,
1159                    connection.owner_id.clone(),
1160                )];
1161                acl_items.extend_from_slice(connection.access);
1162
1163                self.insert_item(
1164                    item_id,
1165                    connection.oid,
1166                    name.clone(),
1167                    item,
1168                    connection.owner_id.clone(),
1169                    PrivilegeMap::from_mz_acl_items(acl_items),
1170                );
1171            }
1172        }
1173    }
1174
1175    /// Whether `update` concerns an ephemeral item whose owning session is not
1176    /// connected to this process, in which case applying it must be a no-op.
1177    fn is_nonlocal_ephemeral_item_update(&self, update: &StateUpdate) -> bool {
1178        let StateUpdateKind::Item(item) = &update.kind else {
1179            return false;
1180        };
1181        // Only ephemeral items have ephemeral_owner_session
1182        let Some(owner) = item.ephemeral_owner_session else {
1183            return false;
1184        };
1185        match update.diff {
1186            // An addition is local when the owning session is connected here.
1187            StateDiff::Addition => !self.temporary_namespaces.contains_uuid(&owner),
1188            // A retraction must be applied iff the matching addition above
1189            // was applied here, and entry presence records exactly that.
1190            StateDiff::Retraction => !self.entry_by_id.contains_key(&item.id),
1191        }
1192    }
1193
1194    #[instrument(level = "debug")]
1195    fn apply_item_update(
1196        &mut self,
1197        item: mz_catalog::durable::Item,
1198        diff: StateDiff,
1199        retractions: &mut InProgressRetractions,
1200        local_expression_cache: &mut LocalExpressionCache,
1201    ) -> Result<(), CatalogError> {
1202        match diff {
1203            StateDiff::Addition => {
1204                let key = item.key();
1205                let mz_catalog::durable::Item {
1206                    id,
1207                    oid,
1208                    global_id,
1209                    schema_id,
1210                    name,
1211                    create_sql,
1212                    owner_id,
1213                    privileges,
1214                    extra_versions,
1215                    ephemeral_owner_session,
1216                } = item;
1217
1218                // Temporary items live in the temporary schema of the owning
1219                // session's connection, not in the schema named by
1220                // `schema_id` (which is the temporary schema sentinel).
1221                // Updates for sessions not connected to this process are
1222                // filtered out in `apply_updates_inner`, so the mapping must
1223                // exist here.
1224                let conn_id = ephemeral_owner_session.map(|owner| {
1225                    self.temporary_namespaces
1226                        .conn_for_uuid(&owner)
1227                        .cloned()
1228                        .unwrap_or_else(|| {
1229                            panic!("no session record applied for temporary item owner {owner}")
1230                        })
1231                });
1232                let name = match &conn_id {
1233                    Some(_) => QualifiedItemName {
1234                        qualifiers: ItemQualifiers {
1235                            database_spec: ResolvedDatabaseSpecifier::Ambient,
1236                            schema_spec: SchemaSpecifier::Temporary,
1237                        },
1238                        item: name.clone(),
1239                    },
1240                    None => {
1241                        let schema = self.find_non_temp_schema(&schema_id);
1242                        QualifiedItemName {
1243                            qualifiers: ItemQualifiers {
1244                                database_spec: schema.database().clone(),
1245                                schema_spec: schema.id().clone(),
1246                            },
1247                            item: name.clone(),
1248                        }
1249                    }
1250                };
1251                let entry = match retractions.items.remove(&key) {
1252                    Some(mut retraction) => {
1253                        assert_eq!(retraction.id, id);
1254
1255                        // We only reparse the SQL if it's changed. Otherwise, we use the existing
1256                        // item. This is a performance optimization and not needed for correctness.
1257                        // This makes it difficult to use the `UpdateFrom` trait, but the structure
1258                        // is still the same as the trait.
1259                        if retraction.create_sql() != create_sql {
1260                            let mut catalog_item = self
1261                                .deserialize_item(
1262                                    global_id,
1263                                    &create_sql,
1264                                    &extra_versions,
1265                                    local_expression_cache,
1266                                    Some(retraction.item),
1267                                )
1268                                .unwrap_or_else(|e| {
1269                                    panic!("{e:?}: invalid persisted SQL: {create_sql}")
1270                                });
1271                            if conn_id.is_some() {
1272                                // Have to patch up the item because parsing
1273                                // doesn't take into account temporary
1274                                // schemas/conn_id.
1275                                // NOTE(aljoscha): I don't like how we're patching
1276                                // this in here, but it's but one of the ways in
1277                                // which temporary items are a bit weird. So, here
1278                                // we are ...
1279                                catalog_item.set_conn_id(conn_id.clone());
1280                                // Deserializing replans the SQL, and the
1281                                // planner's canonical printing can differ from
1282                                // `create_sql`, for example when a feature flag
1283                                // changed how references are printed since
1284                                // `create_sql` was produced. Keep the exact
1285                                // input. A later op in the same transaction
1286                                // retracts this item by re-serializing it, and
1287                                // that retraction must cancel byte-for-byte
1288                                // against this addition during consolidation,
1289                                // else two retractions of the same id survive
1290                                // and applying them panics.
1291                                catalog_item.set_create_sql(create_sql);
1292                            }
1293                            retraction.item = catalog_item;
1294                        }
1295
1296                        retraction.id = id;
1297                        retraction.oid = oid;
1298                        retraction.name = name;
1299                        retraction.owner_id = owner_id;
1300                        retraction.privileges = PrivilegeMap::from_mz_acl_items(privileges);
1301                        retraction
1302                    }
1303                    None => {
1304                        let mut catalog_item = self
1305                            .deserialize_item(
1306                                global_id,
1307                                &create_sql,
1308                                &extra_versions,
1309                                local_expression_cache,
1310                                None,
1311                            )
1312                            .unwrap_or_else(|e| {
1313                                panic!("{e:?}: invalid persisted SQL: {create_sql}")
1314                            });
1315
1316                        if conn_id.is_some() {
1317                            // See the patch-up comments on the reparse above.
1318                            catalog_item.set_conn_id(conn_id.clone());
1319                            catalog_item.set_create_sql(create_sql);
1320                        }
1321
1322                        CatalogEntry {
1323                            item: catalog_item,
1324                            referenced_by: Vec::new(),
1325                            used_by: Vec::new(),
1326                            id,
1327                            oid,
1328                            name,
1329                            owner_id,
1330                            privileges: PrivilegeMap::from_mz_acl_items(privileges),
1331                        }
1332                    }
1333                };
1334
1335                self.insert_entry(entry);
1336            }
1337            StateDiff::Retraction => {
1338                let entry = self.drop_item(item.id);
1339                let key = item.into_key_value().0;
1340                retractions.items.insert(key, entry);
1341            }
1342        }
1343        Ok(())
1344    }
1345
1346    #[instrument(level = "debug")]
1347    fn apply_comment_update(
1348        &mut self,
1349        comment: mz_catalog::durable::Comment,
1350        diff: StateDiff,
1351        _retractions: &mut InProgressRetractions,
1352    ) {
1353        match diff {
1354            StateDiff::Addition => {
1355                let prev = Arc::make_mut(&mut self.comments).update_comment(
1356                    comment.object_id,
1357                    comment.sub_component,
1358                    Some(comment.comment),
1359                );
1360                assert_eq!(
1361                    prev, None,
1362                    "values must be explicitly retracted before inserting a new value"
1363                );
1364            }
1365            StateDiff::Retraction => {
1366                let prev = Arc::make_mut(&mut self.comments).update_comment(
1367                    comment.object_id,
1368                    comment.sub_component,
1369                    None,
1370                );
1371                assert_eq!(
1372                    prev,
1373                    Some(comment.comment),
1374                    "retraction does not match existing value: ({:?}, {:?})",
1375                    comment.object_id,
1376                    comment.sub_component,
1377                );
1378            }
1379        }
1380    }
1381
1382    #[instrument(level = "debug")]
1383    fn apply_source_references_update(
1384        &mut self,
1385        source_references: mz_catalog::durable::SourceReferences,
1386        diff: StateDiff,
1387        _retractions: &mut InProgressRetractions,
1388    ) {
1389        match diff {
1390            StateDiff::Addition => {
1391                let prev = self
1392                    .source_references
1393                    .insert(source_references.source_id, source_references.into());
1394                assert!(
1395                    prev.is_none(),
1396                    "values must be explicitly retracted before inserting a new value: {prev:?}"
1397                );
1398            }
1399            StateDiff::Retraction => {
1400                let prev = self.source_references.remove(&source_references.source_id);
1401                assert!(
1402                    prev.is_some(),
1403                    "retraction for a non-existent existing value: {source_references:?}"
1404                );
1405            }
1406        }
1407    }
1408
1409    #[instrument(level = "debug")]
1410    fn apply_storage_collection_metadata_update(
1411        &mut self,
1412        storage_collection_metadata: mz_catalog::durable::StorageCollectionMetadata,
1413        diff: StateDiff,
1414        _retractions: &mut InProgressRetractions,
1415    ) {
1416        apply_inverted_lookup(
1417            &mut Arc::make_mut(&mut self.storage_metadata).collection_metadata,
1418            &storage_collection_metadata.id,
1419            storage_collection_metadata.shard,
1420            diff,
1421        );
1422    }
1423
1424    #[instrument(level = "debug")]
1425    fn apply_unfinalized_shard_update(
1426        &mut self,
1427        unfinalized_shard: mz_catalog::durable::UnfinalizedShard,
1428        diff: StateDiff,
1429        _retractions: &mut InProgressRetractions,
1430    ) {
1431        match diff {
1432            StateDiff::Addition => {
1433                let newly_inserted = Arc::make_mut(&mut self.storage_metadata)
1434                    .unfinalized_shards
1435                    .insert(unfinalized_shard.shard);
1436                assert!(
1437                    newly_inserted,
1438                    "values must be explicitly retracted before inserting a new value: {unfinalized_shard:?}",
1439                );
1440            }
1441            StateDiff::Retraction => {
1442                let removed = Arc::make_mut(&mut self.storage_metadata)
1443                    .unfinalized_shards
1444                    .remove(&unfinalized_shard.shard);
1445                assert!(
1446                    removed,
1447                    "retraction does not match existing value: {unfinalized_shard:?}"
1448                );
1449            }
1450        }
1451    }
1452
1453    /// Generate a list of `BuiltinTableUpdate`s that correspond to a single update made to the
1454    /// durable catalog.
1455    #[instrument(level = "debug")]
1456    pub(crate) fn generate_builtin_table_update(
1457        &self,
1458        kind: StateUpdateKind,
1459        diff: StateDiff,
1460    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
1461        let diff = diff.into();
1462        match kind {
1463            // mz_roles and mz_role_parameters are MaterializedViews backed by
1464            // mz_internal.mz_catalog_raw, so role rows do not produce builtin
1465            // table updates here.
1466            StateUpdateKind::Role(_) => Vec::new(),
1467            StateUpdateKind::RoleAuth(role_auth) => {
1468                vec![self.pack_role_auth_update(role_auth.role_id, diff)]
1469            }
1470            // mz_default_privileges and mz_system_privileges are MaterializedViews
1471            // backed by mz_internal.mz_catalog_raw, so privilege rows do not
1472            // produce builtin table updates here.
1473            StateUpdateKind::DefaultPrivilege(_) => Vec::new(),
1474            StateUpdateKind::SystemPrivilege(_) => Vec::new(),
1475            StateUpdateKind::SystemConfiguration(_) => Vec::new(),
1476            // mz_internal.mz_{cluster,replica}_system_parameters are
1477            // MaterializedViews backed by mz_internal.mz_catalog_raw, so the
1478            // durable scoped-configuration rows do not produce builtin table
1479            // updates here. (The in-memory working copy used for resolution is
1480            // maintained separately in `apply_*_system_configuration_update`.)
1481            StateUpdateKind::ClusterSystemConfiguration(_) => Vec::new(),
1482            StateUpdateKind::ReplicaSystemConfiguration(_) => Vec::new(),
1483            // mz_clusters and mz_cluster_schedules are MaterializedViews backed
1484            // by mz_internal.mz_catalog_raw, so cluster rows do not produce
1485            // builtin table updates here.
1486            StateUpdateKind::Cluster(_) => Vec::new(),
1487            StateUpdateKind::IntrospectionSourceIndex(introspection_source_index) => {
1488                self.pack_item_update(introspection_source_index.item_id, diff)
1489            }
1490            // mz_cluster_replicas is a MaterializedView backed by
1491            // mz_internal.mz_catalog_raw.
1492            StateUpdateKind::ClusterReplica(_) => Vec::new(),
1493            StateUpdateKind::ClusterReplicaSize(size) => {
1494                Self::pack_replica_size_update(&size.name, &size.allocation, diff)
1495            }
1496            StateUpdateKind::SystemObjectMapping(system_object_mapping) => {
1497                // Runtime-alterable system objects have real entries in the
1498                // items collection and so get handled through the normal
1499                // `StateUpdateKind::Item`.`
1500                if !system_object_mapping.unique_identifier.runtime_alterable() {
1501                    self.pack_item_update(system_object_mapping.unique_identifier.catalog_id, diff)
1502                } else {
1503                    vec![]
1504                }
1505            }
1506            StateUpdateKind::Item(item) => self.pack_item_update(item.id, diff),
1507            StateUpdateKind::Comment(_) => Vec::new(),
1508            StateUpdateKind::SourceReferences(_) => Vec::new(),
1509            // mz_audit_events is a MaterializedView backed by
1510            // mz_internal.mz_catalog_raw, so audit log rows do not produce
1511            // builtin table updates here.
1512            StateUpdateKind::AuditLog(_) => Vec::new(),
1513            StateUpdateKind::Database(_)
1514            | StateUpdateKind::Schema(_)
1515            | StateUpdateKind::NetworkPolicy(_)
1516            | StateUpdateKind::StorageCollectionMetadata(_)
1517            | StateUpdateKind::UnfinalizedShard(_) => Vec::new(),
1518        }
1519    }
1520
1521    fn get_entry_mut(&mut self, id: &CatalogItemId) -> &mut CatalogEntry {
1522        self.entry_by_id
1523            .get_mut(id)
1524            .unwrap_or_else(|| panic!("catalog out of sync, missing id {id}"))
1525    }
1526
1527    /// Set the optimized plan for the item identified by `id`.
1528    ///
1529    /// # Panics
1530    /// If the item is not an `Index`, `MaterializedView`, or `MetricSink`.
1531    pub(super) fn set_optimized_plan(
1532        &mut self,
1533        id: GlobalId,
1534        plan: DataflowDescription<mz_expr::OptimizedMirRelationExpr>,
1535    ) {
1536        let item_id = self.entry_by_global_id[&id];
1537        let entry = self.get_entry_mut(&item_id);
1538        match entry.item_mut() {
1539            CatalogItem::Index(idx) => idx.optimized_plan = Some(Arc::new(plan)),
1540            CatalogItem::MaterializedView(mv) => mv.optimized_plan = Some(Arc::new(plan)),
1541            CatalogItem::MetricSink(ms) => ms.optimized_plan = Some(Arc::new(plan)),
1542            other => panic!("set_optimized_plan called on {} ({:?})", id, other.typ()),
1543        }
1544    }
1545
1546    /// Set the physical plan for the item identified by `id`.
1547    ///
1548    /// # Panics
1549    /// If the item is not an `Index`, `MaterializedView`, or `MetricSink`.
1550    pub(super) fn set_physical_plan(
1551        &mut self,
1552        id: GlobalId,
1553        plan: mz_compute_types::dataflows::LirDataflowDescription,
1554    ) {
1555        let item_id = self.entry_by_global_id[&id];
1556        let entry = self.get_entry_mut(&item_id);
1557        match entry.item_mut() {
1558            CatalogItem::Index(idx) => idx.physical_plan = Some(Arc::new(plan)),
1559            CatalogItem::MaterializedView(mv) => mv.physical_plan = Some(Arc::new(plan)),
1560            CatalogItem::MetricSink(ms) => ms.physical_plan = Some(Arc::new(plan)),
1561            other => panic!("set_physical_plan called on {} ({:?})", id, other.typ()),
1562        }
1563    }
1564
1565    /// Set the `DataflowMetainfo` for the item identified by `id`.
1566    ///
1567    /// # Panics
1568    /// If the item is not an `Index`, `MaterializedView`, or `MetricSink`.
1569    pub(super) fn set_dataflow_metainfo(
1570        &mut self,
1571        id: GlobalId,
1572        metainfo: DataflowMetainfo<Arc<OptimizerNotice>>,
1573    ) {
1574        // Add entries to the `notices_by_dep_id` lookup map.
1575        for notice in metainfo.optimizer_notices.iter() {
1576            for dep_id in notice.dependencies.iter() {
1577                self.notices_by_dep_id
1578                    .entry(*dep_id)
1579                    .or_default()
1580                    .push(Arc::clone(notice));
1581            }
1582            if let Some(item_id) = notice.item_id {
1583                soft_assert_eq_or_log!(
1584                    item_id,
1585                    id,
1586                    "notice.item_id should match the id for whom we are saving the notice"
1587                );
1588            }
1589        }
1590        // Set the metainfo on the catalog object.
1591        let item_id = self.entry_by_global_id[&id];
1592        let entry = self.get_entry_mut(&item_id);
1593        match entry.item_mut() {
1594            CatalogItem::Index(idx) => idx.dataflow_metainfo = Some(metainfo),
1595            CatalogItem::MaterializedView(mv) => mv.dataflow_metainfo = Some(metainfo),
1596            CatalogItem::MetricSink(ms) => ms.dataflow_metainfo = Some(metainfo),
1597            other => panic!("set_dataflow_metainfo called on {} ({:?})", id, other.typ()),
1598        }
1599    }
1600
1601    /// Clean up optimizer notices for the given dropped catalog entries.
1602    ///
1603    /// This extracts notices directly from the owned entries (which
1604    /// have already been removed from the catalog maps), cleans up
1605    /// the `notices_by_dep_id` reverse index, and removes notices
1606    /// from other (still-live) catalog objects that depended on the
1607    /// dropped items.
1608    ///
1609    /// Returns the set of all dropped notices for builtin table
1610    /// retraction.
1611    #[mz_ore::instrument(level = "trace")]
1612    pub(super) fn drop_optimizer_notices(
1613        &mut self,
1614        dropped_entries: Vec<CatalogEntry>,
1615    ) -> BTreeSet<Arc<OptimizerNotice>> {
1616        let mut dropped_notices = BTreeSet::new();
1617        let mut drop_ids = BTreeSet::new();
1618
1619        // Extract notices directly from the owned dropped entries.
1620        for mut entry in dropped_entries {
1621            drop_ids.extend(entry.global_ids());
1622            if let Some(metainfo) = entry.item_mut().dataflow_metainfo_mut() {
1623                soft_assert_or_log!(
1624                    metainfo.optimizer_notices.iter().all_unique(),
1625                    "should have been pushed there by \
1626                     `push_optimizer_notice_dedup`"
1627                );
1628                for n in metainfo.optimizer_notices.drain(..) {
1629                    // Clean up notices_by_dep_id for this notice's
1630                    // dependencies.
1631                    for dep_id in n.dependencies.iter() {
1632                        if let Some(notices) = self.notices_by_dep_id.get_mut(dep_id) {
1633                            notices.retain(|x| &n != x);
1634                            if notices.is_empty() {
1635                                self.notices_by_dep_id.remove(dep_id);
1636                            }
1637                        }
1638                    }
1639                    dropped_notices.insert(n);
1640                }
1641            }
1642        }
1643
1644        // Remove notices_by_dep_id entries keyed by the dropped IDs.
1645        // These are notices on OTHER items that depend on a dropped
1646        // item. We need to remove them from those items' metainfo.
1647        for id in &drop_ids {
1648            if let Some(notices) = self.notices_by_dep_id.remove(id) {
1649                for n in notices.into_iter() {
1650                    // Remove the notice from the catalog object it
1651                    // lives on (if that object still exists — it
1652                    // may have been dropped too, in which case the
1653                    // notice was already collected above).
1654                    if let Some(item_id) = n.item_id.as_ref() {
1655                        if let Some(entry) = self.try_get_entry_by_global_id(item_id) {
1656                            let catalog_item_id = entry.id();
1657                            let entry = self.get_entry_mut(&catalog_item_id);
1658                            if let Some(m) = entry.item_mut().dataflow_metainfo_mut() {
1659                                m.optimizer_notices.retain(|x| &n != x);
1660                            }
1661                        }
1662                    }
1663                    dropped_notices.insert(n);
1664                }
1665            }
1666        }
1667
1668        // Clean up notices_by_dep_id entries for dependency IDs
1669        // that are NOT being dropped but had dropped notices.
1670        let todo_dep_ids: BTreeSet<GlobalId> = dropped_notices
1671            .iter()
1672            .flat_map(|n| n.dependencies.iter())
1673            .filter(|dep_id| !drop_ids.contains(dep_id))
1674            .copied()
1675            .collect();
1676        for id in todo_dep_ids {
1677            if let Some(notices) = self.notices_by_dep_id.get_mut(&id) {
1678                notices.retain(|n| !dropped_notices.contains(n));
1679                if notices.is_empty() {
1680                    self.notices_by_dep_id.remove(&id);
1681                }
1682            }
1683        }
1684
1685        dropped_notices
1686    }
1687
1688    fn get_schema_mut(
1689        &mut self,
1690        database_spec: &ResolvedDatabaseSpecifier,
1691        schema_spec: &SchemaSpecifier,
1692        conn_id: &ConnectionId,
1693    ) -> &mut Schema {
1694        // Keep in sync with `get_schemas`
1695        match (database_spec, schema_spec) {
1696            (ResolvedDatabaseSpecifier::Ambient, SchemaSpecifier::Temporary) => self
1697                .temporary_namespaces
1698                .schema_mut(conn_id)
1699                .expect("catalog out of sync"),
1700            (ResolvedDatabaseSpecifier::Ambient, SchemaSpecifier::Id(id)) => self
1701                .ambient_schemas_by_id
1702                .get_mut(id)
1703                .expect("catalog out of sync"),
1704            (ResolvedDatabaseSpecifier::Id(database_id), SchemaSpecifier::Id(schema_id)) => self
1705                .database_by_id
1706                .get_mut(database_id)
1707                .expect("catalog out of sync")
1708                .schemas_by_id
1709                .get_mut(schema_id)
1710                .expect("catalog out of sync"),
1711            (ResolvedDatabaseSpecifier::Id(_), SchemaSpecifier::Temporary) => {
1712                unreachable!("temporary schemas are in the ambient database")
1713            }
1714        }
1715    }
1716
1717    /// Install builtin views to the catalog. This is its own function so that views can be
1718    /// optimized in parallel.
1719    ///
1720    /// The implementation is similar to `apply_updates_for_bootstrap` and determines dependency
1721    /// problems by sniffing out specific errors and then retrying once those dependencies are
1722    /// complete. This doesn't work for everything (casts, function implementations) so we also need
1723    /// to have a bucket for everything at the end. Additionally, because this executes in parellel,
1724    /// we must maintain a completed set otherwise races could result in orphaned views languishing
1725    /// in awaiting with nothing retriggering the attempt.
1726    #[instrument(name = "catalog::parse_views")]
1727    async fn parse_builtin_views(
1728        state: &mut CatalogState,
1729        builtin_views: Vec<(&'static BuiltinView, CatalogItemId, GlobalId)>,
1730        retractions: &mut InProgressRetractions,
1731        local_expression_cache: &mut LocalExpressionCache,
1732    ) -> Vec<BuiltinTableUpdate<&'static BuiltinTable>> {
1733        let mut builtin_table_updates = Vec::with_capacity(builtin_views.len());
1734        let (updates, additions): (Vec<_>, Vec<_>) =
1735            builtin_views
1736                .into_iter()
1737                .partition_map(|(view, item_id, gid)| {
1738                    match retractions.system_object_mappings.remove(&item_id) {
1739                        Some(entry) => Either::Left(entry),
1740                        None => Either::Right((view, item_id, gid)),
1741                    }
1742                });
1743
1744        for entry in updates {
1745            // This implies that we updated the fingerprint for some builtin view. The retraction
1746            // was parsed, planned, and optimized using the compiled in definition, not the
1747            // definition from a previous version. So we can just stick the old entry back into the
1748            // catalog.
1749            let item_id = entry.id();
1750            state.insert_entry(entry);
1751            builtin_table_updates.extend(state.pack_item_update(item_id, Diff::ONE));
1752        }
1753
1754        let mut handles = Vec::new();
1755        let mut awaiting_id_dependencies: BTreeMap<CatalogItemId, Vec<CatalogItemId>> =
1756            BTreeMap::new();
1757        let mut awaiting_name_dependencies: BTreeMap<String, Vec<CatalogItemId>> = BTreeMap::new();
1758        // Some errors are due to the implementation of casts or SQL functions that depend on some
1759        // view. Instead of figuring out the exact view dependency, delay these until the end.
1760        let mut awaiting_all = Vec::new();
1761        // Completed views, needed to avoid race conditions.
1762        let mut completed_ids: BTreeSet<CatalogItemId> = BTreeSet::new();
1763        let mut completed_names: BTreeSet<String> = BTreeSet::new();
1764
1765        // Avoid some reference lifetime issues by not passing `builtin` into the spawned task.
1766        let mut views: BTreeMap<CatalogItemId, (&BuiltinView, GlobalId)> = additions
1767            .into_iter()
1768            .map(|(view, item_id, gid)| (item_id, (view, gid)))
1769            .collect();
1770        let item_ids: Vec<_> = views.keys().copied().collect();
1771
1772        let mut ready: VecDeque<CatalogItemId> = views.keys().cloned().collect();
1773        while !handles.is_empty() || !ready.is_empty() || !awaiting_all.is_empty() {
1774            if handles.is_empty() && ready.is_empty() {
1775                // Enqueue the views that were waiting for all the others.
1776                ready.extend(awaiting_all.drain(..));
1777            }
1778
1779            // Spawn tasks for all ready views.
1780            if !ready.is_empty() {
1781                let spawn_state = Arc::new(state.clone());
1782                while let Some(id) = ready.pop_front() {
1783                    let (view, global_id) = views.get(&id).expect("must exist");
1784                    let global_id = *global_id;
1785                    let create_sql = view.create_sql();
1786                    // Views can't be versioned.
1787                    let versions = BTreeMap::new();
1788
1789                    let span = info_span!(parent: None, "parse builtin view", name = view.name);
1790                    OpenTelemetryContext::obtain().attach_as_parent_to(&span);
1791                    let task_state = Arc::clone(&spawn_state);
1792                    let cached_expr = local_expression_cache.remove_cached_expression(&global_id);
1793                    let handle = mz_ore::task::spawn_blocking(
1794                        || "parse view",
1795                        move || {
1796                            span.in_scope(|| {
1797                                let res = task_state.parse_item_inner(
1798                                    global_id,
1799                                    &create_sql,
1800                                    &versions,
1801                                    None,
1802                                    false,
1803                                    None,
1804                                    cached_expr,
1805                                    None,
1806                                );
1807                                (id, global_id, res)
1808                            })
1809                        },
1810                    );
1811                    handles.push(handle);
1812                }
1813            }
1814
1815            // Wait for a view to be ready.
1816            let (selected, _idx, remaining) = future::select_all(handles).await;
1817            handles = remaining;
1818            let (id, global_id, res) = selected;
1819            let mut insert_cached_expr = |cached_expr| {
1820                if let Some(cached_expr) = cached_expr {
1821                    local_expression_cache.insert_cached_expression(global_id, cached_expr);
1822                }
1823            };
1824            match res {
1825                Ok((item, uncached_expr)) => {
1826                    if let Some((uncached_expr, optimizer_features)) = uncached_expr {
1827                        local_expression_cache.insert_uncached_expression(
1828                            global_id,
1829                            uncached_expr,
1830                            optimizer_features,
1831                            RelationVersion::root(),
1832                        );
1833                    }
1834                    // Add item to catalog.
1835                    let (view, _gid) = views.remove(&id).expect("must exist");
1836                    let schema_id = state
1837                        .ambient_schemas_by_name
1838                        .get(view.schema)
1839                        .unwrap_or_else(|| panic!("unknown ambient schema: {}", view.schema));
1840                    let qname = QualifiedItemName {
1841                        qualifiers: ItemQualifiers {
1842                            database_spec: ResolvedDatabaseSpecifier::Ambient,
1843                            schema_spec: SchemaSpecifier::Id(*schema_id),
1844                        },
1845                        item: view.name.into(),
1846                    };
1847                    let mut acl_items = vec![rbac::owner_privilege(
1848                        mz_sql::catalog::ObjectType::View,
1849                        MZ_SYSTEM_ROLE_ID,
1850                    )];
1851                    acl_items.extend_from_slice(&view.access);
1852
1853                    state.insert_item(
1854                        id,
1855                        view.oid,
1856                        qname,
1857                        item,
1858                        MZ_SYSTEM_ROLE_ID,
1859                        PrivilegeMap::from_mz_acl_items(acl_items),
1860                    );
1861
1862                    // Enqueue any items waiting on this dependency.
1863                    let mut resolved_dependent_items = Vec::new();
1864                    if let Some(dependent_items) = awaiting_id_dependencies.remove(&id) {
1865                        resolved_dependent_items.extend(dependent_items);
1866                    }
1867                    let entry = state.get_entry(&id);
1868                    let full_name = state.resolve_full_name(entry.name(), None).to_string();
1869                    if let Some(dependent_items) = awaiting_name_dependencies.remove(&full_name) {
1870                        resolved_dependent_items.extend(dependent_items);
1871                    }
1872                    ready.extend(resolved_dependent_items);
1873
1874                    completed_ids.insert(id);
1875                    completed_names.insert(full_name);
1876                }
1877                // If we were missing a dependency, wait for it to be added.
1878                Err((
1879                    AdapterError::PlanError(plan::PlanError::InvalidId(missing_dep)),
1880                    cached_expr,
1881                )) => {
1882                    insert_cached_expr(cached_expr);
1883                    if completed_ids.contains(&missing_dep) {
1884                        ready.push_back(id);
1885                    } else {
1886                        awaiting_id_dependencies
1887                            .entry(missing_dep)
1888                            .or_default()
1889                            .push(id);
1890                    }
1891                }
1892                // If we were missing a dependency, wait for it to be added.
1893                Err((
1894                    AdapterError::PlanError(plan::PlanError::Catalog(
1895                        SqlCatalogError::UnknownItem(missing_dep),
1896                    )),
1897                    cached_expr,
1898                )) => {
1899                    insert_cached_expr(cached_expr);
1900                    match CatalogItemId::from_str(&missing_dep) {
1901                        Ok(missing_dep) => {
1902                            if completed_ids.contains(&missing_dep) {
1903                                ready.push_back(id);
1904                            } else {
1905                                awaiting_id_dependencies
1906                                    .entry(missing_dep)
1907                                    .or_default()
1908                                    .push(id);
1909                            }
1910                        }
1911                        Err(_) => {
1912                            if completed_names.contains(&missing_dep) {
1913                                ready.push_back(id);
1914                            } else {
1915                                awaiting_name_dependencies
1916                                    .entry(missing_dep)
1917                                    .or_default()
1918                                    .push(id);
1919                            }
1920                        }
1921                    }
1922                }
1923                Err((
1924                    AdapterError::PlanError(plan::PlanError::InvalidCast { .. }),
1925                    cached_expr,
1926                )) => {
1927                    insert_cached_expr(cached_expr);
1928                    awaiting_all.push(id);
1929                }
1930                Err((e, _)) => {
1931                    let (bad_view, _gid) = views.get(&id).expect("must exist");
1932                    panic!(
1933                        "internal error: failed to load bootstrap view:\n\
1934                            {name}\n\
1935                            error:\n\
1936                            {e:?}\n\n\
1937                            Make sure that the schema name is specified in the builtin view's create sql statement.
1938                            ",
1939                        name = bad_view.name,
1940                    )
1941                }
1942            }
1943        }
1944
1945        assert!(awaiting_id_dependencies.is_empty());
1946        assert!(
1947            awaiting_name_dependencies.is_empty(),
1948            "awaiting_name_dependencies: {awaiting_name_dependencies:?}"
1949        );
1950        assert!(awaiting_all.is_empty());
1951        assert!(views.is_empty());
1952
1953        // Generate a builtin table update for all the new views.
1954        builtin_table_updates.extend(
1955            item_ids
1956                .into_iter()
1957                .flat_map(|id| state.pack_item_update(id, Diff::ONE)),
1958        );
1959
1960        builtin_table_updates
1961    }
1962
1963    /// Associates a name, `CatalogItemId`, and entry.
1964    fn insert_entry(&mut self, entry: CatalogEntry) {
1965        if !entry.id.is_system() {
1966            if let Some(cluster_id) = entry.item.cluster_id() {
1967                self.clusters_by_id
1968                    .get_mut(&cluster_id)
1969                    .expect("catalog out of sync")
1970                    .bound_objects
1971                    .insert(entry.id);
1972            };
1973        }
1974
1975        for u in entry.references().items() {
1976            match self.entry_by_id.get_mut(u) {
1977                Some(metadata) => metadata.referenced_by.push(entry.id()),
1978                None => panic!(
1979                    "Catalog: missing dependent catalog item {} while installing {}",
1980                    u,
1981                    self.resolve_full_name(entry.name(), entry.conn_id())
1982                ),
1983            }
1984        }
1985        for u in entry.uses() {
1986            // Ignore self for self-referential tasks (e.g. Continual Tasks), if
1987            // present.
1988            if u == entry.id() {
1989                continue;
1990            }
1991            match self.entry_by_id.get_mut(&u) {
1992                Some(metadata) => metadata.used_by.push(entry.id()),
1993                None => panic!(
1994                    "Catalog: missing dependent catalog item {} while installing {}",
1995                    u,
1996                    self.resolve_full_name(entry.name(), entry.conn_id())
1997                ),
1998            }
1999        }
2000        for gid in entry.item.global_ids() {
2001            self.entry_by_global_id.insert(gid, entry.id());
2002        }
2003        let conn_id = entry.item().conn_id().unwrap_or(&SYSTEM_CONN_ID);
2004        // Lazily create the temporary schema if this is a temporary item and the schema
2005        // doesn't exist yet.
2006        if entry.name().qualifiers.schema_spec == SchemaSpecifier::Temporary {
2007            self.temporary_namespaces
2008                .ensure_schema(conn_id, entry.owner_id);
2009        }
2010        let schema = self.get_schema_mut(
2011            &entry.name().qualifiers.database_spec,
2012            &entry.name().qualifiers.schema_spec,
2013            conn_id,
2014        );
2015
2016        let prev_id = match entry.item() {
2017            CatalogItem::Func(_) => schema
2018                .functions
2019                .insert(entry.name().item.clone(), entry.id()),
2020            CatalogItem::Type(_) => schema.types.insert(entry.name().item.clone(), entry.id()),
2021            _ => schema.items.insert(entry.name().item.clone(), entry.id()),
2022        };
2023
2024        assert!(
2025            prev_id.is_none(),
2026            "builtin name collision on {:?}",
2027            entry.name().item.clone()
2028        );
2029
2030        self.entry_by_id.insert(entry.id(), entry.clone());
2031    }
2032
2033    /// Associates a name, [`CatalogItemId`], and entry.
2034    fn insert_item(
2035        &mut self,
2036        id: CatalogItemId,
2037        oid: u32,
2038        name: QualifiedItemName,
2039        item: CatalogItem,
2040        owner_id: RoleId,
2041        privileges: PrivilegeMap,
2042    ) {
2043        let entry = CatalogEntry {
2044            item,
2045            name,
2046            id,
2047            oid,
2048            used_by: Vec::new(),
2049            referenced_by: Vec::new(),
2050            owner_id,
2051            privileges,
2052        };
2053
2054        self.insert_entry(entry);
2055    }
2056
2057    #[mz_ore::instrument(level = "trace")]
2058    fn drop_item(&mut self, id: CatalogItemId) -> CatalogEntry {
2059        let metadata = self.entry_by_id.remove(&id).expect("catalog out of sync");
2060        for u in metadata.references().items() {
2061            if let Some(dep_metadata) = self.entry_by_id.get_mut(u) {
2062                dep_metadata.referenced_by.retain(|u| *u != metadata.id())
2063            }
2064        }
2065        for u in metadata.uses() {
2066            if let Some(dep_metadata) = self.entry_by_id.get_mut(&u) {
2067                dep_metadata.used_by.retain(|u| *u != metadata.id())
2068            }
2069        }
2070        for gid in metadata.global_ids() {
2071            self.entry_by_global_id.remove(&gid);
2072        }
2073
2074        let conn_id = metadata.item().conn_id().unwrap_or(&SYSTEM_CONN_ID);
2075        let schema = self.get_schema_mut(
2076            &metadata.name().qualifiers.database_spec,
2077            &metadata.name().qualifiers.schema_spec,
2078            conn_id,
2079        );
2080        if metadata.item_type() == CatalogItemType::Type {
2081            schema
2082                .types
2083                .remove(&metadata.name().item)
2084                .expect("catalog out of sync");
2085        } else {
2086            // Functions would need special handling, but we don't yet support
2087            // dropping functions.
2088            assert_ne!(metadata.item_type(), CatalogItemType::Func);
2089
2090            schema
2091                .items
2092                .remove(&metadata.name().item)
2093                .expect("catalog out of sync");
2094        };
2095
2096        if !id.is_system() {
2097            if let Some(cluster_id) = metadata.item().cluster_id() {
2098                assert!(
2099                    self.clusters_by_id
2100                        .get_mut(&cluster_id)
2101                        .expect("catalog out of sync")
2102                        .bound_objects
2103                        .remove(&id),
2104                    "catalog out of sync"
2105                );
2106            }
2107        }
2108
2109        metadata
2110    }
2111
2112    fn insert_introspection_source_index(
2113        &mut self,
2114        cluster_id: ClusterId,
2115        log: &'static BuiltinLog,
2116        item_id: CatalogItemId,
2117        global_id: GlobalId,
2118        oid: u32,
2119    ) {
2120        let (index_name, index) =
2121            self.create_introspection_source_index(cluster_id, log, global_id);
2122        self.insert_item(
2123            item_id,
2124            oid,
2125            index_name,
2126            index,
2127            MZ_SYSTEM_ROLE_ID,
2128            PrivilegeMap::default(),
2129        );
2130    }
2131
2132    fn create_introspection_source_index(
2133        &self,
2134        cluster_id: ClusterId,
2135        log: &'static BuiltinLog,
2136        global_id: GlobalId,
2137    ) -> (QualifiedItemName, CatalogItem) {
2138        let source_name = FullItemName {
2139            database: RawDatabaseSpecifier::Ambient,
2140            schema: log.schema.into(),
2141            item: log.name.into(),
2142        };
2143        let index_name = format!("{}_{}_primary_idx", log.name, cluster_id);
2144        let mut index_name = QualifiedItemName {
2145            qualifiers: ItemQualifiers {
2146                database_spec: ResolvedDatabaseSpecifier::Ambient,
2147                schema_spec: SchemaSpecifier::Id(self.get_mz_introspection_schema_id()),
2148            },
2149            item: index_name.clone(),
2150        };
2151        index_name = self.find_available_name(index_name, &SYSTEM_CONN_ID);
2152        let index_item_name = index_name.item.clone();
2153        let (log_item_id, log_global_id) = self.resolve_builtin_log(log);
2154        let index = CatalogItem::Index(Index {
2155            global_id,
2156            on: log_global_id,
2157            keys: log
2158                .variant
2159                .index_by()
2160                .into_iter()
2161                .map(MirScalarExpr::column)
2162                .collect(),
2163            create_sql: index_sql(
2164                index_item_name,
2165                cluster_id,
2166                source_name,
2167                &log.variant.desc(),
2168                &log.variant.index_by(),
2169            ),
2170            conn_id: None,
2171            resolved_ids: [(log_item_id, log_global_id)].into_iter().collect(),
2172            cluster_id,
2173            is_retained_metrics_object: false,
2174            custom_logical_compaction_window: None,
2175            optimized_plan: None,
2176            physical_plan: None,
2177            dataflow_metainfo: None,
2178        });
2179        (index_name, index)
2180    }
2181
2182    /// Insert system configuration `name` with `value`.
2183    ///
2184    /// Return a `bool` value indicating whether the configuration was modified
2185    /// by the call.
2186    fn insert_system_configuration(&mut self, name: &str, value: VarInput) -> Result<bool, Error> {
2187        Ok(Arc::make_mut(&mut self.system_configuration).set(name, value)?)
2188    }
2189
2190    /// Reset system configuration `name`.
2191    ///
2192    /// Return a `bool` value indicating whether the configuration was modified
2193    /// by the call.
2194    fn remove_system_configuration(&mut self, name: &str) -> Result<bool, Error> {
2195        Ok(Arc::make_mut(&mut self.system_configuration).reset(name)?)
2196    }
2197}
2198
2199/// Sort [`StateUpdate`]s in dependency order.
2200///
2201/// # Panics
2202///
2203/// This function assumes that all provided `updates` have the same timestamp and will panic
2204/// otherwise. It also requires that the provided `updates` are consolidated, i.e. all contained
2205/// `StateUpdateKinds` are unique.
2206fn sort_updates(updates: Vec<StateUpdate>) -> Vec<StateUpdate> {
2207    fn push_update<T>(
2208        update: T,
2209        diff: StateDiff,
2210        retractions: &mut Vec<T>,
2211        additions: &mut Vec<T>,
2212    ) {
2213        match diff {
2214            StateDiff::Retraction => retractions.push(update),
2215            StateDiff::Addition => additions.push(update),
2216        }
2217    }
2218
2219    soft_assert_no_log!(
2220        updates.iter().map(|update| update.ts).all_equal(),
2221        "all timestamps should be equal: {updates:?}"
2222    );
2223    soft_assert_no_log!(
2224        {
2225            let mut dedup = BTreeSet::new();
2226            updates.iter().all(|update| dedup.insert(&update.kind))
2227        },
2228        "updates should be consolidated: {updates:?}"
2229    );
2230
2231    // Partition updates by type so that we can weave different update types into the right spots.
2232    let mut pre_cluster_retractions = Vec::new();
2233    let mut pre_cluster_additions = Vec::new();
2234    let mut cluster_retractions = Vec::new();
2235    let mut cluster_additions = Vec::new();
2236    let mut builtin_item_updates = Vec::new();
2237    let mut item_retractions = Vec::new();
2238    let mut item_additions = Vec::new();
2239    let mut post_item_retractions = Vec::new();
2240    let mut post_item_additions = Vec::new();
2241    for update in updates {
2242        let diff = update.diff.clone();
2243        match update.kind {
2244            StateUpdateKind::Role(_)
2245            | StateUpdateKind::RoleAuth(_)
2246            | StateUpdateKind::Database(_)
2247            | StateUpdateKind::Schema(_)
2248            | StateUpdateKind::DefaultPrivilege(_)
2249            | StateUpdateKind::SystemPrivilege(_)
2250            | StateUpdateKind::SystemConfiguration(_)
2251            | StateUpdateKind::NetworkPolicy(_)
2252            | StateUpdateKind::ClusterReplicaSize(_) => push_update(
2253                update,
2254                diff,
2255                &mut pre_cluster_retractions,
2256                &mut pre_cluster_additions,
2257            ),
2258            StateUpdateKind::Cluster(_)
2259            | StateUpdateKind::ClusterSystemConfiguration(_)
2260            | StateUpdateKind::IntrospectionSourceIndex(_)
2261            | StateUpdateKind::ClusterReplica(_)
2262            | StateUpdateKind::ReplicaSystemConfiguration(_) => push_update(
2263                update,
2264                diff,
2265                &mut cluster_retractions,
2266                &mut cluster_additions,
2267            ),
2268            StateUpdateKind::SystemObjectMapping(system_object_mapping) => {
2269                builtin_item_updates.push((system_object_mapping, update.ts, update.diff))
2270            }
2271            StateUpdateKind::Item(item) => push_update(
2272                (item, update.ts, update.diff),
2273                diff,
2274                &mut item_retractions,
2275                &mut item_additions,
2276            ),
2277            StateUpdateKind::Comment(_)
2278            | StateUpdateKind::SourceReferences(_)
2279            | StateUpdateKind::AuditLog(_)
2280            | StateUpdateKind::StorageCollectionMetadata(_)
2281            | StateUpdateKind::UnfinalizedShard(_) => push_update(
2282                update,
2283                diff,
2284                &mut post_item_retractions,
2285                &mut post_item_additions,
2286            ),
2287        }
2288    }
2289
2290    // Sort builtin item updates by dependency. The builtin definition order must be a dependency
2291    // order, so we can use that to avoid a more expensive topo sorting.
2292    let builtin_item_updates = builtin_item_updates
2293        .into_iter()
2294        .map(|(system_object_mapping, ts, diff)| {
2295            let idx = BUILTIN_LOOKUP
2296                .get(&system_object_mapping.description)
2297                .expect("missing builtin")
2298                .0;
2299            (idx, system_object_mapping, ts, diff)
2300        })
2301        .sorted_by_key(|(idx, _, _, _)| *idx)
2302        .map(|(_, system_object_mapping, ts, diff)| (system_object_mapping, ts, diff));
2303
2304    // Partition builtins based on whether or not they should be applied before or after clusters.
2305    // Clusters depend on sources (introspection logs), so sources need to be applied first.
2306    // Everything else can be applied after clusters.
2307    let mut builtin_source_retractions = Vec::new();
2308    let mut builtin_source_additions = Vec::new();
2309    let mut other_builtin_retractions = Vec::new();
2310    let mut other_builtin_additions = Vec::new();
2311    for (builtin_item_update, ts, diff) in builtin_item_updates {
2312        let object_type = builtin_item_update.description.object_type;
2313        let update = StateUpdate {
2314            kind: StateUpdateKind::SystemObjectMapping(builtin_item_update),
2315            ts,
2316            diff,
2317        };
2318        if object_type == CatalogItemType::Source {
2319            push_update(
2320                update,
2321                diff,
2322                &mut builtin_source_retractions,
2323                &mut builtin_source_additions,
2324            );
2325        } else {
2326            push_update(
2327                update,
2328                diff,
2329                &mut other_builtin_retractions,
2330                &mut other_builtin_additions,
2331            );
2332        }
2333    }
2334
2335    /// Sort items by their dependencies using topological sort.
2336    ///
2337    /// # Panics
2338    ///
2339    /// This function requires that all provided items have unique item IDs.
2340    fn sort_items_topological(items: &mut Vec<(mz_catalog::durable::Item, Timestamp, StateDiff)>) {
2341        tracing::debug!(?items, "sorting items by dependencies");
2342
2343        let key_fn = |item: &(mz_catalog::durable::Item, _, _)| item.0.id;
2344        let dependencies_fn = |item: &(mz_catalog::durable::Item, _, _)| {
2345            let statement = mz_sql::parse::parse(&item.0.create_sql)
2346                .expect("valid create_sql")
2347                .into_element()
2348                .ast;
2349            mz_sql::names::dependencies(&statement).expect("failed to find dependencies of item")
2350        };
2351        sort_topological(items, key_fn, dependencies_fn);
2352    }
2353
2354    /// Sort item updates by dependency.
2355    ///
2356    /// First we group items into groups that are totally ordered by dependency. For example, when
2357    /// sorting all items by dependency we know that all tables can come after all sources, because
2358    /// a source can never depend on a table. Second, we sort the items in each group in
2359    /// topological order, or by ID, depending on the type.
2360    ///
2361    /// It used to be the case that the ID order of ALL items matched the dependency order. However,
2362    /// certain migrations shuffled item IDs around s.t. this was no longer true. A much better
2363    /// approach would be to investigate each item, discover their exact dependencies, and then
2364    /// perform a topological sort. This is non-trivial because we only have the CREATE SQL of each
2365    /// item here. Within the SQL the dependent items are sometimes referred to by ID and sometimes
2366    /// referred to by name.
2367    ///
2368    /// The logic of this function should match [`sort_temp_item_updates`].
2369    fn sort_item_updates(
2370        item_updates: Vec<(mz_catalog::durable::Item, Timestamp, StateDiff)>,
2371    ) -> VecDeque<(mz_catalog::durable::Item, Timestamp, StateDiff)> {
2372        // Partition items into groups s.t. each item in one group has a predefined order with all
2373        // items in other groups. For example, all sinks are ordered greater than all tables.
2374        let mut types = Vec::new();
2375        // N.B. Functions can depend on system tables, but not user tables.
2376        // TODO(udf): This will change when UDFs are supported.
2377        let mut funcs = Vec::new();
2378        let mut secrets = Vec::new();
2379        let mut connections = Vec::new();
2380        let mut sources = Vec::new();
2381        let mut tables = Vec::new();
2382        let mut derived_items = Vec::new();
2383        let mut sinks = Vec::new();
2384        for update in item_updates {
2385            match update.0.item_type() {
2386                CatalogItemType::Type => types.push(update),
2387                CatalogItemType::Func => funcs.push(update),
2388                CatalogItemType::Secret => secrets.push(update),
2389                CatalogItemType::Connection => connections.push(update),
2390                CatalogItemType::Source => sources.push(update),
2391                CatalogItemType::Table => tables.push(update),
2392                CatalogItemType::View
2393                | CatalogItemType::MaterializedView
2394                | CatalogItemType::Index
2395                | CatalogItemType::MetricSink => derived_items.push(update),
2396                CatalogItemType::Sink => sinks.push(update),
2397            }
2398        }
2399
2400        // For some groups, the items in them can depend on each other and can be `ALTER`ed so that
2401        // an item ends up depending on an item with a greater ID. Thus we need to perform
2402        // topological sort for these groups.
2403        sort_items_topological(&mut connections);
2404        sort_items_topological(&mut derived_items);
2405
2406        // Other groups we can simply sort by ID.
2407        for group in [
2408            &mut types,
2409            &mut funcs,
2410            &mut secrets,
2411            &mut sources,
2412            &mut tables,
2413            &mut sinks,
2414        ] {
2415            group.sort_by_key(|(item, _, _)| item.id);
2416        }
2417
2418        iter::empty()
2419            .chain(types)
2420            .chain(funcs)
2421            .chain(secrets)
2422            .chain(connections)
2423            .chain(sources)
2424            .chain(tables)
2425            .chain(derived_items)
2426            .chain(sinks)
2427            .collect()
2428    }
2429
2430    // Temporary and non-temporary items sort together: the type groups order
2431    // cross-type dependencies and the topological sorts order dependencies
2432    // within a group, regardless of which items are temporary.
2433    fn into_state_updates(
2434        item_updates: VecDeque<(mz_catalog::durable::Item, Timestamp, StateDiff)>,
2435    ) -> Vec<StateUpdate> {
2436        item_updates
2437            .into_iter()
2438            .map(|(item, ts, diff)| StateUpdate {
2439                kind: StateUpdateKind::Item(item),
2440                ts,
2441                diff,
2442            })
2443            .collect()
2444    }
2445    let item_retractions = into_state_updates(sort_item_updates(item_retractions));
2446    let item_additions = into_state_updates(sort_item_updates(item_additions));
2447
2448    // Put everything back together.
2449    iter::empty()
2450        // All retractions must be reversed.
2451        .chain(post_item_retractions.into_iter().rev())
2452        .chain(item_retractions.into_iter().rev())
2453        .chain(other_builtin_retractions.into_iter().rev())
2454        .chain(cluster_retractions.into_iter().rev())
2455        .chain(builtin_source_retractions.into_iter().rev())
2456        .chain(pre_cluster_retractions.into_iter().rev())
2457        .chain(pre_cluster_additions)
2458        .chain(builtin_source_additions)
2459        .chain(cluster_additions)
2460        .chain(other_builtin_additions)
2461        .chain(item_additions)
2462        .chain(post_item_additions)
2463        .collect()
2464}
2465
2466/// Groups of updates of certain types are applied in batches to improve
2467/// performance. A constraint is that updates must be applied in order. This
2468/// process is modeled as a state machine that batches then applies groups of
2469/// updates.
2470enum ApplyState {
2471    /// Additions of builtin views.
2472    BuiltinViewAdditions(Vec<(&'static BuiltinView, CatalogItemId, GlobalId)>),
2473    /// Item updates that aren't builtin view additions.
2474    ///
2475    /// This contains all updates whose application requires calling
2476    /// `parse_item` and thus toggling the `enable_for_item_parsing` feature
2477    /// flags.
2478    Items(Vec<StateUpdate>),
2479    /// All other updates.
2480    Updates(Vec<StateUpdate>),
2481}
2482
2483impl ApplyState {
2484    fn new(update: StateUpdate) -> Self {
2485        use StateUpdateKind::*;
2486        match &update.kind {
2487            SystemObjectMapping(som)
2488                if som.description.object_type == CatalogItemType::View
2489                    && update.diff == StateDiff::Addition =>
2490            {
2491                let view_addition = lookup_builtin_view_addition(som.clone());
2492                Self::BuiltinViewAdditions(vec![view_addition])
2493            }
2494
2495            IntrospectionSourceIndex(_) | SystemObjectMapping(_) | Item(_) => {
2496                Self::Items(vec![update])
2497            }
2498
2499            Role(_)
2500            | RoleAuth(_)
2501            | Database(_)
2502            | Schema(_)
2503            | DefaultPrivilege(_)
2504            | SystemPrivilege(_)
2505            | SystemConfiguration(_)
2506            | ClusterSystemConfiguration(_)
2507            | ReplicaSystemConfiguration(_)
2508            | Cluster(_)
2509            | NetworkPolicy(_)
2510            | ClusterReplicaSize(_)
2511            | ClusterReplica(_)
2512            | SourceReferences(_)
2513            | Comment(_)
2514            | AuditLog(_)
2515            | StorageCollectionMetadata(_)
2516            | UnfinalizedShard(_) => Self::Updates(vec![update]),
2517        }
2518    }
2519
2520    /// Apply all updates that have been batched in `self`.
2521    ///
2522    /// We make sure to enable all "enable_for_item_parsing" feature flags when applying item
2523    /// updates during bootstrap. See [`CatalogState::with_enable_for_item_parsing`] for more
2524    /// details.
2525    async fn apply(
2526        self,
2527        state: &mut CatalogState,
2528        retractions: &mut InProgressRetractions,
2529        local_expression_cache: &mut LocalExpressionCache,
2530    ) -> (
2531        Vec<BuiltinTableUpdate<&'static BuiltinTable>>,
2532        Vec<ParsedStateUpdate>,
2533    ) {
2534        match self {
2535            Self::BuiltinViewAdditions(builtin_view_additions) => {
2536                let restore = Arc::clone(&state.system_configuration);
2537                Arc::make_mut(&mut state.system_configuration).enable_for_item_parsing();
2538                let builtin_table_updates = CatalogState::parse_builtin_views(
2539                    state,
2540                    builtin_view_additions,
2541                    retractions,
2542                    local_expression_cache,
2543                )
2544                .await;
2545                state.system_configuration = restore;
2546                (builtin_table_updates, Vec::new())
2547            }
2548            Self::Items(updates) => state.with_enable_for_item_parsing(|state| {
2549                state
2550                    .apply_updates_inner(updates, retractions, local_expression_cache)
2551                    .expect("corrupt catalog")
2552            }),
2553            Self::Updates(updates) => state
2554                .apply_updates_inner(updates, retractions, local_expression_cache)
2555                .expect("corrupt catalog"),
2556        }
2557    }
2558
2559    async fn step(
2560        self,
2561        next: Self,
2562        state: &mut CatalogState,
2563        retractions: &mut InProgressRetractions,
2564        local_expression_cache: &mut LocalExpressionCache,
2565    ) -> (
2566        Self,
2567        (
2568            Vec<BuiltinTableUpdate<&'static BuiltinTable>>,
2569            Vec<ParsedStateUpdate>,
2570        ),
2571    ) {
2572        match (self, next) {
2573            (
2574                Self::BuiltinViewAdditions(mut builtin_view_additions),
2575                Self::BuiltinViewAdditions(next_builtin_view_additions),
2576            ) => {
2577                // Continue batching builtin view additions.
2578                builtin_view_additions.extend(next_builtin_view_additions);
2579                (
2580                    Self::BuiltinViewAdditions(builtin_view_additions),
2581                    (Vec::new(), Vec::new()),
2582                )
2583            }
2584            (Self::Items(mut updates), Self::Items(next_updates)) => {
2585                // Continue batching item updates.
2586                updates.extend(next_updates);
2587                (Self::Items(updates), (Vec::new(), Vec::new()))
2588            }
2589            (Self::Updates(mut updates), Self::Updates(next_updates)) => {
2590                // Continue batching updates.
2591                updates.extend(next_updates);
2592                (Self::Updates(updates), (Vec::new(), Vec::new()))
2593            }
2594            (apply_state, next_apply_state) => {
2595                // Apply the current batch and start batching new apply state.
2596                let updates = apply_state
2597                    .apply(state, retractions, local_expression_cache)
2598                    .await;
2599                (next_apply_state, updates)
2600            }
2601        }
2602    }
2603}
2604
2605/// Trait abstracting over map operations needed by [`apply_inverted_lookup`] and
2606/// [`apply_with_update`]. Both [`BTreeMap`] and [`imbl::OrdMap`] implement this.
2607trait MutableMap<K, V> {
2608    fn insert(&mut self, key: K, value: V) -> Option<V>;
2609    fn remove(&mut self, key: &K) -> Option<V>;
2610}
2611
2612impl<K: Ord, V> MutableMap<K, V> for BTreeMap<K, V> {
2613    fn insert(&mut self, key: K, value: V) -> Option<V> {
2614        BTreeMap::insert(self, key, value)
2615    }
2616    fn remove(&mut self, key: &K) -> Option<V> {
2617        BTreeMap::remove(self, key)
2618    }
2619}
2620
2621impl<K: Ord + Clone, V: Clone> MutableMap<K, V> for imbl::OrdMap<K, V> {
2622    fn insert(&mut self, key: K, value: V) -> Option<V> {
2623        imbl::OrdMap::insert(self, key, value)
2624    }
2625    fn remove(&mut self, key: &K) -> Option<V> {
2626        imbl::OrdMap::remove(self, key)
2627    }
2628}
2629
2630/// Helper method to updated inverted lookup maps. The keys are generally names and the values are
2631/// generally IDs.
2632///
2633/// Importantly, when retracting it's expected that the existing value will match `value` exactly.
2634fn apply_inverted_lookup<K, V>(map: &mut impl MutableMap<K, V>, key: &K, value: V, diff: StateDiff)
2635where
2636    K: Ord + Clone + Debug,
2637    V: PartialEq + Debug,
2638{
2639    match diff {
2640        StateDiff::Retraction => {
2641            let prev = map.remove(key);
2642            assert_eq!(
2643                prev,
2644                Some(value),
2645                "retraction does not match existing value: {key:?}"
2646            );
2647        }
2648        StateDiff::Addition => {
2649            let prev = map.insert(key.clone(), value);
2650            assert_eq!(
2651                prev, None,
2652                "values must be explicitly retracted before inserting a new value: {key:?}"
2653            );
2654        }
2655    }
2656}
2657
2658/// Helper method to update catalog state, that may need to be updated from a previously retracted
2659/// object.
2660fn apply_with_update<K, V, D>(
2661    map: &mut impl MutableMap<K, V>,
2662    durable: D,
2663    key_fn: impl FnOnce(&D) -> K,
2664    diff: StateDiff,
2665    retractions: &mut BTreeMap<D::Key, V>,
2666) where
2667    K: Ord,
2668    V: UpdateFrom<D> + PartialEq + Debug,
2669    D: DurableType,
2670    D::Key: Ord,
2671{
2672    match diff {
2673        StateDiff::Retraction => {
2674            let mem_key = key_fn(&durable);
2675            let value = map
2676                .remove(&mem_key)
2677                .expect("retraction does not match existing value: {key:?}");
2678            let durable_key = durable.into_key_value().0;
2679            retractions.insert(durable_key, value);
2680        }
2681        StateDiff::Addition => {
2682            let mem_key = key_fn(&durable);
2683            let durable_key = durable.key();
2684            let value = match retractions.remove(&durable_key) {
2685                Some(mut retraction) => {
2686                    retraction.update_from(durable);
2687                    retraction
2688                }
2689                None => durable.into(),
2690            };
2691            let prev = map.insert(mem_key, value);
2692            assert_eq!(
2693                prev, None,
2694                "values must be explicitly retracted before inserting a new value"
2695            );
2696        }
2697    }
2698}
2699
2700/// Looks up a [`BuiltinView`] from a [`SystemObjectMapping`].
2701fn lookup_builtin_view_addition(
2702    mapping: SystemObjectMapping,
2703) -> (&'static BuiltinView, CatalogItemId, GlobalId) {
2704    let (_, builtin) = BUILTIN_LOOKUP
2705        .get(&mapping.description)
2706        .expect("missing builtin view");
2707    let Builtin::View(view) = builtin else {
2708        unreachable!("programming error, expected BuiltinView found {builtin:?}");
2709    };
2710
2711    (
2712        view,
2713        mapping.unique_identifier.catalog_id,
2714        mapping.unique_identifier.global_id,
2715    )
2716}