Skip to main content

mz_catalog/durable/
upgrade.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//! This module contains all the helpers and code paths for upgrading/migrating the `Catalog`.
11//!
12//! We facilitate migrations by keeping snapshots of the objects we previously stored, and relying
13//! entirely on these snapshots. These snapshots exist in the [`mz_catalog_protos`] crate in the
14//! form of `catalog-protos/protos/objects_vXX.proto`. By maintaining and relying on snapshots we
15//! don't have to worry about changes elsewhere in the codebase effecting our migrations because
16//! our application and serialization logic is decoupled, and the objects of the Catalog for a
17//! given version are "frozen in time".
18//!
19//! > **Note**: The protobuf snapshot files themselves live in a separate crate because it takes a
20//!             relatively significant amount of time to codegen and build them. By placing them in
21//!             a separate crate we don't have to pay this compile time cost when building the
22//!             catalog, allowing for faster iteration.
23//!
24//! You cannot make any changes to the following message or anything that they depend on:
25//!
26//!   - Config
27//!   - Setting
28//!   - FenceToken
29//!   - AuditLog
30//!
31//! When you want to make a change to the `Catalog` you need to follow these steps:
32//!
33//! 1. Check the current [`CATALOG_VERSION`], make sure an `objects_v<CATALOG_VERSION>.proto` file
34//!    exists. If one doesn't, copy and paste the current `objects.proto` file, renaming it to
35//!    `objects_v<CATALOG_VERSION>.proto`.
36//! 2. Bump [`CATALOG_VERSION`] by one.
37//! 3. Make your changes to `objects.proto`.
38//! 4. Copy and paste `objects.proto`, naming the copy `objects_v<CATALOG_VERSION>.proto`. Update
39//!    the package name of the `.proto` to `package objects_v<CATALOG_VERSION>;`
40//! 5. We should now have a copy of the protobuf objects as they currently exist, and a copy of
41//!    how we want them to exist. For example, if the version of the Catalog before we made our
42//!    changes was 15, we should now have `objects_v15.proto` and `objects_v16.proto`.
43//! 6. Rebuild Materialize which will error because the hashes stored in
44//!    `src/catalog-protos/protos/hashes.json` have now changed. Update these to match the new
45//!    hashes for objects.proto and `objects_v<CATALOG_VERSION>.proto`.
46//! 7. Add `v<CATALOG_VERSION>` to the call to the `objects!` macro in this file
47//! 8. Add a new file to `catalog/src/durable/upgrade` which is where we'll put the new migration
48//!    path.
49//! 9. Write upgrade functions using the two versions of the protos we now have, e.g.
50//!    `objects_v15.proto` and `objects_v16.proto`. In this migration code you __should not__
51//!    import any defaults or constants from elsewhere in the codebase, because then a future
52//!    change could then impact a previous migration.
53//! 10. Add an import for your new module to this file: mod v<CATALOG_VERSION-1>_to_v<CATALOG_VERSION>;
54//! 11. Call your upgrade function in [`run_upgrade()`].
55//! 12. Generate a test file for the new version:
56//!     ```ignore
57//!     cargo test --package mz-catalog --lib durable::upgrade::tests::generate_missing_encodings -- --ignored
58//!     ```
59//!
60//! When in doubt, reach out to the Surfaces team, and we'll be more than happy to help :)
61
62pub mod json_compatible;
63#[cfg(test)]
64mod tests;
65
66use mz_ore::{soft_assert_eq_or_log, soft_assert_ne_or_log};
67use mz_repr::Diff;
68use paste::paste;
69#[cfg(test)]
70use proptest::prelude::*;
71#[cfg(test)]
72use proptest::strategy::ValueTree;
73#[cfg(test)]
74use proptest_derive::Arbitrary;
75use timely::progress::Timestamp as TimelyTimestamp;
76
77use crate::durable::initialize::USER_VERSION_KEY;
78use crate::durable::objects::serialization::proto;
79use crate::durable::objects::state_update::{
80    IntoStateUpdateKindJson, StateUpdate, StateUpdateKind, StateUpdateKindJson,
81};
82use crate::durable::persist::{Mode, Timestamp, UnopenedPersistCatalogState};
83use crate::durable::{CatalogError, DurableCatalogError};
84
85#[cfg(test)]
86const ENCODED_TEST_CASES: usize = 100;
87
88/// Generate per-version support code.
89///
90/// Here we have to deal with the fact that the pre-v79 objects had a protobuf-generated format,
91/// which gives them additional levels of indirection that the post-v79 objects don't have and thus
92/// requires slightly different code to be generated.
93macro_rules! objects {
94    ( [$( $x_old:ident ),*], [$( $x:ident ),*] ) => {
95        paste! {
96            $(
97                pub(crate) mod [<objects_ $x_old>] {
98                    pub use mz_catalog_protos::[<objects_ $x_old>]::*;
99
100                    use crate::durable::objects::state_update::StateUpdateKindJson;
101
102                    impl From<StateUpdateKind> for StateUpdateKindJson {
103                        fn from(value: StateUpdateKind) -> Self {
104                            let kind = value.kind.expect("kind should be set");
105                            // TODO: This requires that the json->proto->json roundtrips
106                            // exactly, see SQL-895.
107                            StateUpdateKindJson::from_serde(&kind)
108                        }
109                    }
110
111                    impl From<StateUpdateKindJson> for StateUpdateKind {
112                        fn from(value: StateUpdateKindJson) -> Self {
113                            let kind: state_update_kind::Kind = value.to_serde();
114                            StateUpdateKind { kind: Some(kind) }
115                        }
116                    }
117                }
118            )*
119
120            $(
121                pub(crate) mod [<objects_ $x>] {
122                    pub use mz_catalog_protos::[<objects_ $x>]::*;
123
124                    use crate::durable::objects::state_update::StateUpdateKindJson;
125
126                    impl From<StateUpdateKind> for StateUpdateKindJson {
127                        fn from(value: StateUpdateKind) -> Self {
128                            // TODO: This requires that the json->proto->json roundtrips
129                            // exactly, see SQL-895.
130                            StateUpdateKindJson::from_serde(&value)
131                        }
132                    }
133
134                    impl From<StateUpdateKindJson> for StateUpdateKind {
135                        fn from(value: StateUpdateKindJson) -> Self {
136                            value.to_serde()
137                        }
138                    }
139                }
140            )*
141
142            // Generate test helpers for each version.
143
144            #[cfg(test)]
145            #[derive(Debug, Arbitrary)]
146            enum AllVersionsStateUpdateKind {
147                $(
148                    [<$x_old:upper>](crate::durable::upgrade::[<objects_ $x_old>]::StateUpdateKind),
149                )*
150                $(
151                    [<$x:upper>](crate::durable::upgrade::[<objects_ $x>]::StateUpdateKind),
152                )*
153            }
154
155            #[cfg(test)]
156            impl AllVersionsStateUpdateKind {
157                #[cfg(test)]
158                fn arbitrary_vec(version: &str) -> Result<Vec<Self>, String> {
159                    let mut runner = proptest::test_runner::TestRunner::deterministic();
160                    std::iter::repeat(())
161                        .filter_map(|_| {
162                            AllVersionsStateUpdateKind::arbitrary(version, &mut runner)
163                                .transpose()
164                        })
165                        .take(ENCODED_TEST_CASES)
166                        .collect::<Result<_, _>>()
167                }
168
169                #[cfg(test)]
170                fn arbitrary(
171                    version: &str,
172                    runner: &mut proptest::test_runner::TestRunner,
173                ) -> Result<Option<Self>, String> {
174                    match version {
175                        $(
176                            concat!("objects_", stringify!($x_old)) => {
177                                let arbitrary_data =
178                                    crate::durable::upgrade
179                                        ::[<objects_ $x_old>]::StateUpdateKind::arbitrary()
180                                        .new_tree(runner)
181                                        .expect("unable to create arbitrary data")
182                                        .current();
183                                // Skip over generated data where kind is None
184                                // because they are not interesting or possible in
185                                // production. Unfortunately any of the inner fields
186                                // can still be None, which is also not possible in
187                                // production.
188                                // TODO(jkosh44) See if there's an arbitrary config
189                                // that forces Some.
190                                let arbitrary_data = if arbitrary_data.kind.is_some() {
191                                    Some(Self::[<$x_old:upper>](arbitrary_data))
192                                } else {
193                                    None
194                                };
195                                Ok(arbitrary_data)
196                            }
197                        )*
198                        $(
199                            concat!("objects_", stringify!($x)) => {
200                                let arbitrary_data =
201                                    crate::durable::upgrade
202                                        ::[<objects_ $x>]::StateUpdateKind::arbitrary()
203                                        .new_tree(runner)
204                                        .expect("unable to create arbitrary data")
205                                        .current();
206                                Ok(Some(Self::[<$x:upper>](arbitrary_data)))
207                            }
208                        )*
209                        _ => Err(format!("unrecognized version {version} add enum variant")),
210                    }
211                }
212
213                #[cfg(test)]
214                fn try_from_raw(version: &str, raw: StateUpdateKindJson) -> Result<Self, String> {
215                    match version {
216                        $(
217                            concat!("objects_", stringify!($x_old)) => Ok(Self::[<$x_old:upper>](raw.into())),
218                        )*
219                        $(
220                            concat!("objects_", stringify!($x)) => Ok(Self::[<$x:upper>](raw.into())),
221
222                        )*
223                        _ => Err(format!("unrecognized version {version} add enum variant")),
224                    }
225                }
226
227                #[cfg(test)]
228                fn raw(self) -> StateUpdateKindJson {
229                    match self {
230                        $(
231                            Self::[<$x_old:upper>](kind) => kind.into(),
232                        )*
233                        $(
234                            Self::[<$x:upper>](kind) => kind.into(),
235                        )*
236                    }
237                }
238            }
239        }
240    }
241}
242
243objects!(
244    [v74, v75, v76, v77, v78],
245    [
246        v79, v80, v81, v82, v83, v84, v85, v86, v87, v88, v89, v90, v91, v92, v93, v94, v95
247    ]
248);
249
250/// The current version of the `Catalog`.
251pub use mz_catalog_protos::CATALOG_VERSION;
252/// The minimum `Catalog` version number that we support migrating from.
253pub use mz_catalog_protos::MIN_CATALOG_VERSION;
254
255// Note(parkmycar): Ideally we wouldn't have to define these extra constants,
256// but const expressions aren't yet supported in match statements.
257const TOO_OLD_VERSION: u64 = MIN_CATALOG_VERSION - 1;
258const FUTURE_VERSION: u64 = CATALOG_VERSION + 1;
259
260mod v74_to_v75;
261mod v75_to_v76;
262mod v76_to_v77;
263mod v77_to_v78;
264mod v78_to_v79;
265mod v79_to_v80;
266mod v80_to_v81;
267mod v81_to_v82;
268mod v82_to_v83;
269mod v83_to_v84;
270mod v84_to_v85;
271mod v85_to_v86;
272mod v86_to_v87;
273mod v87_to_v88;
274mod v88_to_v89;
275mod v89_to_v90;
276mod v90_to_v91;
277mod v91_to_v92;
278mod v92_to_v93;
279mod v93_to_v94;
280mod v94_to_v95;
281
282/// Describes a single action to take during a migration from `V1` to `V2`.
283#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
284enum MigrationAction<V1: IntoStateUpdateKindJson, V2: IntoStateUpdateKindJson> {
285    /// Deletes the provided key.
286    #[allow(unused)]
287    Delete(V1),
288    /// Inserts the provided key-value pair. The key must not currently exist!
289    #[allow(unused)]
290    Insert(V2),
291    /// Update the key-value pair for the provided key.
292    #[allow(unused)]
293    Update(V1, V2),
294}
295
296impl<V1: IntoStateUpdateKindJson, V2: IntoStateUpdateKindJson> MigrationAction<V1, V2> {
297    /// Converts `self` into a `Vec<StateUpdate<StateUpdateKindBinary>>` that can be appended
298    /// to persist.
299    fn into_updates(self) -> Vec<(StateUpdateKindJson, Diff)> {
300        match self {
301            MigrationAction::Delete(kind) => {
302                vec![(kind.into(), Diff::MINUS_ONE)]
303            }
304            MigrationAction::Insert(kind) => {
305                vec![(kind.into(), Diff::ONE)]
306            }
307            MigrationAction::Update(old_kind, new_kind) => {
308                vec![
309                    (old_kind.into(), Diff::MINUS_ONE),
310                    (new_kind.into(), Diff::ONE),
311                ]
312            }
313        }
314    }
315}
316
317/// Upgrades the data in the catalog to version [`CATALOG_VERSION`].
318///
319/// Returns the current upper after all migrations have executed.
320#[mz_ore::instrument(name = "persist::upgrade", level = "debug")]
321pub(crate) async fn upgrade(
322    persist_handle: &mut UnopenedPersistCatalogState,
323    mut commit_ts: Timestamp,
324    force_migration: bool,
325) -> Result<Timestamp, CatalogError> {
326    soft_assert_ne_or_log!(
327        persist_handle.upper,
328        Timestamp::minimum(),
329        "cannot upgrade uninitialized catalog"
330    );
331
332    // Consolidate to avoid migrating old state.
333    persist_handle.consolidate();
334    let mut version = persist_handle
335        .get_user_version()
336        .await?
337        .expect("initialized catalog must have a version");
338    // Run migrations until we're up-to-date.
339    while version < CATALOG_VERSION {
340        (version, commit_ts) = run_upgrade(persist_handle, version, commit_ts).await?;
341    }
342    if force_migration {
343        tracing::info!(version, "running forced Catalog upgrade");
344        commit_ts = apply_migration(persist_handle, Vec::new(), commit_ts).await?;
345    }
346
347    Ok(commit_ts)
348}
349
350/// Determines which upgrade to run for the `version` and executes it.
351///
352/// Returns the new version and upper.
353async fn run_upgrade(
354    unopened_catalog_state: &mut UnopenedPersistCatalogState,
355    version: u64,
356    commit_ts: Timestamp,
357) -> Result<(u64, Timestamp), CatalogError> {
358    let incompatible = DurableCatalogError::IncompatibleDataVersion {
359        found_version: version,
360        min_catalog_version: MIN_CATALOG_VERSION,
361        catalog_version: CATALOG_VERSION,
362    }
363    .into();
364
365    match version {
366        ..=TOO_OLD_VERSION => Err(incompatible),
367
368        74 => {
369            run_versioned_upgrade(
370                unopened_catalog_state,
371                version,
372                commit_ts,
373                v74_to_v75::upgrade,
374            )
375            .await
376        }
377        75 => {
378            run_versioned_upgrade(
379                unopened_catalog_state,
380                version,
381                commit_ts,
382                v75_to_v76::upgrade,
383            )
384            .await
385        }
386        76 => {
387            run_versioned_upgrade(
388                unopened_catalog_state,
389                version,
390                commit_ts,
391                v76_to_v77::upgrade,
392            )
393            .await
394        }
395        77 => {
396            run_versioned_upgrade(
397                unopened_catalog_state,
398                version,
399                commit_ts,
400                v77_to_v78::upgrade,
401            )
402            .await
403        }
404        78 => {
405            run_versioned_upgrade(
406                unopened_catalog_state,
407                version,
408                commit_ts,
409                v78_to_v79::upgrade,
410            )
411            .await
412        }
413        79 => {
414            run_versioned_upgrade(
415                unopened_catalog_state,
416                version,
417                commit_ts,
418                v79_to_v80::upgrade,
419            )
420            .await
421        }
422        80 => {
423            run_versioned_upgrade(
424                unopened_catalog_state,
425                version,
426                commit_ts,
427                v80_to_v81::upgrade,
428            )
429            .await
430        }
431        81 => {
432            run_versioned_upgrade(
433                unopened_catalog_state,
434                version,
435                commit_ts,
436                v81_to_v82::upgrade,
437            )
438            .await
439        }
440        // v82→v83 is a one-shot byte-level repair, not a proto evolution. It
441        // needs raw access to the snapshot's diffs (which `run_versioned_upgrade`
442        // strips) so it plugs into `run_upgrade` directly.
443        82 => v82_to_v83::upgrade(unopened_catalog_state, commit_ts).await,
444        83 => v83_to_v84::upgrade(unopened_catalog_state, commit_ts).await,
445        84 => {
446            run_versioned_upgrade(
447                unopened_catalog_state,
448                version,
449                commit_ts,
450                v84_to_v85::upgrade,
451            )
452            .await
453        }
454        85 => {
455            run_versioned_upgrade(
456                unopened_catalog_state,
457                version,
458                commit_ts,
459                v85_to_v86::upgrade,
460            )
461            .await
462        }
463        86 => {
464            run_versioned_upgrade(
465                unopened_catalog_state,
466                version,
467                commit_ts,
468                v86_to_v87::upgrade,
469            )
470            .await
471        }
472        87 => {
473            run_versioned_upgrade(
474                unopened_catalog_state,
475                version,
476                commit_ts,
477                v87_to_v88::upgrade,
478            )
479            .await
480        }
481        88 => {
482            run_versioned_upgrade(
483                unopened_catalog_state,
484                version,
485                commit_ts,
486                v88_to_v89::upgrade,
487            )
488            .await
489        }
490        89 => {
491            run_versioned_upgrade(
492                unopened_catalog_state,
493                version,
494                commit_ts,
495                v89_to_v90::upgrade,
496            )
497            .await
498        }
499        90 => {
500            run_versioned_upgrade(
501                unopened_catalog_state,
502                version,
503                commit_ts,
504                v90_to_v91::upgrade,
505            )
506            .await
507        }
508        91 => {
509            run_versioned_upgrade(
510                unopened_catalog_state,
511                version,
512                commit_ts,
513                v91_to_v92::upgrade,
514            )
515            .await
516        }
517        92 => {
518            run_versioned_upgrade(
519                unopened_catalog_state,
520                version,
521                commit_ts,
522                v92_to_v93::upgrade,
523            )
524            .await
525        }
526        93 => {
527            run_versioned_upgrade(
528                unopened_catalog_state,
529                version,
530                commit_ts,
531                v93_to_v94::upgrade,
532            )
533            .await
534        }
535        94 => {
536            run_versioned_upgrade(
537                unopened_catalog_state,
538                version,
539                commit_ts,
540                v94_to_v95::upgrade,
541            )
542            .await
543        }
544        // Up-to-date, no migration needed!
545        CATALOG_VERSION => Ok((CATALOG_VERSION, commit_ts)),
546        FUTURE_VERSION.. => Err(incompatible),
547    }
548}
549
550/// Runs `migration_logic` on the contents of the current catalog assuming a current version of
551/// `current_version`.
552///
553/// Returns the new version and upper.
554async fn run_versioned_upgrade<V1: IntoStateUpdateKindJson, V2: IntoStateUpdateKindJson>(
555    unopened_catalog_state: &mut UnopenedPersistCatalogState,
556    current_version: u64,
557    commit_ts: Timestamp,
558    migration_logic: impl FnOnce(Vec<V1>) -> Vec<MigrationAction<V1, V2>>,
559) -> Result<(u64, Timestamp), CatalogError> {
560    tracing::info!(current_version, "running versioned Catalog upgrade");
561
562    // 1. Use the V1 to deserialize the contents of the current snapshot.
563    let snapshot: Vec<_> = unopened_catalog_state
564        .snapshot
565        .iter()
566        .map(|(kind, ts, diff)| {
567            soft_assert_eq_or_log!(
568                *diff,
569                Diff::ONE,
570                "snapshot is consolidated, ({kind:?}, {ts:?}, {diff:?})"
571            );
572            V1::try_from(kind.clone()).expect("invalid catalog data persisted")
573        })
574        .collect();
575
576    // 2. Generate updates from version specific migration logic.
577    let migration_actions = migration_logic(snapshot);
578    let mut updates: Vec<_> = migration_actions
579        .into_iter()
580        .flat_map(|action| action.into_updates().into_iter())
581        .collect();
582    // Validate that we're not migrating an un-migratable collection.
583    for (update, _) in &updates {
584        if update.is_always_deserializable() {
585            panic!("migration to un-migratable collection: {update:?}\nall updates: {updates:?}");
586        }
587    }
588
589    // 3. Add a retraction for old version and insertion for new version into updates.
590    let next_version = current_version + 1;
591    let version_retraction = (version_update_kind(current_version), Diff::MINUS_ONE);
592    updates.push(version_retraction);
593    let version_insertion = (version_update_kind(next_version), Diff::ONE);
594    updates.push(version_insertion);
595
596    // 4. Apply migration to catalog.
597    let commit_ts = apply_migration(unopened_catalog_state, updates, commit_ts).await?;
598
599    // 5. Consolidate snapshot to remove old versions.
600    unopened_catalog_state.consolidate();
601
602    Ok((next_version, commit_ts))
603}
604
605/// Applies one migration's `updates` at `commit_ts` and returns the new upper.
606async fn apply_migration(
607    unopened_catalog_state: &mut UnopenedPersistCatalogState,
608    updates: Vec<(StateUpdateKindJson, Diff)>,
609    commit_ts: Timestamp,
610) -> Result<Timestamp, CatalogError> {
611    if matches!(unopened_catalog_state.mode, Mode::Writable) {
612        Ok(unopened_catalog_state
613            .compare_and_append(updates, commit_ts)
614            .await
615            .map_err(|e| e.unwrap_fence_error())?)
616    } else {
617        let ts = commit_ts;
618        let updates = updates
619            .into_iter()
620            .map(|(kind, diff)| StateUpdate { kind, ts, diff });
621        unopened_catalog_state.apply_updates_and_consolidate(updates)?;
622        Ok(commit_ts.step_forward())
623    }
624}
625
626/// Generates a [`proto::StateUpdateKind`] to update the user version.
627fn version_update_kind(version: u64) -> StateUpdateKindJson {
628    // We can use the current version because Configs can never be migrated and are always wire
629    // compatible.
630    StateUpdateKind::Config(
631        proto::ConfigKey {
632            key: USER_VERSION_KEY.to_string(),
633        },
634        proto::ConfigValue { value: version },
635    )
636    .into()
637}