Skip to main content

mz_catalog/
expr_cache.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//! A cache for optimized expressions.
11
12use std::collections::{BTreeMap, BTreeSet};
13use std::future::Future;
14use std::sync::Arc;
15
16use bytes::Bytes;
17use mz_compute_types::dataflows::DataflowDescription;
18use mz_durable_cache::{DurableCache, DurableCacheCodec};
19use mz_dyncfg::ConfigSet;
20use mz_expr::OptimizedMirRelationExpr;
21use mz_ore::channel::trigger;
22use mz_ore::soft_panic_or_log;
23use mz_ore::task::spawn;
24use mz_persist_client::PersistClient;
25use mz_persist_client::cli::admin::{
26    EXPRESSION_CACHE_FORCE_COMPACTION_FUEL, EXPRESSION_CACHE_FORCE_COMPACTION_WAIT,
27};
28use mz_persist_types::codec_impls::VecU8Schema;
29use mz_persist_types::{Codec, ShardId};
30use mz_repr::optimize::OptimizerFeatures;
31use mz_repr::{GlobalId, RelationVersion};
32use mz_transform::dataflow::DataflowMetainfo;
33use mz_transform::notice::OptimizerNotice;
34use semver::Version;
35use serde::{Deserialize, Serialize};
36use tokio::sync::mpsc;
37use tracing::{debug, info, warn};
38
39#[derive(
40    Debug,
41    Clone,
42    PartialEq,
43    Eq,
44    PartialOrd,
45    Ord,
46    Hash,
47    Serialize,
48    Deserialize
49)]
50enum ExpressionType {
51    Local,
52    Global,
53}
54
55/// The data that is cached per catalog object as a result of local optimizations.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
57pub struct LocalExpressions {
58    pub local_mir: OptimizedMirRelationExpr,
59    pub optimizer_features: OptimizerFeatures,
60    /// The owning item's latest version, see [`ExpressionCache::open`].
61    pub item_version: RelationVersion,
62}
63
64/// The data that is cached per catalog object as a result of global optimizations.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66pub struct GlobalExpressions {
67    pub global_mir: DataflowDescription<OptimizedMirRelationExpr>,
68    pub physical_plan: mz_compute_types::dataflows::LirDataflowDescription,
69    pub dataflow_metainfos: DataflowMetainfo<Arc<OptimizerNotice>>,
70    pub optimizer_features: OptimizerFeatures,
71    /// The owning item's latest version, see [`ExpressionCache::open`].
72    pub item_version: RelationVersion,
73}
74
75impl GlobalExpressions {
76    fn index_imports(&self) -> impl Iterator<Item = &GlobalId> {
77        self.global_mir
78            .index_imports
79            .keys()
80            .chain(self.physical_plan.index_imports.keys())
81    }
82}
83
84/// The item version to record with an item's cached expressions, the latest of its `versions`.
85pub fn latest_item_version(versions: &BTreeMap<RelationVersion, GlobalId>) -> RelationVersion {
86    versions
87        .last_key_value()
88        .map_or_else(RelationVersion::root, |(version, _)| *version)
89}
90
91#[derive(
92    Debug,
93    Clone,
94    PartialEq,
95    Eq,
96    PartialOrd,
97    Ord,
98    Hash,
99    Serialize,
100    Deserialize
101)]
102struct CacheKey {
103    build_version: String,
104    id: GlobalId,
105    expr_type: ExpressionType,
106}
107
108#[derive(Debug, PartialEq, Eq)]
109struct ExpressionCodec;
110
111impl DurableCacheCodec for ExpressionCodec {
112    type Key = CacheKey;
113    // We use a raw bytes instead of `Expressions` so that there is no backwards compatibility
114    // requirement on `Expressions` between build versions.
115    type Val = Bytes;
116    type KeyCodec = Bytes;
117    type ValCodec = Bytes;
118
119    fn schemas() -> (
120        <Self::KeyCodec as Codec>::Schema,
121        <Self::ValCodec as Codec>::Schema,
122    ) {
123        (VecU8Schema::default(), VecU8Schema::default())
124    }
125
126    fn encode(key: &Self::Key, val: &Self::Val) -> (Self::KeyCodec, Self::ValCodec) {
127        let key = bincode::serialize(key).expect("must serialize");
128        (Bytes::from(key), val.clone())
129    }
130
131    fn decode(key: &Self::KeyCodec, val: &Self::ValCodec) -> (Self::Key, Self::Val) {
132        let key = bincode::deserialize(key).expect("must deserialize");
133        (key, val.clone())
134    }
135}
136
137/// Counts of the entries [`ExpressionCache::open`] removed, by reason.
138#[derive(Debug, Default)]
139struct RemovedEntries {
140    unparsable_build_version: usize,
141    dropped_item: usize,
142    item_version_mismatch: usize,
143    dropped_index_import: usize,
144    prior_build_version: usize,
145}
146
147/// Configuration needed to initialize an [`ExpressionCache`].
148#[derive(Debug, Clone)]
149pub struct ExpressionCacheConfig {
150    pub build_version: Version,
151    pub persist: PersistClient,
152    pub shard_id: ShardId,
153    /// Every `GlobalId` that may have entries, mapped to the latest version of the item it
154    /// belongs to. All ids of an item map to that same version, which is the one its entries
155    /// record, rather than the version the id itself denotes.
156    pub current_items: BTreeMap<GlobalId, RelationVersion>,
157    /// Whether to durably remove the entries of previous build versions.
158    pub remove_prior_versions: bool,
159    pub compact_shard: bool,
160    pub dyncfgs: ConfigSet,
161}
162
163/// A durable cache of optimized expressions.
164pub struct ExpressionCache {
165    build_version: Version,
166    durable_cache: DurableCache<ExpressionCodec>,
167}
168
169impl ExpressionCache {
170    /// Creates a new [`ExpressionCache`] and reconciles all entries in current build version.
171    /// Reconciliation removes entries whose id is not in `current_items` or whose recorded
172    /// item version differs from the current one, and global entries that import an index
173    /// that is not in `current_items`.
174    ///
175    /// An entry records the item version it was optimized for, because applying a materialized
176    /// view replacement changes an item's definition while the item keeps its `GlobalId`s and
177    /// bumps its [`RelationVersion`]. Dropping entries recorded at another version therefore
178    /// catches stale entries whichever process wrote them. That includes the replacement's own
179    /// entries: they are recorded at its root version, and the apply makes its id a later
180    /// version of the target.
181    ///
182    /// The apply also invalidates the target's entries, but that alone would not do: it only
183    /// reaches entries under the applying process's build version, while a 0dt deployment in
184    /// its read-only phase caches under its own build version and reads those entries again
185    /// when it reboots after promotion.
186    ///
187    /// If `remove_prior_versions` is `true`, then the entries of all previous build versions are
188    /// durably removed from the cache.
189    ///
190    /// If `compact_shard` is `true`, then this function will block on fully compacting the backing
191    /// persist shard.
192    ///
193    /// Returns all cached expressions in the current build version, after reconciliation.
194    pub async fn open(
195        ExpressionCacheConfig {
196            build_version,
197            persist,
198            shard_id,
199            current_items,
200            remove_prior_versions,
201            compact_shard,
202            dyncfgs,
203        }: ExpressionCacheConfig,
204    ) -> (
205        Self,
206        BTreeMap<GlobalId, LocalExpressions>,
207        BTreeMap<GlobalId, GlobalExpressions>,
208    ) {
209        let durable_cache = DurableCache::new(&persist, shard_id, "expressions").await;
210        let mut cache = Self {
211            build_version,
212            durable_cache,
213        };
214
215        const RETRIES: usize = 100;
216        for _ in 0..RETRIES {
217            match cache
218                .try_open(
219                    &current_items,
220                    remove_prior_versions,
221                    compact_shard,
222                    &dyncfgs,
223                )
224                .await
225            {
226                Ok((local_expressions, global_expressions)) => {
227                    return (cache, local_expressions, global_expressions);
228                }
229                Err(err) => debug!("failed to open cache: {err} ... retrying"),
230            }
231        }
232
233        panic!("Unable to open expression cache after {RETRIES} retries");
234    }
235
236    async fn try_open(
237        &mut self,
238        current_items: &BTreeMap<GlobalId, RelationVersion>,
239        remove_prior_versions: bool,
240        compact_shard: bool,
241        dyncfgs: &ConfigSet,
242    ) -> Result<
243        (
244            BTreeMap<GlobalId, LocalExpressions>,
245            BTreeMap<GlobalId, GlobalExpressions>,
246        ),
247        mz_durable_cache::Error,
248    > {
249        let mut keys_to_remove = Vec::new();
250        let mut local_expressions = BTreeMap::new();
251        let mut global_expressions = BTreeMap::new();
252        let mut removed = RemovedEntries::default();
253        let mut unreadable = 0;
254
255        for (key, expressions) in self.durable_cache.entries_local() {
256            let build_version = match key.build_version.parse::<Version>() {
257                Ok(build_version) => build_version,
258                Err(err) => {
259                    warn!("unable to parse build version: {key:?}: {err:?}");
260                    removed.unparsable_build_version += 1;
261                    keys_to_remove.push((key.clone(), None));
262                    continue;
263                }
264            };
265            if build_version == self.build_version {
266                // Only deserialize the current build version.
267                match key.expr_type {
268                    ExpressionType::Local => {
269                        let expressions: LocalExpressions = match bincode::deserialize(expressions)
270                        {
271                            Ok(expressions) => expressions,
272                            Err(err) => {
273                                soft_panic_or_log!(
274                                    "unable to deserialize local expressions: ({key:?}, {expressions:?}): {err:?}"
275                                );
276                                unreadable += 1;
277                                continue;
278                            }
279                        };
280                        match current_items.get(&key.id) {
281                            None => {
282                                removed.dropped_item += 1;
283                                keys_to_remove.push((key.clone(), None));
284                            }
285                            // The entry was optimized for another definition of the item.
286                            Some(version) if *version != expressions.item_version => {
287                                removed.item_version_mismatch += 1;
288                                keys_to_remove.push((key.clone(), None));
289                            }
290                            Some(_) => {
291                                local_expressions.insert(key.id, expressions);
292                            }
293                        }
294                    }
295                    ExpressionType::Global => {
296                        let expressions: GlobalExpressions = match bincode::deserialize(expressions)
297                        {
298                            Ok(expressions) => expressions,
299                            Err(err) => {
300                                soft_panic_or_log!(
301                                    "unable to deserialize global expressions: ({key:?}, {expressions:?}): {err:?}"
302                                );
303                                unreadable += 1;
304                                continue;
305                            }
306                        };
307                        match current_items.get(&key.id) {
308                            None => {
309                                removed.dropped_item += 1;
310                                keys_to_remove.push((key.clone(), None));
311                            }
312                            Some(version) if *version != expressions.item_version => {
313                                removed.item_version_mismatch += 1;
314                                keys_to_remove.push((key.clone(), None));
315                            }
316                            // The plan reads an index that was dropped while the dataflow kept
317                            // running, so the consumer has to be re-optimized at the next boot.
318                            Some(_)
319                                if !expressions
320                                    .index_imports()
321                                    .all(|id| current_items.contains_key(id)) =>
322                            {
323                                removed.dropped_index_import += 1;
324                                keys_to_remove.push((key.clone(), None));
325                            }
326                            Some(_) => {
327                                global_expressions.insert(key.id, expressions);
328                            }
329                        }
330                    }
331                }
332            } else if remove_prior_versions {
333                removed.prior_build_version += 1;
334                keys_to_remove.push((key.clone(), None));
335            }
336        }
337        let keys_to_remove: Vec<_> = keys_to_remove
338            .iter()
339            .map(|(key, expressions)| (key, expressions.as_ref()))
340            .collect();
341        self.durable_cache.try_set_many(&keys_to_remove).await?;
342        let RemovedEntries {
343            unparsable_build_version,
344            dropped_item,
345            item_version_mismatch,
346            dropped_index_import,
347            prior_build_version,
348        } = removed;
349        info!(
350            build_version = %self.build_version,
351            remove_prior_versions,
352            kept_local = local_expressions.len(),
353            kept_global = global_expressions.len(),
354            skipped_unreadable = unreadable,
355            removed_unparsable_build_version = unparsable_build_version,
356            removed_dropped_item = dropped_item,
357            removed_item_version_mismatch = item_version_mismatch,
358            removed_dropped_index_import = dropped_index_import,
359            removed_prior_build_version = prior_build_version,
360            "reconciled the expression cache"
361        );
362
363        if remove_prior_versions {
364            // We've purged old build versions from the cache. Upgrade the backing Persist version
365            // as well.
366            self.durable_cache.upgrade_version().await;
367        }
368
369        if compact_shard {
370            let fuel = EXPRESSION_CACHE_FORCE_COMPACTION_FUEL.handle(dyncfgs);
371            let wait = EXPRESSION_CACHE_FORCE_COMPACTION_WAIT.handle(dyncfgs);
372            self.durable_cache
373                .dangerous_compact_shard(move || fuel.get(), move || wait.get())
374                .await;
375        }
376
377        Ok((local_expressions, global_expressions))
378    }
379
380    /// Durably removes all entries given by `invalidate_ids` and inserts `new_local_expressions`
381    /// and `new_global_expressions` into current build version.
382    ///
383    /// If there is a duplicate ID in both `invalidate_ids` and one of the new expressions vector,
384    /// then the final value will be taken from the new expressions vector.
385    async fn update(
386        &mut self,
387        new_local_expressions: Vec<(GlobalId, LocalExpressions)>,
388        new_global_expressions: Vec<(GlobalId, GlobalExpressions)>,
389        invalidate_ids: BTreeSet<GlobalId>,
390    ) {
391        let mut entries = BTreeMap::new();
392        let build_version = self.build_version.to_string();
393        // Important to do `invalidate_ids` first, so that `new_X_expressions` overwrites duplicate
394        // keys.
395        for id in invalidate_ids {
396            entries.insert(
397                CacheKey {
398                    id,
399                    build_version: build_version.clone(),
400                    expr_type: ExpressionType::Local,
401                },
402                None,
403            );
404            entries.insert(
405                CacheKey {
406                    id,
407                    build_version: build_version.clone(),
408                    expr_type: ExpressionType::Global,
409                },
410                None,
411            );
412        }
413        for (id, expressions) in new_local_expressions {
414            let expressions = match bincode::serialize(&expressions) {
415                Ok(expressions) => Bytes::from(expressions),
416                Err(err) => {
417                    soft_panic_or_log!(
418                        "unable to serialize local expressions: {expressions:?}: {err:?}"
419                    );
420                    continue;
421                }
422            };
423            entries.insert(
424                CacheKey {
425                    id,
426                    build_version: build_version.clone(),
427                    expr_type: ExpressionType::Local,
428                },
429                Some(expressions),
430            );
431        }
432        for (id, expressions) in new_global_expressions {
433            let expressions = match bincode::serialize(&expressions) {
434                Ok(expressions) => Bytes::from(expressions),
435                Err(err) => {
436                    soft_panic_or_log!(
437                        "unable to serialize global expressions: {expressions:?}: {err:?}"
438                    );
439                    continue;
440                }
441            };
442            entries.insert(
443                CacheKey {
444                    id,
445                    build_version: build_version.clone(),
446                    expr_type: ExpressionType::Global,
447                },
448                Some(expressions),
449            );
450        }
451        let entries: Vec<_> = entries
452            .iter()
453            .map(|(key, expressions)| (key, expressions.as_ref()))
454            .collect();
455        self.durable_cache.set_many(&entries).await
456    }
457}
458
459/// Operations to perform on the cache.
460enum CacheOperation {
461    /// See [`ExpressionCache::update`].
462    Update {
463        new_local_expressions: Vec<(GlobalId, LocalExpressions)>,
464        new_global_expressions: Vec<(GlobalId, GlobalExpressions)>,
465        invalidate_ids: BTreeSet<GlobalId>,
466        trigger: trigger::Trigger,
467    },
468}
469
470#[derive(Debug, Clone)]
471pub struct ExpressionCacheHandle {
472    tx: mpsc::UnboundedSender<CacheOperation>,
473}
474
475impl ExpressionCacheHandle {
476    /// Spawns a task responsible for managing the expression cache. See [`ExpressionCache::open`].
477    ///
478    /// Returns a handle to interact with the cache and the initial contents of the cache.
479    pub async fn spawn_expression_cache(
480        config: ExpressionCacheConfig,
481    ) -> (
482        Self,
483        BTreeMap<GlobalId, LocalExpressions>,
484        BTreeMap<GlobalId, GlobalExpressions>,
485    ) {
486        let (mut cache, local_expressions, global_expressions) =
487            ExpressionCache::open(config).await;
488        let (tx, mut rx) = mpsc::unbounded_channel();
489        spawn(|| "expression-cache-task", async move {
490            while let Some(op) = rx.recv().await {
491                match op {
492                    CacheOperation::Update {
493                        new_local_expressions,
494                        new_global_expressions,
495                        invalidate_ids,
496                        trigger: _trigger,
497                    } => {
498                        cache
499                            .update(
500                                new_local_expressions,
501                                new_global_expressions,
502                                invalidate_ids,
503                            )
504                            .await
505                    }
506                }
507            }
508        });
509
510        (Self { tx }, local_expressions, global_expressions)
511    }
512
513    pub fn update(
514        &self,
515        new_local_expressions: Vec<(GlobalId, LocalExpressions)>,
516        new_global_expressions: Vec<(GlobalId, GlobalExpressions)>,
517        invalidate_ids: BTreeSet<GlobalId>,
518    ) -> impl Future<Output = ()> + use<> {
519        let (trigger, trigger_rx) = trigger::channel();
520        let op = CacheOperation::Update {
521            new_local_expressions,
522            new_global_expressions,
523            invalidate_ids,
524            trigger,
525        };
526        // If the send fails, then we must be shutting down.
527        let _ = self.tx.send(op);
528        trigger_rx
529    }
530}
531
532#[cfg(test)]
533mod tests {
534    use std::collections::{BTreeMap, BTreeSet};
535
536    use bytes::Bytes;
537    use mz_compute_types::dataflows::{DataflowDescription, IndexDesc, IndexImport};
538    use mz_durable_cache::DurableCacheCodec;
539    use mz_dyncfg::ConfigSet;
540    use mz_expr::{MirRelationExpr, OptimizedMirRelationExpr};
541    use mz_persist_client::PersistClient;
542    use mz_persist_types::ShardId;
543    use mz_repr::{Datum, GlobalId, ReprRelationType, ReprScalarType};
544    use semver::Version;
545
546    use super::*;
547
548    #[mz_ore::test(tokio::test)]
549    #[cfg_attr(miri, ignore)] // unsupported operation: returning ready events from epoll_wait is not yet implemented
550    async fn expression_cache() {
551        let first_build_version = Version::new(0, 1, 0);
552        let second_build_version = Version::new(0, 2, 0);
553        let persist = PersistClient::new_for_tests().await;
554        let shard_id = ShardId::new();
555
556        let mut current_items = BTreeMap::new();
557        let mut remove_prior_versions = false;
558        // Compacting the shard takes too long, so we leave it to integration tests.
559        let compact_shard = false;
560        let dyncfgs = &mz_persist_client::cfg::all_dyncfgs(ConfigSet::default());
561        let spawn = |build_version: &Version,
562                     current_items: &BTreeMap<GlobalId, RelationVersion>,
563                     remove_prior_versions: bool| {
564            ExpressionCacheHandle::spawn_expression_cache(ExpressionCacheConfig {
565                build_version: build_version.clone(),
566                persist: persist.clone(),
567                shard_id,
568                current_items: current_items.clone(),
569                remove_prior_versions,
570                compact_shard,
571                dyncfgs: dyncfgs.clone(),
572            })
573        };
574
575        let mut next_id = 0;
576
577        let (mut local_exps, mut global_exps) = {
578            // Open a new empty cache.
579            let (cache, local_exprs, global_exprs) =
580                spawn(&first_build_version, &current_items, remove_prior_versions).await;
581            assert_eq!(local_exprs, BTreeMap::new(), "new cache should be empty");
582            assert_eq!(global_exprs, BTreeMap::new(), "new cache should be empty");
583
584            // Insert some expressions into the cache.
585            let mut local_exps = BTreeMap::new();
586            let mut global_exps = BTreeMap::new();
587            for _ in 0..4 {
588                let id = GlobalId::User(next_id);
589                let local_exp = gen_local_expressions();
590                let global_exp = gen_global_expressions();
591
592                cache
593                    .update(
594                        vec![(id, local_exp.clone())],
595                        vec![(id, global_exp.clone())],
596                        BTreeSet::new(),
597                    )
598                    .await;
599
600                current_items.insert(id, RelationVersion::root());
601                current_items.extend(
602                    global_exp
603                        .index_imports()
604                        .map(|id| (*id, RelationVersion::root())),
605                );
606                local_exps.insert(id, local_exp);
607                global_exps.insert(id, global_exp);
608
609                next_id += 1;
610            }
611            (local_exps, global_exps)
612        };
613
614        {
615            // Re-open the cache.
616            let (_cache, local_entries, global_entries) =
617                spawn(&first_build_version, &current_items, remove_prior_versions).await;
618            assert_eq!(
619                local_entries, local_exps,
620                "local expression with non-matching optimizer features should be removed during reconciliation"
621            );
622            assert_eq!(
623                global_entries, global_exps,
624                "global expression with non-matching optimizer features should be removed during reconciliation"
625            );
626        }
627
628        {
629            // Simulate dropping an object.
630            let id_to_remove = local_exps.keys().next().expect("not empty").clone();
631            current_items.remove(&id_to_remove);
632            let _removed_local_exp = local_exps.remove(&id_to_remove);
633            let _removed_global_exp = global_exps.remove(&id_to_remove);
634
635            // Re-open the cache.
636            let (_cache, local_entries, global_entries) =
637                spawn(&first_build_version, &current_items, remove_prior_versions).await;
638            assert_eq!(
639                local_entries, local_exps,
640                "dropped local objects should be removed during reconciliation"
641            );
642            assert_eq!(
643                global_entries, global_exps,
644                "dropped global objects should be removed during reconciliation"
645            );
646        }
647
648        {
649            // Simulate applying a replacement: the item keeps its id at a bumped version.
650            let id_to_bump = global_exps.keys().next_back().expect("not empty").clone();
651            current_items.insert(id_to_bump, RelationVersion::root().bump());
652            let _removed_local_exp = local_exps.remove(&id_to_bump);
653            let _removed_global_exp = global_exps.remove(&id_to_bump);
654
655            // Re-open the cache.
656            let (_cache, local_entries, global_entries) =
657                spawn(&first_build_version, &current_items, remove_prior_versions).await;
658            assert_eq!(
659                local_entries, local_exps,
660                "local expressions of an earlier item version should be removed during reconciliation"
661            );
662            assert_eq!(
663                global_entries, global_exps,
664                "global expressions of an earlier item version should be removed during reconciliation"
665            );
666
667            // The removal is durable: restoring the item version does not bring the entries back.
668            current_items.insert(id_to_bump, RelationVersion::root());
669            let (_cache, local_entries, global_entries) =
670                spawn(&first_build_version, &current_items, remove_prior_versions).await;
671            assert_eq!(
672                local_entries, local_exps,
673                "local expressions of an earlier item version should be durably removed"
674            );
675            assert_eq!(
676                global_entries, global_exps,
677                "global expressions of an earlier item version should be durably removed"
678            );
679        }
680
681        {
682            // Simulate dropping an object dependency.
683            let global_exp_to_remove = global_exps.keys().next().expect("not empty").clone();
684            let removed_global_exp = global_exps
685                .remove(&global_exp_to_remove)
686                .expect("known to exist");
687            let dependency_to_remove = removed_global_exp
688                .index_imports()
689                .next()
690                .expect("generator always makes non-empty index imports");
691            current_items.remove(dependency_to_remove);
692
693            // If the dependency is also tracked in the cache remove it.
694            let _removed_local_exp = local_exps.remove(dependency_to_remove);
695            let _removed_global_exp = global_exps.remove(dependency_to_remove);
696            // Remove any other exps that depend on dependency.
697            global_exps.retain(|_, exp| {
698                let index_imports: BTreeSet<_> = exp.index_imports().collect();
699                !index_imports.contains(&dependency_to_remove)
700            });
701
702            // Re-open the cache.
703            let (_cache, local_entries, global_entries) =
704                spawn(&first_build_version, &current_items, remove_prior_versions).await;
705            assert_eq!(
706                local_entries, local_exps,
707                "dropped object dependencies should NOT remove local expressions"
708            );
709            assert_eq!(
710                global_entries, global_exps,
711                "dropped object dependencies should remove global expressions"
712            );
713        }
714
715        let (new_gen_local_exps, new_gen_global_exps) = {
716            // Open the cache at a new build version.
717            let (cache, local_entries, global_entries) =
718                spawn(&second_build_version, &current_items, remove_prior_versions).await;
719            assert_eq!(
720                local_entries,
721                BTreeMap::new(),
722                "new build version should be empty"
723            );
724            assert_eq!(
725                global_entries,
726                BTreeMap::new(),
727                "new build version should be empty"
728            );
729
730            // Insert some expressions at the new build version.
731            let mut local_exps = BTreeMap::new();
732            let mut global_exps = BTreeMap::new();
733            for _ in 0..2 {
734                let id = GlobalId::User(next_id);
735                let local_exp = gen_local_expressions();
736                let global_exp = gen_global_expressions();
737
738                cache
739                    .update(
740                        vec![(id, local_exp.clone())],
741                        vec![(id, global_exp.clone())],
742                        BTreeSet::new(),
743                    )
744                    .await;
745
746                current_items.insert(id, RelationVersion::root());
747                current_items.extend(
748                    global_exp
749                        .index_imports()
750                        .map(|id| (*id, RelationVersion::root())),
751                );
752                local_exps.insert(id, local_exp);
753                global_exps.insert(id, global_exp);
754
755                next_id += 1;
756            }
757            (local_exps, global_exps)
758        };
759
760        {
761            // Re-open the cache at the first build version.
762            let (_cache, local_entries, global_entries) =
763                spawn(&first_build_version, &current_items, remove_prior_versions).await;
764            assert_eq!(
765                local_entries, local_exps,
766                "Previous build version local expressions should still exist"
767            );
768            assert_eq!(
769                global_entries, global_exps,
770                "Previous build version global expressions should still exist"
771            );
772        }
773
774        {
775            // Open the cache at a new build version and clear previous build versions.
776            remove_prior_versions = true;
777            let (_cache, local_entries, global_entries) =
778                spawn(&second_build_version, &current_items, remove_prior_versions).await;
779            assert_eq!(
780                local_entries, new_gen_local_exps,
781                "new build version local expressions should be persisted"
782            );
783            assert_eq!(
784                global_entries, new_gen_global_exps,
785                "new build version global expressions should be persisted"
786            );
787        }
788
789        {
790            // Re-open the cache at the first build version.
791            let (_cache, local_entries, global_entries) =
792                spawn(&first_build_version, &current_items, remove_prior_versions).await;
793            assert_eq!(
794                local_entries,
795                BTreeMap::new(),
796                "Previous build version local expressions should be cleared"
797            );
798            assert_eq!(
799                global_entries,
800                BTreeMap::new(),
801                "Previous build version global expressions should be cleared"
802            );
803        }
804    }
805
806    #[mz_ore::test]
807    fn local_expr_cache_roundtrip() {
808        let key = CacheKey {
809            id: GlobalId::User(1),
810            build_version: "1.2.3".into(),
811            expr_type: ExpressionType::Local,
812        };
813        let val = gen_local_expressions();
814
815        let bincode_val = Bytes::from(bincode::serialize(&val).expect("must serialize"));
816        let (encoded_key, encoded_val) = ExpressionCodec::encode(&key, &bincode_val);
817        let (decoded_key, decoded_val) = ExpressionCodec::decode(&encoded_key, &encoded_val);
818        let decoded_val: LocalExpressions =
819            bincode::deserialize(&decoded_val).expect("local expressions should roundtrip");
820
821        assert_eq!(key, decoded_key);
822        assert_eq!(val, decoded_val);
823    }
824
825    #[mz_ore::test]
826    fn global_expr_cache_roundtrip() {
827        let key = CacheKey {
828            id: GlobalId::User(1),
829            build_version: "1.2.3".into(),
830            expr_type: ExpressionType::Global,
831        };
832        let val = gen_global_expressions();
833
834        let bincode_val = Bytes::from(bincode::serialize(&val).expect("must serialize"));
835        let (encoded_key, encoded_val) = ExpressionCodec::encode(&key, &bincode_val);
836        let (decoded_key, decoded_val) = ExpressionCodec::decode(&encoded_key, &encoded_val);
837        let decoded_val: GlobalExpressions =
838            bincode::deserialize(&decoded_val).expect("global expressions should roundtrip");
839
840        assert_eq!(key, decoded_key);
841        assert_eq!(val, decoded_val);
842    }
843
844    /// Generate a random [`LocalExpressions`] value.
845    ///
846    /// The returned values are mostly hardcoded and only differ in a single randomized number.
847    /// That's sufficient for the expr cache tests, since the cache mostly treats expressions as
848    /// opaque objects.
849    fn gen_local_expressions() -> LocalExpressions {
850        let datum = Datum::UInt64(rand::random());
851
852        LocalExpressions {
853            local_mir: OptimizedMirRelationExpr(MirRelationExpr::constant(
854                vec![vec![datum]],
855                ReprRelationType::new(vec![ReprScalarType::UInt64.nullable(false)]),
856            )),
857            optimizer_features: Default::default(),
858            item_version: RelationVersion::root(),
859        }
860    }
861
862    /// Generate a random [`GlobalExpressions`] value.
863    ///
864    /// The returned values are mostly hardcoded and only differ in a single randomized string.
865    /// That's sufficient for the expr cache tests, since the cache mostly treats expressions as
866    /// opaque objects.
867    fn gen_global_expressions() -> GlobalExpressions {
868        let name = format!("test-{}", rand::random::<u64>());
869
870        let mut global_mir = DataflowDescription::new(name.clone());
871        let mut physical_plan = DataflowDescription::new(name);
872
873        // Add pieces expected by tests.
874        let index_imports = BTreeMap::from_iter([(
875            GlobalId::User(2),
876            IndexImport {
877                desc: IndexDesc {
878                    on_id: GlobalId::User(1),
879                    key: Default::default(),
880                },
881                typ: ReprRelationType::empty(),
882                monotonic: false,
883                with_snapshot: true,
884            },
885        )]);
886        global_mir.index_imports = index_imports.clone();
887        physical_plan.index_imports = index_imports;
888
889        GlobalExpressions {
890            global_mir,
891            physical_plan,
892            dataflow_metainfos: Default::default(),
893            optimizer_features: Default::default(),
894            item_version: RelationVersion::root(),
895        }
896    }
897}