Skip to main content

mz_adapter/coord/catalog_implications/
parsed_state_updates.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! Utilities for parsing and augmenting "raw" catalog changes
11//! ([StateUpdateKind]), so that we can update in-memory adapter state, apply
12//! implications, and apply derived commands to the controller(s).
13//!
14//! See [parse_state_update] for details.
15
16use mz_catalog::builtin::BUILTIN_LOG_LOOKUP;
17use mz_catalog::memory::objects::{
18    CatalogItem, DataSourceDesc, StateDiff, StateUpdate, StateUpdateKind,
19};
20use mz_catalog::{durable, memory};
21use mz_compute_client::logging::LogVariant;
22use mz_controller_types::ClusterId;
23use mz_ore::instrument;
24use mz_repr::{CatalogItemId, GlobalId, Timestamp};
25use mz_sql::names::FullItemName;
26use mz_storage_types::connections::inline::IntoInlineConnection;
27use mz_storage_types::sources::GenericSourceConnection;
28
29// DO NOT add any more imports from `crate` outside of `crate::catalog`.
30use crate::catalog::CatalogState;
31
32/// An update that needs to be applied to a controller.
33#[derive(Debug, Clone)]
34pub struct ParsedStateUpdate {
35    pub kind: ParsedStateUpdateKind,
36    pub ts: Timestamp,
37    pub diff: StateDiff,
38}
39
40/// An update that needs to be applied to a controller.
41#[derive(Debug, Clone)]
42pub enum ParsedStateUpdateKind {
43    Item {
44        durable_item: durable::objects::Item,
45        parsed_item: memory::objects::CatalogItem,
46        connection: Option<GenericSourceConnection>,
47        parsed_full_name: FullItemName,
48    },
49    Cluster {
50        durable_cluster: durable::objects::Cluster,
51        parsed_cluster: memory::objects::Cluster,
52    },
53    ClusterReplica {
54        durable_cluster_replica: durable::objects::ClusterReplica,
55        parsed_cluster_replica: memory::objects::ClusterReplica,
56    },
57    IntrospectionSourceIndex {
58        cluster_id: ClusterId,
59        log: LogVariant,
60        index_id: GlobalId,
61    },
62    /// A replica-scoped system-parameter override changed. The implication
63    /// re-pushes the complete per-replica dyncfg layer from the catalog working
64    /// copy, so it does not consume `durable`. We keep the row only so it shows
65    /// up in the `tracing::trace!` of the parsed update.
66    ReplicaSystemConfiguration {
67        durable: durable::objects::ReplicaSystemConfiguration,
68    },
69    /// An environment-wide system-parameter changed. The implication re-runs the
70    /// `SystemVars` callbacks against the committed values, so it does not
71    /// consume `durable`. We keep the row only for the `tracing::trace!`.
72    SystemConfiguration {
73        durable: durable::objects::SystemConfiguration,
74    },
75}
76
77/// Potentially generate a [ParsedStateUpdate] that corresponds to the given
78/// change to the catalog.
79///
80/// This technically doesn't "parse" the given state update but uses the given
81/// in-memory [CatalogState] as a shortcut. It already contains the parsed
82/// representation of the item. In theory, we could re-construct the parsed
83/// items by hand if we're given all the changes that lead to a given catalog
84/// state.
85///
86/// For changes with a positive diff, the given [CatalogState] must reflect the
87/// catalog state _after_ applying the catalog change to the catalog. For
88/// negative changes, the given [CatalogState] must reflect the catalog state
89/// _before_ applying the changes. This is so that we can easily extract the
90/// state of an object before it is removed.
91///
92/// Will return `None` if the given catalog change is purely internal to the
93/// catalog and does not have implications for anything else.
94#[instrument(level = "debug")]
95pub fn parse_state_update(
96    catalog: &CatalogState,
97    state_update: StateUpdate,
98) -> Option<ParsedStateUpdate> {
99    let kind = match state_update.kind {
100        StateUpdateKind::Item(item) => Some(parse_item_update(catalog, item)),
101        StateUpdateKind::Cluster(cluster) => Some(parse_cluster_update(catalog, cluster)),
102        StateUpdateKind::ClusterReplica(replica) => {
103            Some(parse_cluster_replica_update(catalog, replica))
104        }
105        StateUpdateKind::IntrospectionSourceIndex(isi) => {
106            Some(parse_introspection_source_index_update(isi))
107        }
108        StateUpdateKind::ReplicaSystemConfiguration(durable) => {
109            Some(ParsedStateUpdateKind::ReplicaSystemConfiguration { durable })
110        }
111        StateUpdateKind::SystemConfiguration(durable) => {
112            Some(ParsedStateUpdateKind::SystemConfiguration { durable })
113        }
114        _ => {
115            // The controllers are currently not interested in other kinds of
116            // changes to the catalog. Cluster-scoped system-parameter overrides
117            // are read at plan time and have no controller effect, so they stay
118            // here too.
119            None
120        }
121    };
122
123    kind.map(|kind| ParsedStateUpdate {
124        kind,
125        ts: state_update.ts,
126        diff: state_update.diff,
127    })
128}
129
130fn parse_item_update(
131    catalog: &CatalogState,
132    durable_item: durable::objects::Item,
133) -> ParsedStateUpdateKind {
134    let (parsed_item, connection, parsed_full_name) =
135        parse_item_update_common(catalog, &durable_item.id);
136
137    ParsedStateUpdateKind::Item {
138        durable_item,
139        parsed_item,
140        connection,
141        parsed_full_name,
142    }
143}
144
145fn parse_item_update_common(
146    catalog: &CatalogState,
147    item_id: &CatalogItemId,
148) -> (CatalogItem, Option<GenericSourceConnection>, FullItemName) {
149    let entry = catalog.get_entry(item_id);
150
151    let parsed_item = entry.item().clone();
152    let parsed_full_name = catalog.resolve_full_name(entry.name(), entry.conn_id());
153
154    let connection = match &parsed_item {
155        memory::objects::CatalogItem::Source(source) => {
156            if let DataSourceDesc::Ingestion { desc, .. }
157            | DataSourceDesc::OldSyntaxIngestion { desc, .. } = &source.data_source
158            {
159                Some(desc.connection.clone().into_inline_connection(catalog))
160            } else {
161                None
162            }
163        }
164        _ => None,
165    };
166
167    (parsed_item, connection, parsed_full_name)
168}
169
170fn parse_cluster_update(
171    catalog: &CatalogState,
172    durable_cluster: durable::objects::Cluster,
173) -> ParsedStateUpdateKind {
174    let parsed_cluster = catalog.get_cluster(durable_cluster.id);
175
176    ParsedStateUpdateKind::Cluster {
177        durable_cluster,
178        parsed_cluster: parsed_cluster.clone(),
179    }
180}
181
182fn parse_introspection_source_index_update(
183    isi: durable::objects::IntrospectionSourceIndex,
184) -> ParsedStateUpdateKind {
185    let builtin_log = BUILTIN_LOG_LOOKUP
186        .get(isi.name.as_str())
187        .expect("introspection source index must reference a known log");
188
189    ParsedStateUpdateKind::IntrospectionSourceIndex {
190        cluster_id: isi.cluster_id,
191        log: builtin_log.variant.clone(),
192        index_id: isi.index_id,
193    }
194}
195
196fn parse_cluster_replica_update(
197    catalog: &CatalogState,
198    durable_cluster_replica: durable::objects::ClusterReplica,
199) -> ParsedStateUpdateKind {
200    let parsed_cluster_replica = catalog.get_cluster_replica(
201        durable_cluster_replica.cluster_id,
202        durable_cluster_replica.replica_id,
203    );
204
205    ParsedStateUpdateKind::ClusterReplica {
206        durable_cluster_replica,
207        parsed_cluster_replica: parsed_cluster_replica.clone(),
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use mz_catalog::durable::objects::{ReplicaSystemConfiguration, SystemConfiguration};
214    use mz_catalog::memory::objects::{StateDiff, StateUpdate, StateUpdateKind};
215    use mz_controller_types::ReplicaId;
216    use mz_repr::Timestamp;
217
218    use crate::catalog::Catalog;
219
220    use super::{ParsedStateUpdateKind, parse_state_update};
221
222    /// A replica-scoped system-parameter change must produce a parsed update so
223    /// the controller push fires. It was previously dropped as a change the
224    /// controllers were not interested in.
225    #[mz_ore::test(tokio::test)]
226    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function on OS `linux`
227    async fn replica_system_configuration_is_parsed() {
228        Catalog::with_debug(|catalog| async move {
229            let durable = ReplicaSystemConfiguration {
230                replica_id: ReplicaId::User(1),
231                name: "persist_pager".to_string(),
232                value: "on".to_string(),
233            };
234            let update = StateUpdate {
235                kind: StateUpdateKind::ReplicaSystemConfiguration(durable),
236                ts: Timestamp::MIN,
237                diff: StateDiff::Addition,
238            };
239            let parsed = parse_state_update(catalog.state(), update)
240                .expect("replica system configuration must produce a parsed update");
241            assert!(matches!(
242                parsed.kind,
243                ParsedStateUpdateKind::ReplicaSystemConfiguration { .. }
244            ));
245        })
246        .await
247    }
248
249    /// An environment-wide system-parameter change must produce a parsed update
250    /// so the `SystemVars` callback notification fires from
251    /// `apply_catalog_implications`, including on a follower `environmentd` that
252    /// only replays the committed diff. It was previously dropped as a change the
253    /// controllers were not interested in.
254    #[mz_ore::test(tokio::test)]
255    #[cfg_attr(miri, ignore)] // unsupported operation: can't call foreign function on OS `linux`
256    async fn system_configuration_is_parsed() {
257        Catalog::with_debug(|catalog| async move {
258            let durable = SystemConfiguration {
259                name: "max_connections".to_string(),
260                value: "42".to_string(),
261            };
262            let update = StateUpdate {
263                kind: StateUpdateKind::SystemConfiguration(durable),
264                ts: Timestamp::MIN,
265                diff: StateDiff::Addition,
266            };
267            let parsed = parse_state_update(catalog.state(), update)
268                .expect("system configuration must produce a parsed update");
269            assert!(matches!(
270                parsed.kind,
271                ParsedStateUpdateKind::SystemConfiguration { .. }
272            ));
273        })
274        .await
275    }
276}