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