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