mz_compute_types/plan/pinned.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//! The durable form of an LIR dataflow.
11//!
12//! A [`PinnedDataflow`] holds the durable plan for a dataflow. It is the
13//! root of the stable LIR schema traced in `tests/lir_schema.rs`, so every
14//! type reachable from it is part of the frozen serialization surface.
15//!
16//! We try to avoid pinning things that can be re-derived from the catalog
17//! when loading the plan. This means keeping track of assumptions about
18//! inputs but only the wiring for outputs. The rest can be recomputed.
19//!
20//! Loading a pinned plan is called _instantiation_, and it is done by means
21//! of an [`InstantiationContext`].
22//!
23//! Only dataflows behind catalog items can be pinned: indexes, materialized
24//! views, and metric sinks. Subscribes and one-shot copies have no durable
25//! identity to pin under, and the [`TryFrom`] conversion rejects them, along
26//! with any export under a transient id (a peek's index, a curated metric
27//! sink) and a dataflow that exports nothing.
28//!
29//! [`TryFrom`] also rejects a dataflow that imports or exports a builtin
30//! collection. Builtins keep their ids across releases while their schemas
31//! and definitions change, and the plan inlines the bodies of builtin views,
32//! which instantiation cannot check.
33//!
34//! Index keys are stored as [`LirScalarExpr`] and converted back to
35//! `MirScalarExpr` on instantiation. LIR scalars are a subset of MIR scalars,
36//! so that direction is total, and `DataflowDescription` keeps its MIR-typed
37//! index keys for the optimizer's benefit. Source operators are already an
38//! [`MfpPlan`] over LIR expressions in a lowered description and are stored
39//! as they are.
40//!
41//! The stored form is JSON, whose map keys must be strings. Maps keyed by
42//! `GlobalId` serialize their keys through [`GlobalIdKey`], the id's string
43//! form.
44
45use std::collections::BTreeMap;
46use std::fmt;
47
48use itertools::Itertools;
49use mz_expr::{Id, MfpPlan, MirScalarExpr};
50use mz_ore::soft_assert_or_log;
51use mz_repr::refresh_schedule::RefreshSchedule;
52use mz_repr::{GlobalId, RelationDesc, ReprRelationType, SqlRelationType};
53use serde::de::{self, Visitor};
54use serde::{Deserialize, Deserializer, Serialize, Serializer};
55use timely::progress::Antichain;
56
57use crate::dataflows::{
58 BuildDesc, DataflowDescription, IndexDesc, IndexImport, LirDataflowDescription, SourceImport,
59};
60use crate::plan::scalar::LirScalarExpr;
61use crate::plan::{LirRelationExpr, LirRelationNode};
62use crate::sinks::{
63 ComputeSinkConnection, ComputeSinkDesc, MaterializedViewSinkConnection, MetricSinkConnection,
64};
65use crate::sources::{SourceInstanceArguments, SourceInstanceDesc};
66
67/// A pinned LIR plan: the optimizer's decisions for one dataflow.
68#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
69pub struct PinnedDataflow {
70 /// Sources the plan reads, with the operators pushed down onto each.
71 #[serde(with = "global_id_keys")]
72 pub source_imports: BTreeMap<GlobalId, PinnedSourceImport>,
73 /// Indexes the plan reads, keyed by index id.
74 #[serde(with = "global_id_keys")]
75 pub index_imports: BTreeMap<GlobalId, PinnedIndexImport>,
76 /// Objects to build, in dependency order.
77 pub objects_to_build: Vec<BuildDesc<LirRelationExpr>>,
78 /// Indexes the plan exports, mapping each index id to the id of the
79 /// object it arranges.
80 #[serde(with = "global_id_keys")]
81 pub index_exports: BTreeMap<GlobalId, GlobalId>,
82 /// Sinks the plan exports, keyed by sink id.
83 #[serde(with = "global_id_keys")]
84 pub sink_exports: BTreeMap<GlobalId, PinnedSink>,
85}
86
87/// A `GlobalId` in its string form (`u1`, `s2`, `t3`), for use as a map key.
88///
89/// `GlobalId` itself serializes as an enum, which JSON cannot use as a key.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
91pub struct GlobalIdKey(pub GlobalId);
92
93impl Serialize for GlobalIdKey {
94 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
95 serializer.serialize_newtype_struct("GlobalIdKey", &self.0.to_string())
96 }
97}
98
99impl<'de> Deserialize<'de> for GlobalIdKey {
100 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
101 struct KeyVisitor;
102
103 impl<'de> Visitor<'de> for KeyVisitor {
104 type Value = GlobalIdKey;
105
106 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
107 f.write_str("a global id such as u1")
108 }
109
110 fn visit_newtype_struct<D: Deserializer<'de>>(
111 self,
112 deserializer: D,
113 ) -> Result<Self::Value, D::Error> {
114 let id = String::deserialize(deserializer)?;
115 id.parse().map(GlobalIdKey).map_err(de::Error::custom)
116 }
117
118 fn visit_str<E: de::Error>(self, id: &str) -> Result<Self::Value, E> {
119 id.parse().map(GlobalIdKey).map_err(de::Error::custom)
120 }
121 }
122
123 deserializer.deserialize_newtype_struct("GlobalIdKey", KeyVisitor)
124 }
125}
126
127/// Serializes a `GlobalId`-keyed map with [`GlobalIdKey`] keys.
128mod global_id_keys {
129 use super::*;
130
131 pub fn serialize<V: Serialize, S: Serializer>(
132 map: &BTreeMap<GlobalId, V>,
133 serializer: S,
134 ) -> Result<S::Ok, S::Error> {
135 serializer.collect_map(map.iter().map(|(id, value)| (GlobalIdKey(*id), value)))
136 }
137
138 pub fn deserialize<'de, V: Deserialize<'de>, D: Deserializer<'de>>(
139 deserializer: D,
140 ) -> Result<BTreeMap<GlobalId, V>, D::Error> {
141 let map = BTreeMap::<GlobalIdKey, V>::deserialize(deserializer)?;
142 Ok(map.into_iter().map(|(key, value)| (key.0, value)).collect())
143 }
144}
145
146/// The plan's assumptions about an imported source.
147#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
148pub struct PinnedSourceImport {
149 /// Operators pushed down onto the source read, with any temporal filter
150 /// held as the plan's bounds.
151 pub operators: Option<MfpPlan<LirScalarExpr>>,
152 /// The relation type the plan was compiled against.
153 pub typ: SqlRelationType,
154 /// Whether the plan relies on the source being monotonic.
155 pub monotonic: bool,
156 /// Whether the plan needs the source's snapshot.
157 pub with_snapshot: bool,
158}
159
160/// The plan's assumptions about an imported index.
161#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
162pub struct PinnedIndexImport {
163 /// The object the index arranges and the key it arranges by.
164 pub desc: IndexDesc<LirScalarExpr>,
165 /// The relation type of the arranged object.
166 pub typ: ReprRelationType,
167 /// Whether the plan relies on the index being monotonic.
168 pub monotonic: bool,
169 /// Whether the plan needs the index's snapshot.
170 pub with_snapshot: bool,
171}
172
173/// The wiring of an exported sink. Its descriptor is rebuilt from the catalog.
174#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
175pub struct PinnedSink {
176 /// The built object the sink reads.
177 pub from: GlobalId,
178 /// The kind of catalog item the sink was compiled for.
179 pub kind: PinnedSinkKind,
180 /// Which form of `from` the sink consumes, as lowering chose it. See
181 /// `ComputeSinkDesc::from_arrangement`.
182 pub from_arrangement: Option<(Vec<LirScalarExpr>, MfpPlan<LirScalarExpr>)>,
183}
184
185/// The kinds of sink a pinned dataflow may export.
186#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
187pub enum PinnedSinkKind {
188 /// A materialized view. Its column names, non-null assertions, and
189 /// refresh schedule come from the catalog item.
190 MaterializedView {
191 /// The sink's relation type as the optimizer built it: the plan's
192 /// inferred nullability and keys, with the non-null assertions
193 /// applied.
194 typ: SqlRelationType,
195 },
196 /// A metric sink. Its label and the shaped description of its input come
197 /// from the catalog item.
198 MetricSink,
199}
200
201///////////////////////////////////////////////////////////////////////////////
202// PINNING
203///////////////////////////////////////////////////////////////////////////////
204
205/// Why a `DataflowDescription` could not be pinned.
206///
207/// Lowering scalar expressions to LIR is not an error case: expression
208/// preparation rejects unmaterializable functions before lowering, and
209/// temporal extraction moves `mz_now()` into `MfpPlan` bounds, so a failure
210/// there is a lowering bug and panics like the other LIR conversions.
211#[derive(Clone, Debug, PartialEq, Eq)]
212pub enum PinError {
213 /// The dataflow exports a sink kind that has no catalog identity.
214 UnpinnableSink(GlobalId),
215 /// The dataflow exports an index under a transient id, as a peek does.
216 TransientIndexExport(GlobalId),
217 /// The dataflow exports a sink under a transient id, as a curated
218 /// per-replica metric sink does.
219 TransientSinkExport(GlobalId),
220 /// The dataflow exports nothing, so no catalog item anchors it.
221 NoExports,
222 /// The dataflow imports or exports the builtin collection under this id.
223 BuiltinCollection(GlobalId),
224}
225
226impl fmt::Display for PinError {
227 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
228 match self {
229 PinError::UnpinnableSink(id) => {
230 write!(f, "sink {id} is a one-shot sink and cannot be pinned")
231 }
232 PinError::TransientIndexExport(id) => {
233 write!(
234 f,
235 "index {id} is a peek's transient result and cannot be pinned"
236 )
237 }
238 PinError::TransientSinkExport(id) => {
239 write!(
240 f,
241 "sink {id} has a transient id and no catalog item, so it cannot be pinned"
242 )
243 }
244 PinError::NoExports => write!(f, "a dataflow with no exports cannot be pinned"),
245 PinError::BuiltinCollection(id) => {
246 write!(
247 f,
248 "a dataflow that reads or writes builtin collection {id} cannot be pinned"
249 )
250 }
251 }
252 }
253}
254
255impl std::error::Error for PinError {}
256
257impl TryFrom<LirDataflowDescription> for PinnedDataflow {
258 type Error = PinError;
259
260 fn try_from(desc: LirDataflowDescription) -> Result<Self, PinError> {
261 // Destructured without `..` on purpose, so a new field on any of
262 // these types fails to compile here until it is either pinned or
263 // listed among the supplied fields.
264 let DataflowDescription {
265 source_imports,
266 index_imports,
267 objects_to_build,
268 index_exports,
269 sink_exports,
270 as_of: _,
271 until: _,
272 initial_storage_as_of: _,
273 refresh_schedule: _,
274 debug_name: _,
275 time_dependence: _,
276 } = desc;
277
278 // Every dataflow we run should have exports; missing exports indicate
279 // a serious error somewhere.
280 soft_assert_or_log!(
281 !(index_exports.is_empty() && sink_exports.is_empty()),
282 "pinning a dataflow with no exports"
283 );
284 if index_exports.is_empty() && sink_exports.is_empty() {
285 return Err(PinError::NoExports);
286 }
287 if let Some(id) = source_imports
288 .keys()
289 .chain(index_imports.keys())
290 .chain(index_exports.keys())
291 .chain(sink_exports.keys())
292 .find(|id| id.is_system())
293 {
294 return Err(PinError::BuiltinCollection(*id));
295 }
296
297 let source_imports = source_imports
298 .into_iter()
299 .map(|(id, import)| {
300 let SourceImport {
301 desc:
302 SourceInstanceDesc {
303 arguments: SourceInstanceArguments { operators },
304 storage_metadata: (),
305 typ,
306 },
307 monotonic,
308 with_snapshot,
309 upper: _,
310 } = import;
311 (
312 id,
313 PinnedSourceImport {
314 operators,
315 typ,
316 monotonic,
317 with_snapshot,
318 },
319 )
320 })
321 .collect();
322
323 let index_imports = index_imports
324 .into_iter()
325 .map(|(id, import)| {
326 let IndexImport {
327 desc,
328 typ,
329 monotonic,
330 with_snapshot,
331 } = import;
332 (
333 id,
334 PinnedIndexImport {
335 desc: desc.as_lir(),
336 typ,
337 monotonic,
338 with_snapshot,
339 },
340 )
341 })
342 .collect();
343
344 let index_exports = index_exports
345 .into_iter()
346 .map(|(id, (IndexDesc { on_id, key: _ }, _typ))| {
347 if id.is_transient() {
348 return Err(PinError::TransientIndexExport(id));
349 }
350 Ok((id, on_id))
351 })
352 .collect::<Result<_, _>>()?;
353
354 let sink_exports = sink_exports
355 .into_iter()
356 .map(|(id, sink)| {
357 let ComputeSinkDesc {
358 from,
359 from_desc,
360 connection,
361 with_snapshot: _,
362 up_to: _,
363 non_null_assertions: _,
364 refresh_schedule: _,
365 from_arrangement,
366 } = sink;
367 let kind = match connection {
368 ComputeSinkConnection::MaterializedView(_) => {
369 PinnedSinkKind::MaterializedView {
370 typ: from_desc.into_typ(),
371 }
372 }
373 ComputeSinkConnection::MetricSink(_) => PinnedSinkKind::MetricSink,
374 ComputeSinkConnection::Subscribe(_)
375 | ComputeSinkConnection::CopyToS3Oneshot(_) => {
376 return Err(PinError::UnpinnableSink(id));
377 }
378 };
379 if id.is_transient() {
380 return Err(PinError::TransientSinkExport(id));
381 }
382 Ok((
383 id,
384 PinnedSink {
385 from,
386 kind,
387 from_arrangement,
388 },
389 ))
390 })
391 .collect::<Result<_, _>>()?;
392
393 Ok(PinnedDataflow {
394 source_imports,
395 index_imports,
396 objects_to_build,
397 index_exports,
398 sink_exports,
399 })
400 }
401}
402
403///////////////////////////////////////////////////////////////////////////////
404// INSTANTIATION
405///////////////////////////////////////////////////////////////////////////////
406
407/// What [`PinnedDataflow::instantiate`] needs from its environment: catalog
408/// facts about the plan's imports and exports, looked up by id, and fresh
409/// transient ids. Each lookup returns `None` when no item of that kind exists.
410pub trait InstantiationContext {
411 /// The relation type of the storage collection `id`, as the optimizer
412 /// imports it.
413 fn collection_type(&self, id: GlobalId) -> Option<SqlRelationType>;
414 /// The index under `id`.
415 fn index(&self, id: GlobalId) -> Option<IndexInfo>;
416 /// The materialized view whose sink is exported under `id`.
417 fn materialized_view(&self, id: GlobalId) -> Option<MaterializedViewInfo>;
418 /// The metric sink exported under `id`.
419 fn metric_sink(&self, id: GlobalId) -> Option<MetricSinkInfo>;
420 /// A transient id no other dataflow in this process uses.
421 fn allocate_transient_id(&self) -> GlobalId;
422}
423
424/// The catalog facts an index import is checked against and an index export
425/// is rebuilt from.
426#[derive(Clone, Debug, PartialEq, Eq)]
427pub struct IndexInfo {
428 /// The object the index is on.
429 pub on: GlobalId,
430 /// The key the index arranges by.
431 pub keys: Vec<MirScalarExpr>,
432 /// The relation type of the object the index is on.
433 pub typ: ReprRelationType,
434}
435
436/// The catalog facts an exported materialized view sink is rebuilt from.
437#[derive(Clone, Debug, PartialEq, Eq)]
438pub struct MaterializedViewInfo {
439 /// The view's description. Instantiation takes its column names from it
440 /// and checks its type against the pinned type.
441 pub desc: RelationDesc,
442 /// Columns asserted non-null.
443 pub non_null_assertions: Vec<usize>,
444 /// The view's refresh schedule.
445 pub refresh_schedule: Option<RefreshSchedule>,
446}
447
448/// The catalog facts an exported metric sink is rebuilt from.
449#[derive(Clone, Debug, PartialEq, Eq)]
450pub struct MetricSinkInfo {
451 /// The `sink` label on the sink's health gauges.
452 pub label: String,
453 /// The description of the shaped object the sink reads. This is what the
454 /// metric sink optimizer derives from the source description and prefix,
455 /// not the source description itself.
456 pub from_desc: RelationDesc,
457}
458
459/// Why a [`PinnedDataflow`] could not be instantiated.
460#[derive(Clone, Debug, PartialEq, Eq)]
461pub enum InstantiateError {
462 /// The catalog no longer has an item of the expected kind under this id.
463 MissingCatalogItem {
464 /// The import's or export's id.
465 id: GlobalId,
466 /// The kind of item the plan was compiled for.
467 kind: &'static str,
468 },
469 /// The catalog item and the pinned plan disagree about the import.
470 ImportDrift {
471 /// The import's id.
472 id: GlobalId,
473 /// What disagrees.
474 detail: String,
475 },
476 /// The catalog item and the pinned plan disagree about the export.
477 ExportDrift {
478 /// The export's id.
479 id: GlobalId,
480 /// What disagrees.
481 detail: String,
482 },
483}
484
485impl fmt::Display for InstantiateError {
486 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
487 match self {
488 InstantiateError::MissingCatalogItem { id, kind } => {
489 write!(f, "no {kind} catalog item for {id}")
490 }
491 InstantiateError::ImportDrift { id, detail } => {
492 write!(f, "import {id} drifted from its pinned plan: {detail}")
493 }
494 InstantiateError::ExportDrift { id, detail } => {
495 write!(f, "export {id} drifted from its pinned plan: {detail}")
496 }
497 }
498 }
499}
500impl std::error::Error for InstantiateError {}
501
502impl PinnedDataflow {
503 /// Rebuilds the description the compute controller expects.
504 ///
505 /// The result has no `as_of`, `until`, `initial_storage_as_of`, or
506 /// `time_dependence`, and its source imports carry no upper frontier or
507 /// storage metadata. The caller supplies those the same way it does for a
508 /// freshly optimized dataflow. The dataflow-level refresh schedule is the
509 /// exported materialized view's, if there is one.
510 ///
511 /// The result equals the description that was pinned, up to transient
512 /// build ids, which are reallocated from `ctx`.
513 ///
514 /// Fails if a catalog item an import or export was compiled for is gone,
515 /// or if the catalog disagrees with the plan about what it is. Only the
516 /// first problem found is reported.
517 ///
518 /// NOTE: The plan also contains the bodies of the views it inlines, and
519 /// of SQL-defined builtin functions, and nothing here verifies those.
520 /// Pinning refuses builtin collections, which keeps out builtin views
521 /// that read one, but not a builtin view that reads no collection or a
522 /// SQL-defined function.
523 // TODO(mgree): the import and export checks are independent, so
524 // collecting every problem into a list instead of returning at the first
525 // one would be easy.
526 pub fn instantiate(
527 self,
528 ctx: &dyn InstantiationContext,
529 debug_name: String,
530 ) -> Result<LirDataflowDescription, InstantiateError> {
531 let PinnedDataflow {
532 source_imports,
533 index_imports,
534 mut objects_to_build,
535 index_exports,
536 sink_exports,
537 } = self;
538
539 // Transient ids come from a per-process counter, so the internal
540 // build ids of a plan pinned elsewhere can collide with a dataflow
541 // built in this process. Compute keys its LIR introspection by build
542 // id, and a collision merges two dataflows' operators there.
543 let mut renamed = BTreeMap::new();
544 for object in &mut objects_to_build {
545 if object.id.is_transient() {
546 let fresh = ctx.allocate_transient_id();
547 renamed.insert(object.id, fresh);
548 object.id = fresh;
549 }
550 }
551 let rename = |id: GlobalId| renamed.get(&id).copied().unwrap_or(id);
552 for object in &mut objects_to_build {
553 replace_global_ids(&mut object.plan, &rename);
554 }
555
556 // An import's id resolving is not evidence that the plan's assumptions
557 // about it hold: a materialized view's inferred nullability and keys
558 // can change between releases. So each import is checked against what
559 // the catalog says now.
560 let mut rebuilt_source_imports = BTreeMap::new();
561 for (id, import) in source_imports {
562 let PinnedSourceImport {
563 operators,
564 typ,
565 monotonic,
566 with_snapshot,
567 } = import;
568 let catalog_typ =
569 ctx.collection_type(id)
570 .ok_or(InstantiateError::MissingCatalogItem {
571 id,
572 kind: "collection",
573 })?;
574 if catalog_typ != typ {
575 return Err(InstantiateError::ImportDrift {
576 id,
577 detail: format!("pinned type {typ:?}, catalog says {catalog_typ:?}"),
578 });
579 }
580 let import = SourceImport {
581 desc: SourceInstanceDesc {
582 arguments: SourceInstanceArguments { operators },
583 storage_metadata: (),
584 typ,
585 },
586 monotonic,
587 with_snapshot,
588 upper: Antichain::new(),
589 };
590 rebuilt_source_imports.insert(id, import);
591 }
592
593 let mut rebuilt_index_imports = BTreeMap::new();
594 for (id, import) in index_imports {
595 let PinnedIndexImport {
596 desc: IndexDesc { on_id, key },
597 typ,
598 monotonic,
599 with_snapshot,
600 } = import;
601 let info = ctx
602 .index(id)
603 .ok_or(InstantiateError::MissingCatalogItem { id, kind: "index" })?;
604 let key: Vec<MirScalarExpr> = key.iter().map(MirScalarExpr::from).collect();
605 if info.on != on_id {
606 return Err(InstantiateError::ImportDrift {
607 id,
608 detail: format!("pinned on {on_id}, catalog says {}", info.on),
609 });
610 }
611 if info.keys != key {
612 return Err(InstantiateError::ImportDrift {
613 id,
614 detail: format!("pinned key {key:?}, catalog says {:?}", info.keys),
615 });
616 }
617 if info.typ != typ {
618 return Err(InstantiateError::ImportDrift {
619 id,
620 detail: format!("pinned type {typ:?}, catalog says {:?}", info.typ),
621 });
622 }
623 let import = IndexImport {
624 desc: IndexDesc { on_id, key },
625 typ,
626 monotonic,
627 with_snapshot,
628 };
629 rebuilt_index_imports.insert(id, import);
630 }
631
632 let is_built = |id: GlobalId| objects_to_build.iter().any(|object| object.id == id);
633
634 let mut rebuilt_index_exports = BTreeMap::new();
635 for (id, on) in index_exports {
636 let info = ctx
637 .index(id)
638 .ok_or(InstantiateError::MissingCatalogItem { id, kind: "index" })?;
639 if info.on != on {
640 return Err(InstantiateError::ExportDrift {
641 id,
642 detail: format!("pinned on {on}, catalog says {}", info.on),
643 });
644 }
645 let Some(build) = objects_to_build.iter().find(|object| object.id == id) else {
646 return Err(InstantiateError::ExportDrift {
647 id,
648 detail: "the plan builds no object for the index".to_string(),
649 });
650 };
651 // The index's build arranges by the catalog key, either with an
652 // explicit `ArrangeBy` or, when lowering found the key already
653 // available on the object it reads, with a bare `Get` whose
654 // `keys` lists it.
655 let arranged = match &build.plan.node {
656 LirRelationNode::ArrangeBy { forms, .. } => &forms.arranged,
657 LirRelationNode::Get { keys, .. } => &keys.arranged,
658 node => {
659 return Err(InstantiateError::ExportDrift {
660 id,
661 detail: format!(
662 "the index's build is neither an arrangement nor a read: {node:?}"
663 ),
664 });
665 }
666 };
667 let lir_key: Vec<LirScalarExpr> = info
668 .keys
669 .iter()
670 .map(|expr| {
671 LirScalarExpr::try_from(expr).expect("catalog index keys are materializable")
672 })
673 .collect();
674 if !arranged.iter().any(|(key, _, _)| *key == lir_key) {
675 return Err(InstantiateError::ExportDrift {
676 id,
677 detail: format!("catalog key {lir_key:?} is not among the plan's arrangements"),
678 });
679 }
680 rebuilt_index_exports.insert(
681 id,
682 (
683 IndexDesc {
684 on_id: on,
685 key: info.keys,
686 },
687 info.typ,
688 ),
689 );
690 }
691
692 let mut refresh_schedule = None;
693 let mut rebuilt_sink_exports = BTreeMap::new();
694 for (
695 id,
696 PinnedSink {
697 from,
698 kind,
699 from_arrangement,
700 },
701 ) in sink_exports
702 {
703 let from = rename(from);
704 if !is_built(from) {
705 return Err(InstantiateError::ExportDrift {
706 id,
707 detail: format!("the plan builds no object {from} for the sink to read"),
708 });
709 }
710 let desc = match kind {
711 PinnedSinkKind::MaterializedView { typ } => {
712 let info =
713 ctx.materialized_view(id)
714 .ok_or(InstantiateError::MissingCatalogItem {
715 id,
716 kind: "materialized view",
717 })?;
718
719 // TODO(mgree): once we're loading pinned plans, this check compares the
720 // pinned type against itself.
721 let catalog_typ = info.desc.typ();
722 if typ.column_types.len() != catalog_typ.column_types.len() {
723 return Err(InstantiateError::ExportDrift {
724 id,
725 detail: format!(
726 "pinned {} columns, catalog says {}",
727 typ.column_types.len(),
728 catalog_typ.column_types.len()
729 ),
730 });
731 }
732 for (column, (pinned, catalog)) in typ
733 .column_types
734 .iter()
735 .zip_eq(&catalog_typ.column_types)
736 .enumerate()
737 {
738 if pinned.scalar_type != catalog.scalar_type {
739 return Err(InstantiateError::ExportDrift {
740 id,
741 detail: format!(
742 "column {column}: pinned type {:?}, catalog says {:?}",
743 pinned.scalar_type, catalog.scalar_type
744 ),
745 });
746 }
747 }
748 // Arity and scalar types are checked above, so this
749 // compares nullability and keys.
750 if typ != *catalog_typ {
751 return Err(InstantiateError::ExportDrift {
752 id,
753 detail: format!("pinned type {typ:?}, catalog says {catalog_typ:?}"),
754 });
755 }
756 let rel_desc = RelationDesc::new(typ, info.desc.iter_names().cloned());
757 refresh_schedule.clone_from(&info.refresh_schedule);
758 ComputeSinkDesc {
759 from,
760 from_desc: rel_desc.clone(),
761 connection: ComputeSinkConnection::MaterializedView(
762 MaterializedViewSinkConnection {
763 value_desc: rel_desc,
764 storage_metadata: (),
765 },
766 ),
767 with_snapshot: true,
768 up_to: Antichain::new(),
769 non_null_assertions: info.non_null_assertions,
770 refresh_schedule: info.refresh_schedule,
771 from_arrangement,
772 }
773 }
774 PinnedSinkKind::MetricSink => {
775 let info = ctx
776 .metric_sink(id)
777 .ok_or(InstantiateError::MissingCatalogItem {
778 id,
779 kind: "metric sink",
780 })?;
781 ComputeSinkDesc {
782 from,
783 from_desc: info.from_desc,
784 connection: ComputeSinkConnection::MetricSink(MetricSinkConnection {
785 label: info.label,
786 }),
787 with_snapshot: true,
788 up_to: Antichain::new(),
789 non_null_assertions: Vec::new(),
790 refresh_schedule: None,
791 from_arrangement,
792 }
793 }
794 };
795 rebuilt_sink_exports.insert(id, desc);
796 }
797
798 Ok(DataflowDescription {
799 source_imports: rebuilt_source_imports,
800 index_imports: rebuilt_index_imports,
801 objects_to_build,
802 index_exports: rebuilt_index_exports,
803 sink_exports: rebuilt_sink_exports,
804 as_of: None,
805 until: Antichain::new(),
806 initial_storage_as_of: None,
807 refresh_schedule,
808 debug_name,
809 time_dependence: None,
810 })
811 }
812}
813
814fn replace_global_ids(expr: &mut LirRelationExpr, rename: &impl Fn(GlobalId) -> GlobalId) {
815 let mut todo = vec![expr];
816 while let Some(expr) = todo.pop() {
817 if let LirRelationNode::Get {
818 id: Id::Global(id), ..
819 } = &mut expr.node
820 {
821 *id = rename(*id);
822 }
823 todo.extend(expr.node.children_mut());
824 }
825}
826
827#[cfg(test)]
828#[path = "pinned_tests.rs"]
829mod tests;