1use std::sync::Arc;
22use std::time::{Duration, Instant};
23
24use mz_compute_types::plan::LirRelationExpr;
25use mz_compute_types::sinks::{ComputeSinkConnection, ComputeSinkDesc, MetricSinkConnection};
26use mz_expr::func::variadic::Coalesce;
27use mz_expr::{MirRelationExpr, MirScalarExpr, func};
28use mz_repr::explain::trace_plan;
29use mz_repr::{
30 ColumnName, Datum, GlobalId, RelationDesc, ReprRelationType, ReprScalarType, Row, SqlScalarType,
31};
32use mz_sql::optimizer_metrics::OptimizerMetrics;
33use mz_sql::plan::{HirRelationExpr, HirToMirConfig};
34use mz_transform::TransformCtx;
35use mz_transform::dataflow::DataflowMetainfo;
36use mz_transform::normalize_lets::normalize_lets;
37use mz_transform::typecheck::{SharedTypecheckingContext, empty_typechecking_context};
38use timely::progress::Antichain;
39
40use crate::optimize::dataflows::{
41 ComputeInstanceSnapshot, DataflowBuilder, ExprPrep, ExprPrepMaintained,
42};
43use crate::optimize::{
44 LirDataflowDescription, MirDataflowDescription, Optimize, OptimizerCatalog, OptimizerConfig,
45 OptimizerError, optimize_mir_local,
46};
47
48const METRIC_NAME_PATTERN: &str = "^[a-zA-Z_:][a-zA-Z0-9_:]*$";
53
54pub struct Optimizer {
62 typecheck_ctx: SharedTypecheckingContext,
64 catalog: Arc<dyn OptimizerCatalog>,
66 compute_instance: ComputeInstanceSnapshot,
68 view_id: GlobalId,
71 sink_id: GlobalId,
73 config: OptimizerConfig,
75 metrics: OptimizerMetrics,
77 duration: Duration,
79}
80
81impl Optimizer {
82 pub fn new(
83 catalog: Arc<dyn OptimizerCatalog>,
84 compute_instance: ComputeInstanceSnapshot,
85 view_id: GlobalId,
86 sink_id: GlobalId,
87 config: OptimizerConfig,
88 metrics: OptimizerMetrics,
89 ) -> Self {
90 Self {
91 typecheck_ctx: empty_typechecking_context(),
92 catalog,
93 compute_instance,
94 view_id,
95 sink_id,
96 config,
97 metrics,
98 duration: Default::default(),
99 }
100 }
101}
102
103pub struct MetricSink {
105 debug_name: String,
107 from: MetricSinkFrom,
109 prefix: String,
113 label: Option<String>,
116}
117
118impl MetricSink {
119 pub fn new(
121 debug_name: String,
122 from: MetricSinkFrom,
123 prefix: String,
124 label: Option<String>,
125 ) -> Self {
126 Self {
127 debug_name,
128 from,
129 prefix,
130 label,
131 }
132 }
133}
134
135pub enum MetricSinkFrom {
140 Id(GlobalId),
142 Query {
145 expr: HirRelationExpr,
146 desc: RelationDesc,
147 },
148}
149
150#[derive(Clone, Debug)]
153pub struct GlobalMirPlan {
154 df_desc: MirDataflowDescription,
155 df_meta: DataflowMetainfo,
156}
157
158impl GlobalMirPlan {
159 pub fn df_desc(&self) -> &MirDataflowDescription {
160 &self.df_desc
161 }
162}
163
164#[derive(Clone, Debug)]
167pub struct GlobalLirPlan {
168 df_desc: LirDataflowDescription,
169 df_meta: DataflowMetainfo,
170}
171
172impl GlobalLirPlan {
173 pub fn df_desc(&self) -> &LirDataflowDescription {
174 &self.df_desc
175 }
176}
177
178impl Optimize<MetricSink> for Optimizer {
179 type To = GlobalMirPlan;
180
181 fn optimize(&mut self, metric_sink: MetricSink) -> Result<Self::To, OptimizerError> {
182 let time = Instant::now();
183
184 let mut df_builder = {
185 let compute = self.compute_instance.clone();
186 DataflowBuilder::new(&*self.catalog, compute).with_config(&self.config)
187 };
188 let mut df_desc = MirDataflowDescription::new(metric_sink.debug_name);
189 let mut df_meta = DataflowMetainfo::default();
190
191 let (source_expr, source_desc) = match metric_sink.from {
192 MetricSinkFrom::Id(from) => {
193 let from_desc = self
194 .catalog
195 .get_entry(&from)
196 .relation_desc()
197 .expect("can only create a metric sink on items with a valid description")
198 .into_owned();
199 let repr_typ = ReprRelationType::from(from_desc.typ());
200 (MirRelationExpr::global_get(from, repr_typ), from_desc)
201 }
202 MetricSinkFrom::Query { expr, desc } => {
203 let expr = expr.lower(HirToMirConfig::from(&self.config), Some(&self.metrics))?;
206 (expr, desc)
207 }
208 };
209
210 let (shaped_expr, shaped_desc) =
214 shape_metric_sink_source(source_expr, &source_desc, &metric_sink.prefix);
215 let mut local_ctx = TransformCtx::local(
216 &self.config.features,
217 &self.typecheck_ctx,
218 &mut df_meta,
219 Some(&mut self.metrics),
220 Some(self.view_id),
221 );
222 let shaped_expr = optimize_mir_local(shaped_expr, &mut local_ctx)?;
223
224 df_builder.import_view_into_dataflow(
227 &self.view_id,
228 &shaped_expr,
229 &mut df_desc,
230 &self.config.features,
231 )?;
232 df_builder.maybe_reoptimize_imported_views(&mut df_desc, &self.config)?;
233
234 let sink_description = ComputeSinkDesc {
235 from: self.view_id,
236 from_desc: shaped_desc,
237 connection: ComputeSinkConnection::MetricSink(MetricSinkConnection {
238 label: metric_sink
239 .label
240 .unwrap_or_else(|| self.sink_id.to_string()),
241 }),
242 with_snapshot: true,
243 up_to: Antichain::new(),
244 non_null_assertions: Vec::new(),
245 refresh_schedule: None,
246 from_arrangement: None,
248 };
249 df_desc.export_sink(self.sink_id, sink_description);
250
251 let style = ExprPrepMaintained;
253 df_desc.visit_children(
254 |r| style.prep_relation_expr(r),
255 |s| style.prep_scalar_expr(s),
256 )?;
257
258 let mut transform_ctx = TransformCtx::global(
260 &df_builder,
261 &mz_transform::EmptyStatisticsOracle,
262 &self.config.features,
263 &self.typecheck_ctx,
264 &mut df_meta,
265 Some(&mut self.metrics),
266 );
267 mz_transform::optimize_dataflow(&mut df_desc, &mut transform_ctx, false)?;
269
270 self.duration += time.elapsed();
271
272 Ok(GlobalMirPlan { df_desc, df_meta })
273 }
274}
275
276impl Optimize<GlobalMirPlan> for Optimizer {
277 type To = GlobalLirPlan;
278
279 fn optimize(&mut self, plan: GlobalMirPlan) -> Result<Self::To, OptimizerError> {
280 let time = Instant::now();
281
282 let GlobalMirPlan {
283 mut df_desc,
284 df_meta,
285 } = plan;
286
287 for build in df_desc.objects_to_build.iter_mut() {
289 normalize_lets(&mut build.plan.0, &self.config.features)?
290 }
291
292 let df_desc = LirRelationExpr::finalize_dataflow(
294 df_desc,
295 &self.config.features,
296 Some(self.metrics.lowering()),
297 )?;
298
299 trace_plan(&df_desc);
301
302 self.duration += time.elapsed();
303 self.metrics
304 .observe_e2e_optimization_time("metric_sink", self.duration);
305
306 Ok(GlobalLirPlan { df_desc, df_meta })
307 }
308}
309
310impl GlobalLirPlan {
311 pub fn unapply(self) -> (LirDataflowDescription, DataflowMetainfo) {
313 (self.df_desc, self.df_meta)
314 }
315}
316
317fn shape_metric_sink_source(
339 source: MirRelationExpr,
340 source_desc: &RelationDesc,
341 prefix: &str,
342) -> (MirRelationExpr, RelationDesc) {
343 let get_idx = |name: &str| {
348 source_desc
349 .get_by_name(&ColumnName::from(name))
350 .expect("metric-sink source relation must expose the canonical columns")
351 };
352 let (metric_name_idx, metric_name_ct) = get_idx("metric_name");
353 let (metric_type_idx, metric_type_ct) = get_idx("metric_type");
354 let (labels_idx, labels_ct) = get_idx("labels");
355 let (value_idx, value_ct) = get_idx("value");
356 let (help_idx, help_ct) = get_idx("help");
357
358 let arity = source_desc.typ().columns().len();
359 mz_ore::soft_assert_eq_or_log!(source.arity(), arity);
364 let labels_repr_type = ReprScalarType::from(&labels_ct.scalar_type);
365
366 let empty_map_row = {
367 let mut row = Row::default();
368 row.packer().push_dict_with(|_| {});
369 row
370 };
371 let labels_coalesced = MirScalarExpr::call_variadic(
372 Coalesce,
373 vec![
374 MirScalarExpr::column(labels_idx),
375 MirScalarExpr::literal_from_single_element_row(empty_map_row, labels_repr_type),
376 ],
377 );
378 let help_coalesced = MirScalarExpr::call_variadic(
379 Coalesce,
380 vec![
381 MirScalarExpr::column(help_idx),
382 MirScalarExpr::literal_ok(Datum::String(""), ReprScalarType::String),
383 ],
384 );
385
386 let metric_type_literal = |s: &'static str| {
387 MirScalarExpr::column(metric_type_idx).call_binary(
388 MirScalarExpr::literal_ok(Datum::String(s), ReprScalarType::String),
389 func::Eq,
390 )
391 };
392 let metric_kind = metric_type_literal("gauge").if_then_else(
393 MirScalarExpr::literal_ok(Datum::Int32(0), ReprScalarType::Int32),
394 metric_type_literal("counter").if_then_else(
395 MirScalarExpr::literal_ok(Datum::Int32(1), ReprScalarType::Int32),
396 MirScalarExpr::literal_null(ReprScalarType::Int32),
397 ),
398 );
399
400 let prefixed_name = MirScalarExpr::literal_ok(Datum::String(prefix), ReprScalarType::String)
407 .call_binary(
408 MirScalarExpr::column(metric_name_idx),
409 func::TextConcatBinary,
410 );
411
412 let published_name = MirScalarExpr::column(arity + 2);
418 let name_valid = published_name
419 .clone()
420 .call_is_null()
421 .not()
422 .and(published_name.call_binary(
423 MirScalarExpr::literal_ok(Datum::String(METRIC_NAME_PATTERN), ReprScalarType::String),
424 func::IsRegexpMatchCaseSensitive,
425 ));
426
427 let shaped_expr = source
428 .map(vec![
429 labels_coalesced,
430 help_coalesced,
431 prefixed_name,
432 metric_kind,
433 name_valid,
434 ])
435 .project(vec![
436 arity + 2, metric_type_idx,
438 arity, value_idx,
440 arity + 1, arity + 3, arity + 4, ]);
444
445 let mut labels_shaped_ct = labels_ct.clone();
446 labels_shaped_ct.nullable = false;
447 let mut help_shaped_ct = help_ct.clone();
448 help_shaped_ct.nullable = false;
449 let shaped_desc = RelationDesc::from_names_and_types([
450 ("metric_name", metric_name_ct.clone()),
451 ("metric_type", metric_type_ct.clone()),
452 ("labels", labels_shaped_ct),
453 ("value", value_ct.clone()),
454 ("help", help_shaped_ct),
455 ("metric_kind", SqlScalarType::Int32.nullable(true)),
456 ("name_valid", SqlScalarType::Bool.nullable(true)),
457 ]);
458
459 (shaped_expr, shaped_desc)
460}
461
462#[cfg(test)]
463mod tests {
464 use std::collections::BTreeMap;
465
466 use mz_catalog::memory::objects::{CatalogEntry, CatalogItem, Table, TableDataSource};
467 use mz_controller_types::ClusterId;
468 use mz_expr::Eval;
469 use mz_ore::metrics::MetricsRegistry;
470 use mz_repr::adt::mz_acl_item::PrivilegeMap;
471 use mz_repr::role_id::RoleId;
472 use mz_repr::{
473 CatalogItemId, RelationVersion, RelationVersionSelector, RowArena, SqlColumnType,
474 VersionedRelationDesc,
475 };
476 use mz_sql::names::{
477 FullItemName, ItemQualifiers, QualifiedItemName, RawDatabaseSpecifier,
478 ResolvedDatabaseSpecifier, ResolvedIds, SchemaId, SchemaSpecifier,
479 };
480 use mz_sql::session::vars::SystemVars;
481
482 use super::*;
483
484 fn source_desc() -> RelationDesc {
488 RelationDesc::builder()
489 .with_column("metric_name", SqlScalarType::String.nullable(true))
490 .with_column("metric_type", SqlScalarType::String.nullable(false))
491 .with_column(
492 "labels",
493 SqlScalarType::Map {
494 value_type: Box::new(SqlScalarType::String),
495 custom_id: None,
496 }
497 .nullable(true),
498 )
499 .with_column("value", SqlScalarType::Float64.nullable(true))
500 .with_column("help", SqlScalarType::String.nullable(true))
501 .with_column("extra", SqlScalarType::String.nullable(true))
502 .finish()
503 }
504
505 fn source_get(desc: &RelationDesc) -> MirRelationExpr {
507 MirRelationExpr::global_get(TABLE_GID, ReprRelationType::from(desc.typ()))
508 }
509
510 #[mz_ore::test]
511 fn shaped_desc_column_contract() {
512 let (_expr, desc) =
513 shape_metric_sink_source(source_get(&source_desc()), &source_desc(), "app_");
514
515 let cols: Vec<(String, SqlColumnType)> = desc
516 .iter()
517 .map(|(name, ty)| (name.as_str().to_string(), ty.clone()))
518 .collect();
519
520 let names: Vec<&str> = cols.iter().map(|(n, _)| n.as_str()).collect();
523 assert_eq!(
524 names,
525 vec![
526 "metric_name",
527 "metric_type",
528 "labels",
529 "value",
530 "help",
531 "metric_kind",
532 "name_valid",
533 ]
534 );
535
536 let by_name = |name: &str| {
537 cols.iter()
538 .find(|(n, _)| n == name)
539 .expect("column present in shaped desc")
540 .1
541 .clone()
542 };
543
544 assert!(!by_name("labels").nullable);
546 assert!(!by_name("help").nullable);
547
548 assert!(by_name("metric_name").nullable);
550 assert!(by_name("value").nullable);
551
552 assert_eq!(by_name("metric_kind"), SqlScalarType::Int32.nullable(true));
554 assert_eq!(by_name("name_valid"), SqlScalarType::Bool.nullable(true));
555 }
556
557 #[mz_ore::test]
558 fn shaped_expr_projects_seven_columns() {
559 let (expr, _desc) =
560 shape_metric_sink_source(source_get(&source_desc()), &source_desc(), "app_");
561
562 match &expr {
565 MirRelationExpr::Project { outputs, .. } => {
566 assert_eq!(outputs.len(), 7);
567 }
568 other => panic!("expected a Project at the root of the shaped expr, got {other:?}"),
569 }
570 }
571
572 fn shaped_map_scalars(desc: &RelationDesc, prefix: &str) -> Vec<MirScalarExpr> {
575 let (expr, _desc) = shape_metric_sink_source(source_get(desc), desc, prefix);
576 match expr {
577 MirRelationExpr::Project { input, .. } => match *input {
578 MirRelationExpr::Map { scalars, .. } => scalars,
579 other => panic!("expected a Map under the Project, got {other:?}"),
580 },
581 other => panic!("expected a Project at the root, got {other:?}"),
582 }
583 }
584
585 fn eval_shaped_row<'a>(
589 scalars: &'a [MirScalarExpr],
590 input: &[Datum<'a>],
591 arena: &'a RowArena,
592 ) -> Vec<Datum<'a>> {
593 let mut row = input.to_vec();
594 for scalar in scalars {
595 let datum = scalar.eval(&row, arena).expect("scalar eval succeeds");
596 row.push(datum);
597 }
598 row
599 }
600
601 #[mz_ore::test]
602 fn metric_kind_classifies_type() {
603 let scalars = shaped_map_scalars(&source_desc(), "app_");
604 let metric_kind = &scalars[3];
605 let arena = RowArena::new();
606 for (metric_type, expected) in [
608 ("gauge", Datum::Int32(0)),
609 ("counter", Datum::Int32(1)),
610 ("histogram", Datum::Null),
611 ("summary", Datum::Null),
612 ] {
613 let row = [
614 Datum::Null,
615 Datum::String(metric_type),
616 Datum::Null,
617 Datum::Null,
618 Datum::Null,
619 Datum::Null,
620 ];
621 assert_eq!(
622 metric_kind
623 .eval(&row, &arena)
624 .expect("metric_kind eval succeeds"),
625 expected,
626 "metric_type = {metric_type}",
627 );
628 }
629 }
630
631 #[mz_ore::test]
632 fn name_valid_matches_prometheus_grammar() {
633 let scalars = shaped_map_scalars(&source_desc(), "app_");
638 let arena = RowArena::new();
639 for (metric_name, expected) in [
640 (Datum::String("http_requests_total"), Datum::True),
641 (Datum::String("with:colons_and_1_digit"), Datum::True),
642 (Datum::String("1_leading_digit"), Datum::True),
643 (Datum::String("has-a-dash"), Datum::False),
644 (Datum::String(""), Datum::True),
645 (Datum::Null, Datum::False),
646 ] {
647 let input = [
648 metric_name,
649 Datum::Null,
650 Datum::Null,
651 Datum::Null,
652 Datum::Null,
653 Datum::Null,
654 ];
655 let row = eval_shaped_row(&scalars, &input, &arena);
658 assert_eq!(
659 *row.last().expect("row has appended columns"),
660 expected,
661 "metric_name = {metric_name:?}",
662 );
663 }
664 }
665
666 #[derive(Debug)]
669 struct SingleTableCatalog {
670 entry: CatalogEntry,
671 }
672
673 const TABLE_ITEM_ID: CatalogItemId = CatalogItemId::User(1);
674 const TABLE_GID: GlobalId = GlobalId::User(1);
675 const SINK_GID: GlobalId = GlobalId::User(2);
676
677 impl SingleTableCatalog {
678 fn new() -> Self {
679 let table = Table {
680 create_sql: None,
681 desc: VersionedRelationDesc::new(source_desc()),
682 collections: BTreeMap::from([(RelationVersion::root(), TABLE_GID)]),
683 conn_id: None,
684 resolved_ids: ResolvedIds::empty(),
685 custom_logical_compaction_window: None,
686 is_retained_metrics_object: false,
687 data_source: TableDataSource::TableWrites {
688 defaults: Vec::new(),
689 },
690 };
691 let entry = CatalogEntry {
692 item: CatalogItem::Table(table),
693 referenced_by: Vec::new(),
694 used_by: Vec::new(),
695 id: TABLE_ITEM_ID,
696 oid: 20_000,
697 name: QualifiedItemName {
698 qualifiers: ItemQualifiers {
699 database_spec: ResolvedDatabaseSpecifier::Ambient,
700 schema_spec: SchemaSpecifier::Id(SchemaId::User(1)),
701 },
702 item: "t".to_string(),
703 },
704 owner_id: RoleId::User(1),
705 privileges: PrivilegeMap::default(),
706 };
707 Self { entry }
708 }
709 }
710
711 impl OptimizerCatalog for SingleTableCatalog {
712 fn get_entry(&self, _id: &GlobalId) -> mz_catalog::memory::objects::CatalogCollectionEntry {
713 mz_catalog::memory::objects::CatalogCollectionEntry {
714 entry: self.entry.clone(),
715 version: RelationVersionSelector::Latest,
716 }
717 }
718
719 fn get_entry_by_item_id(&self, _id: &CatalogItemId) -> &CatalogEntry {
720 &self.entry
721 }
722
723 fn resolve_full_name(
724 &self,
725 name: &QualifiedItemName,
726 _conn_id: Option<&mz_adapter_types::connection::ConnectionId>,
727 ) -> FullItemName {
728 FullItemName {
729 database: RawDatabaseSpecifier::Ambient,
730 schema: "public".to_string(),
731 item: name.item.clone(),
732 }
733 }
734
735 fn get_indexes_on(
736 &self,
737 _id: GlobalId,
738 _cluster: ClusterId,
739 ) -> Box<dyn Iterator<Item = (GlobalId, &mz_catalog::memory::objects::Index)> + '_>
740 {
741 Box::new(std::iter::empty())
742 }
743 }
744
745 const VIEW_GID: GlobalId = GlobalId::Transient(1);
746
747 fn optimize_from(from: MetricSinkFrom, metric_label: Option<String>) -> LirDataflowDescription {
749 let catalog = Arc::new(SingleTableCatalog::new());
750 let cluster_id = ClusterId::user(1).expect("valid cluster id");
751 let compute_instance = ComputeInstanceSnapshot::new_without_collections(cluster_id);
752 let config = OptimizerConfig::from(&SystemVars::default());
753 let metrics = OptimizerMetrics::register_into(&MetricsRegistry::new(), Duration::MAX);
754
755 let mut optimizer = Optimizer::new(
756 catalog,
757 compute_instance,
758 VIEW_GID,
759 SINK_GID,
760 config,
761 metrics,
762 );
763
764 let global_mir_plan = optimizer
765 .optimize(MetricSink::new(
766 "metric-sink-test".to_string(),
767 from,
768 "app_".to_string(),
769 metric_label,
770 ))
771 .expect("MIR optimization succeeds");
772 let global_lir_plan = optimizer
773 .optimize(global_mir_plan)
774 .expect("LIR optimization succeeds");
775 let (df_desc, _df_meta) = global_lir_plan.unapply();
776 df_desc
777 }
778
779 fn assert_one_shaped_metric_sink_export(df_desc: &LirDataflowDescription) {
782 assert!(df_desc.index_exports.is_empty());
783 let sink_exports: Vec<_> = df_desc.sink_exports.iter().collect();
784 assert_eq!(sink_exports.len(), 1);
785 let (sink_id, sink_desc) = sink_exports[0];
786 assert_eq!(*sink_id, SINK_GID);
787 assert!(matches!(
788 sink_desc.connection,
789 ComputeSinkConnection::MetricSink(_)
790 ));
791 assert_eq!(sink_desc.from, VIEW_GID);
792 let shaped_names: Vec<&str> = sink_desc
793 .from_desc
794 .iter_names()
795 .map(|n| n.as_str())
796 .collect();
797 assert_eq!(
798 shaped_names,
799 vec![
800 "metric_name",
801 "metric_type",
802 "labels",
803 "value",
804 "help",
805 "metric_kind",
806 "name_valid",
807 ]
808 );
809 }
810
811 fn sink_label(df_desc: &LirDataflowDescription) -> &str {
813 match &df_desc
814 .sink_exports
815 .values()
816 .next()
817 .expect("one export")
818 .connection
819 {
820 ComputeSinkConnection::MetricSink(conn) => &conn.label,
821 other => panic!("expected a metric sink connection, got {other:?}"),
822 }
823 }
824
825 #[mz_ore::test]
826 fn optimizer_exports_one_metric_sink() {
827 let df_desc = optimize_from(MetricSinkFrom::Id(TABLE_GID), None);
828 assert_one_shaped_metric_sink_export(&df_desc);
829 assert!(df_desc.source_imports.contains_key(&TABLE_GID));
831 let build_ids: Vec<_> = df_desc.objects_to_build.iter().map(|b| b.id).collect();
832 assert_eq!(build_ids, vec![VIEW_GID]);
833 }
834
835 #[mz_ore::test]
838 fn optimizer_shapes_a_query_source() {
839 let desc = source_desc();
840 let expr = HirRelationExpr::Get {
843 id: mz_expr::Id::Global(TABLE_GID),
844 typ: desc.typ().clone(),
845 };
846
847 let df_desc = optimize_from(
848 MetricSinkFrom::Query {
849 expr,
850 desc: desc.clone(),
851 },
852 None,
853 );
854 assert_one_shaped_metric_sink_export(&df_desc);
855 assert!(df_desc.source_imports.contains_key(&TABLE_GID));
857 let build_ids: Vec<_> = df_desc.objects_to_build.iter().map(|b| b.id).collect();
858 assert_eq!(build_ids, vec![VIEW_GID]);
859 }
860
861 #[mz_ore::test]
864 fn metric_sink_label_defaults_to_sink_id_else_override() {
865 let df_desc = optimize_from(MetricSinkFrom::Id(TABLE_GID), None);
866 assert_eq!(sink_label(&df_desc), SINK_GID.to_string());
867
868 let df_desc = optimize_from(
869 MetricSinkFrom::Id(TABLE_GID),
870 Some("mz_curated".to_string()),
871 );
872 assert_eq!(sink_label(&df_desc), "mz_curated");
873 }
874}