1use std::collections::{BTreeMap, BTreeSet};
18
19use chrono::{DateTime, Utc};
20use maplit::{btreemap, btreeset};
21use tracing::warn;
22
23use mz_catalog::memory::objects::{CatalogItem, DataSourceDesc, Index, TableDataSource, View};
24use mz_compute_client::controller::error::InstanceMissing;
25use mz_compute_types::ComputeInstanceId;
26use mz_compute_types::dataflows::{DataflowDesc, DataflowDescription, IndexDesc};
27use mz_controller::Controller;
28use mz_expr::visit::Visit;
29use mz_expr::{
30 CollectionPlan, Id, MapFilterProject, MirRelationExpr, MirScalarExpr, OptimizedMirRelationExpr,
31 RECURSION_LIMIT, UnmaterializableFunc,
32};
33use mz_ore::cast::ReinterpretCast;
34use mz_ore::stack::{CheckedRecursion, RecursionGuard, RecursionLimitError, maybe_grow};
35use mz_repr::adt::array::ArrayDimension;
36use mz_repr::explain::trace_plan;
37use mz_repr::optimize::OptimizerFeatures;
38use mz_repr::role_id::RoleId;
39use mz_repr::{Datum, GlobalId, ReprRelationType, Row};
40use mz_sql::catalog::CatalogRole;
41use mz_sql::rbac;
42use mz_sql::session::metadata::SessionMetadata;
43use mz_transform::analysis::DerivedBuilder;
44use mz_transform::analysis::monotonic::Monotonic;
45
46use crate::catalog::CatalogState;
47use crate::coord::id_bundle::CollectionIdBundle;
48use crate::optimize::{Optimize, OptimizerCatalog, OptimizerConfig, OptimizerError, view};
49use crate::session::{SERVER_MAJOR_VERSION, SERVER_MINOR_VERSION};
50use crate::util::viewable_variables;
51
52#[derive(Debug, Clone)]
55pub struct ComputeInstanceSnapshot {
56 instance_id: ComputeInstanceId,
57 collections: Option<BTreeSet<GlobalId>>,
62}
63
64impl ComputeInstanceSnapshot {
65 pub fn new(controller: &Controller, id: ComputeInstanceId) -> Result<Self, InstanceMissing> {
66 controller
67 .compute
68 .collection_ids(id)
69 .map(|collection_ids| Self {
70 instance_id: id,
71 collections: Some(collection_ids.collect()),
72 })
73 }
74
75 pub fn new_from_parts(instance_id: ComputeInstanceId, collections: BTreeSet<GlobalId>) -> Self {
76 Self {
77 instance_id,
78 collections: Some(collections),
79 }
80 }
81
82 pub fn new_without_collections(instance_id: ComputeInstanceId) -> Self {
83 Self {
84 instance_id,
85 collections: None,
86 }
87 }
88
89 pub fn instance_id(&self) -> ComputeInstanceId {
91 self.instance_id
92 }
93
94 pub fn contains_collection(&self, id: &GlobalId) -> bool {
97 self.collections
98 .as_ref()
99 .map_or(true, |collections| collections.contains(id))
100 }
101
102 pub fn insert_collection(&mut self, id: GlobalId) {
104 self.collections
105 .as_mut()
106 .expect("insert_collection called on snapshot with None collections")
107 .insert(id);
108 }
109}
110
111#[derive(Debug)]
113pub struct DataflowBuilder<'a> {
114 pub catalog: &'a dyn OptimizerCatalog,
115 pub compute: ComputeInstanceSnapshot,
120 pub replan: Option<GlobalId>,
129 recursion_guard: RecursionGuard,
131}
132
133pub trait ExprPrep {
135 fn prep_relation_expr(&self, expr: &mut OptimizedMirRelationExpr)
137 -> Result<(), OptimizerError>;
138
139 fn prep_scalar_expr(&self, expr: &mut MirScalarExpr) -> Result<(), OptimizerError>;
141}
142
143pub struct ExprPrepNoop;
145impl ExprPrep for ExprPrepNoop {
146 fn prep_relation_expr(&self, _: &mut OptimizedMirRelationExpr) -> Result<(), OptimizerError> {
147 Ok(())
148 }
149 fn prep_scalar_expr(&self, _expr: &mut MirScalarExpr) -> Result<(), OptimizerError> {
150 Ok(())
151 }
152}
153
154pub struct ExprPrepMaintained;
157
158impl ExprPrep for ExprPrepMaintained {
159 fn prep_relation_expr(
160 &self,
161 expr: &mut OptimizedMirRelationExpr,
162 ) -> Result<(), OptimizerError> {
163 expr.0.try_visit_mut_post(&mut |e| {
164 if let MirRelationExpr::Filter { input, predicates } = &*e {
166 let mfp = MapFilterProject::new(input.arity()).filter(predicates.iter().cloned());
167 match mfp.into_plan() {
168 Err(e) => Err(OptimizerError::UnsupportedTemporalExpression(e)),
169 Ok(mut mfp) => {
170 for s in mfp.iter_nontemporal_exprs() {
171 self.prep_scalar_expr(s)?;
172 }
173 Ok(())
174 }
175 }
176 } else {
177 e.try_visit_scalars_mut1(&mut |s| self.prep_scalar_expr(s))
178 }
179 })
180 }
181
182 fn prep_scalar_expr(&self, expr: &mut MirScalarExpr) -> Result<(), OptimizerError> {
183 let mut last_observed_unmaterializable_func = None;
185 expr.visit_mut_post(&mut |e| {
186 if let MirScalarExpr::CallUnmaterializable(f) = e {
187 last_observed_unmaterializable_func = Some(f.clone());
188 }
189 });
190
191 if let Some(f) = last_observed_unmaterializable_func {
192 Err(OptimizerError::UnmaterializableFunction(f))
193 } else {
194 Ok(())
195 }
196 }
197}
198
199pub struct ExprPrepOneShot<'a> {
202 pub logical_time: EvalTime,
203 pub session: &'a dyn SessionMetadata,
204 pub catalog_state: &'a CatalogState,
205}
206
207impl ExprPrep for ExprPrepOneShot<'_> {
208 fn prep_relation_expr(
209 &self,
210 expr: &mut OptimizedMirRelationExpr,
211 ) -> Result<(), OptimizerError> {
212 expr.0
213 .try_visit_scalars_mut(&mut |s| self.prep_scalar_expr(s))
214 }
215
216 fn prep_scalar_expr(&self, expr: &mut MirScalarExpr) -> Result<(), OptimizerError> {
217 expr.try_visit_mut_post(&mut |e| {
220 if let MirScalarExpr::CallUnmaterializable(f) = e {
221 *e = eval_unmaterializable_func(
222 self.catalog_state,
223 f,
224 self.logical_time,
225 self.session,
226 )?;
227 }
228 Ok(())
229 })
230 }
231}
232
233pub struct ExprPrepWebhookValidation {
236 pub now: DateTime<Utc>,
238}
239
240impl ExprPrep for ExprPrepWebhookValidation {
241 fn prep_relation_expr(
242 &self,
243 expr: &mut OptimizedMirRelationExpr,
244 ) -> Result<(), OptimizerError> {
245 expr.0
246 .try_visit_scalars_mut(&mut |s| self.prep_scalar_expr(s))
247 }
248
249 fn prep_scalar_expr(&self, expr: &mut MirScalarExpr) -> Result<(), OptimizerError> {
250 let now = self.now;
251 expr.try_visit_mut_post(&mut |e| {
252 if let MirScalarExpr::CallUnmaterializable(f @ UnmaterializableFunc::CurrentTimestamp) =
253 e
254 {
255 let now: Datum = now.try_into()?;
256 let const_expr = MirScalarExpr::literal_ok(now, f.output_type().scalar_type);
257 *e = const_expr;
258 }
259 Ok(())
260 })
261 }
262}
263
264#[derive(Clone, Copy, Debug)]
265pub enum EvalTime {
266 Time(mz_repr::Timestamp),
267 NotAvailable,
269}
270
271pub fn dataflow_import_id_bundle<P>(
273 dataflow: &DataflowDescription<P>,
274 compute_instance: ComputeInstanceId,
275) -> CollectionIdBundle {
276 let storage_ids = dataflow.source_imports.keys().copied().collect();
277 let compute_ids = dataflow.index_imports.keys().copied().collect();
278 CollectionIdBundle {
279 storage_ids,
280 compute_ids: btreemap! {compute_instance => compute_ids},
281 }
282}
283
284impl<'a> DataflowBuilder<'a> {
285 pub fn new(catalog: &'a dyn OptimizerCatalog, compute: ComputeInstanceSnapshot) -> Self {
286 Self {
287 catalog,
288 compute,
289 replan: None,
290 recursion_guard: RecursionGuard::with_limit(RECURSION_LIMIT),
291 }
292 }
293
294 pub(super) fn with_config(mut self, config: &OptimizerConfig) -> Self {
299 self.replan = config.replan;
300 self
301 }
302
303 pub fn import_into_dataflow(
309 &mut self,
310 id: &GlobalId,
311 dataflow: &mut DataflowDesc,
312 features: &OptimizerFeatures,
313 ) -> Result<(), OptimizerError> {
314 maybe_grow(|| {
315 if dataflow.is_imported(id) {
317 return Ok(());
318 }
319
320 let monotonic = self.monotonic_object(*id, features);
321
322 let mut valid_indexes = self.indexes_on(*id).peekable();
327 if valid_indexes.peek().is_some() {
328 for (index_id, idx) in valid_indexes {
329 let index_desc = IndexDesc {
330 on_id: *id,
331 key: idx.keys.to_vec(),
332 };
333 let entry = self.catalog.get_entry(id);
334 let desc = entry
335 .relation_desc()
336 .expect("indexes can only be built on items with descs");
337 dataflow.import_index(
338 index_id,
339 index_desc,
340 ReprRelationType::from(desc.typ()),
341 monotonic,
342 );
343 }
344 } else {
345 drop(valid_indexes);
346 let entry = self.catalog.get_entry(id);
347 match entry.item() {
349 CatalogItem::Table(table) => {
350 dataflow.import_source(*id, table.desc_for(id).into_typ(), monotonic);
351 }
352 CatalogItem::Source(source) => {
353 dataflow.import_source(*id, source.desc.typ().clone(), monotonic);
354 }
355 CatalogItem::View(view) => {
356 let expr = view.locally_optimized_expr.as_ref();
357 self.import_view_into_dataflow(id, expr, dataflow, features)?;
358 }
359 CatalogItem::MaterializedView(mview) if mview.replacement_target.is_some() => {
360 let expr = mview.locally_optimized_expr.as_ref();
362 self.import_view_into_dataflow(id, expr, dataflow, features)?;
363 }
364 CatalogItem::MaterializedView(mview) => {
365 dataflow.import_source(*id, mview.desc_for(id).into_typ(), monotonic);
366 }
367 CatalogItem::Log(log) => {
368 dataflow.import_source(*id, log.variant.desc().typ().clone(), monotonic);
369 }
370 CatalogItem::Sink(_)
371 | CatalogItem::MetricSink(_)
372 | CatalogItem::Index(_)
373 | CatalogItem::Type(_)
374 | CatalogItem::Func(_)
375 | CatalogItem::Secret(_)
376 | CatalogItem::Connection(_) => {
377 unreachable!()
379 }
380 }
381 }
382 Ok(())
383 })
384 }
385
386 pub fn import_view_into_dataflow(
396 &mut self,
397 view_id: &GlobalId,
398 view: &OptimizedMirRelationExpr,
399 dataflow: &mut DataflowDesc,
400 features: &OptimizerFeatures,
401 ) -> Result<(), OptimizerError> {
402 for get_id in view.depends_on() {
403 self.import_into_dataflow(&get_id, dataflow, features)?;
404 }
405 dataflow.insert_plan(*view_id, view.clone());
406 Ok(())
407 }
408
409 pub fn maybe_reoptimize_imported_views(
412 &self,
413 df_desc: &mut DataflowDesc,
414 config: &OptimizerConfig,
415 ) -> Result<(), OptimizerError> {
416 if !config.features.reoptimize_imported_views {
417 return Ok(()); }
419
420 let mut view_optimizer = view::Optimizer::new(config.clone(), None);
421 for desc in df_desc.objects_to_build.iter_mut().rev() {
422 if matches!(desc.id, GlobalId::Explain | GlobalId::Transient(_)) {
423 continue; }
425 if let CatalogItem::View(view) = &self.catalog.get_entry(&desc.id).item {
426 let _span = tracing::span!(
427 target: "optimizer",
428 tracing::Level::DEBUG,
429 "view",
430 path.segment = desc.id.to_string()
431 )
432 .entered();
433
434 desc.plan = view_optimizer.optimize(view.raw_expr.as_ref().clone())?;
436
437 trace_plan(desc.plan.as_inner());
439 }
440 }
441
442 Ok(())
443 }
444
445 fn monotonic_source(&self, data_source: &DataSourceDesc) -> bool {
447 match data_source {
448 DataSourceDesc::Ingestion { .. } => false,
449 DataSourceDesc::OldSyntaxIngestion {
450 desc, data_config, ..
451 } => data_config.monotonic(&desc.connection),
452 DataSourceDesc::Webhook { .. } => true,
453 DataSourceDesc::IngestionExport {
454 ingestion_id,
455 data_config,
456 ..
457 } => {
458 let source_desc = self
459 .catalog
460 .get_entry_by_item_id(ingestion_id)
461 .source_desc()
462 .expect("ingestion export must reference a source")
463 .expect("ingestion export must reference a source");
464 data_config.monotonic(&source_desc.connection)
465 }
466 DataSourceDesc::Introspection(_)
467 | DataSourceDesc::Progress
468 | DataSourceDesc::Catalog => false,
469 }
470 }
471
472 fn monotonic_object(&self, id: GlobalId, features: &OptimizerFeatures) -> bool {
482 self.monotonic_object_inner(id, &mut BTreeMap::new(), features)
483 .unwrap_or_else(|e| {
484 warn!(%id, "error inspecting object for monotonicity: {e}");
485 false
486 })
487 }
488
489 fn monotonic_object_inner(
490 &self,
491 id: GlobalId,
492 memo: &mut BTreeMap<GlobalId, bool>,
493 features: &OptimizerFeatures,
494 ) -> Result<bool, RecursionLimitError> {
495 if let Some(monotonic) = memo.get(&id) {
498 return Ok(*monotonic);
499 }
500
501 let monotonic = self.checked_recur(|_| {
502 match self.catalog.get_entry(&id).item() {
503 CatalogItem::Source(source) => Ok(self.monotonic_source(&source.data_source)),
504 CatalogItem::Table(table) => match &table.data_source {
505 TableDataSource::TableWrites { .. } => Ok(false),
506 TableDataSource::DataSource { desc, timeline: _ } => {
507 Ok(self.monotonic_source(desc))
508 }
509 },
510 CatalogItem::View(View {
511 locally_optimized_expr: optimized_expr,
512 ..
513 }) => {
514 let view_expr = optimized_expr.as_ref().clone().into_inner();
515
516 let mut monotonic_ids = BTreeSet::new();
519 let recursion_result: Result<(), RecursionLimitError> = view_expr
520 .try_visit_post(&mut |e| {
521 if let MirRelationExpr::Get {
522 id: Id::Global(got_id),
523 ..
524 } = e
525 {
526 if self.monotonic_object_inner(*got_id, memo, features)? {
527 monotonic_ids.insert(*got_id);
528 }
529 }
530 Ok(())
531 });
532 if let Err(error) = recursion_result {
533 warn!(%id, "error inspecting view for monotonicity: {error}");
536 }
537
538 let mut builder = DerivedBuilder::new(features);
539 builder.require(Monotonic::new(monotonic_ids.clone()));
540 let derived = builder.visit(&view_expr);
541
542 Ok(*derived
543 .as_view()
544 .value::<Monotonic>()
545 .expect("Expected monotonic result from non empty tree"))
546 }
547 CatalogItem::Index(Index { on, .. }) => {
548 self.monotonic_object_inner(*on, memo, features)
549 }
550 CatalogItem::Secret(_)
551 | CatalogItem::Type(_)
552 | CatalogItem::Connection(_)
553 | CatalogItem::Log(_)
554 | CatalogItem::MaterializedView(_)
555 | CatalogItem::Sink(_)
556 | CatalogItem::MetricSink(_)
557 | CatalogItem::Func(_) => Ok(false),
558 }
559 })?;
560
561 memo.insert(id, monotonic);
562
563 Ok(monotonic)
564 }
565}
566
567impl<'a> CheckedRecursion for DataflowBuilder<'a> {
568 fn recursion_guard(&self) -> &RecursionGuard {
569 &self.recursion_guard
570 }
571}
572
573fn eval_unmaterializable_func(
574 state: &CatalogState,
575 f: &UnmaterializableFunc,
576 logical_time: EvalTime,
577 session: &dyn SessionMetadata,
578) -> Result<MirScalarExpr, OptimizerError> {
579 if session.restrict_to_user_objects() && !f.allowed_in_restricted_session() {
580 return Err(OptimizerError::RestrictedFunction(f.clone()));
581 }
582
583 let pack_1d_array = |datums: Vec<Datum>| {
584 let mut row = Row::default();
585 row.packer()
586 .try_push_array(
587 &[ArrayDimension {
588 lower_bound: 1,
589 length: datums.len(),
590 }],
591 datums,
592 )
593 .expect("known to be a valid array");
594 Ok(MirScalarExpr::literal_from_single_element_row(
595 row,
596 f.output_type().scalar_type,
597 ))
598 };
599 let pack_dict = |mut datums: Vec<(String, String)>| {
600 datums.sort();
601 let mut row = Row::default();
602 row.packer().push_dict(
603 datums
604 .iter()
605 .map(|(key, value)| (key.as_str(), Datum::from(value.as_str()))),
606 );
607 Ok(MirScalarExpr::literal_from_single_element_row(
608 row,
609 f.output_type().scalar_type,
610 ))
611 };
612 let pack = |datum| {
613 Ok(MirScalarExpr::literal_ok(
614 datum,
615 f.output_type().scalar_type,
616 ))
617 };
618
619 match f {
620 UnmaterializableFunc::CurrentDatabase => pack(Datum::from(session.database())),
621 UnmaterializableFunc::CurrentSchema => {
622 let search_path = state.resolve_search_path(session);
623 let schema = search_path
624 .first()
625 .map(|(db, schema)| &*state.get_schema(db, schema, session.conn_id()).name.schema);
626 pack(Datum::from(schema))
627 }
628 UnmaterializableFunc::CurrentSchemasWithSystem => {
629 let search_path = state.resolve_search_path(session);
630 let search_path = state.effective_search_path(&search_path, false);
631 pack_1d_array(
632 search_path
633 .into_iter()
634 .map(|(db, schema)| {
635 let schema = state.get_schema(&db, &schema, session.conn_id());
636 Datum::String(&schema.name.schema)
637 })
638 .collect(),
639 )
640 }
641 UnmaterializableFunc::CurrentSchemasWithoutSystem => {
642 let search_path = state.resolve_search_path(session);
643 pack_1d_array(
644 search_path
645 .into_iter()
646 .map(|(db, schema)| {
647 let schema = state.get_schema(&db, &schema, session.conn_id());
648 Datum::String(&schema.name.schema)
649 })
650 .collect(),
651 )
652 }
653 UnmaterializableFunc::ViewableVariables => pack_dict(
654 viewable_variables(state, session)
655 .map(|var| (var.name().to_lowercase(), var.value()))
656 .collect(),
657 ),
658 UnmaterializableFunc::CurrentTimestamp => {
659 let t: Datum = session.pcx().wall_time.try_into()?;
660 pack(t)
661 }
662 UnmaterializableFunc::CurrentUser => pack(Datum::from(
663 state.get_role(session.current_role_id()).name(),
664 )),
665 UnmaterializableFunc::SessionUser => pack(Datum::from(
666 state.get_role(session.session_role_id()).name(),
667 )),
668 UnmaterializableFunc::IsRbacEnabled => pack(Datum::from(
669 rbac::is_rbac_enabled_for_session(state.system_config(), session),
670 )),
671 UnmaterializableFunc::MzIsSuperuser => pack(Datum::from(session.is_superuser())),
672 UnmaterializableFunc::MzNow => match logical_time {
673 EvalTime::Time(logical_time) => pack(Datum::MzTimestamp(logical_time)),
674 EvalTime::NotAvailable => Err(OptimizerError::UncallableFunction {
675 func: UnmaterializableFunc::MzNow,
676 context: "this",
677 }),
678 },
679 UnmaterializableFunc::MzRoleOidMemberships => {
680 let role_memberships = role_oid_memberships(state);
681 let mut role_memberships: Vec<(_, Vec<_>)> = role_memberships
682 .into_iter()
683 .map(|(role_id, role_membership)| {
684 (
685 role_id.to_string(),
686 role_membership
687 .into_iter()
688 .map(|role_id| role_id.to_string())
689 .collect(),
690 )
691 })
692 .collect();
693 role_memberships.sort();
694 let mut row = Row::default();
695 row.packer().push_dict_with(|row| {
696 for (role_id, role_membership) in &role_memberships {
697 row.push(Datum::from(role_id.as_str()));
698 row.try_push_array(
699 &[ArrayDimension {
700 lower_bound: 1,
701 length: role_membership.len(),
702 }],
703 role_membership.iter().map(|role_id| Datum::from(role_id.as_str())),
704 ).expect("role_membership is 1 dimensional, and its length is used for the array length");
705 }
706 });
707 Ok(MirScalarExpr::literal_from_single_element_row(
708 row,
709 f.output_type().scalar_type,
710 ))
711 }
712 UnmaterializableFunc::MzSessionId => pack(Datum::from(state.config().session_id)),
713 UnmaterializableFunc::MzSessionRoleMemberships => {
714 let role_id = session.current_role_id();
715 let mut names = session_role_memberships(state, role_id);
716 names.sort();
717 pack_1d_array(names.iter().map(|n| Datum::from(n.as_str())).collect())
718 }
719 UnmaterializableFunc::MzUptime => {
720 let uptime = state.config().start_instant.elapsed();
721 let uptime = chrono::Duration::from_std(uptime).map_or(Datum::Null, Datum::from);
722 pack(uptime)
723 }
724 UnmaterializableFunc::MzVersion => pack(Datum::from(
725 &*state
726 .config()
727 .build_info
728 .human_version(state.config().helm_chart_version.clone()),
729 )),
730 UnmaterializableFunc::MzVersionNum => {
731 pack(Datum::Int32(state.config().build_info.version_num()))
732 }
733 UnmaterializableFunc::PgBackendPid => pack(Datum::Int32(i32::reinterpret_cast(
734 session.conn_id().unhandled(),
735 ))),
736 UnmaterializableFunc::PgPostmasterStartTime => {
737 let t: Datum = state.config().start_time.try_into()?;
738 pack(t)
739 }
740 UnmaterializableFunc::Version => {
741 let build_info = state.config().build_info;
742 let version = format!(
743 "PostgreSQL {}.{} on {} (Materialize {})",
744 SERVER_MAJOR_VERSION,
745 SERVER_MINOR_VERSION,
746 mz_build_info::TARGET_TRIPLE,
747 build_info.version,
748 );
749 pack(Datum::from(&*version))
750 }
751 }
752}
753
754fn role_oid_memberships<'a>(catalog: &'a CatalogState) -> BTreeMap<u32, BTreeSet<u32>> {
755 let mut role_memberships = BTreeMap::new();
756 for role_id in catalog.get_roles() {
757 let role = catalog.get_role(role_id);
758 if !role_memberships.contains_key(&role.oid) {
759 role_oid_memberships_inner(catalog, role_id, &mut role_memberships);
760 }
761 }
762 role_memberships
763}
764
765fn role_oid_memberships_inner<'a>(
766 catalog: &'a CatalogState,
767 role_id: &RoleId,
768 role_memberships: &mut BTreeMap<u32, BTreeSet<u32>>,
769) {
770 let role = catalog.get_role(role_id);
771 role_memberships.insert(role.oid, btreeset! {role.oid});
772 for parent_role_id in role.membership.map.keys() {
773 let parent_role = catalog.get_role(parent_role_id);
774 if !role_memberships.contains_key(&parent_role.oid) {
775 role_oid_memberships_inner(catalog, parent_role_id, role_memberships);
776 }
777 let parent_membership: BTreeSet<_> = role_memberships
778 .get(&parent_role.oid)
779 .expect("inserted in recursive call above")
780 .into_iter()
781 .cloned()
782 .collect();
783 role_memberships
784 .get_mut(&role.oid)
785 .expect("inserted above")
786 .extend(parent_membership);
787 }
788}
789
790fn session_role_memberships(catalog: &CatalogState, role_id: &RoleId) -> Vec<String> {
793 let mut visited = BTreeSet::new();
794 session_role_memberships_inner(catalog, role_id, &mut visited);
795 visited.into_iter().collect()
796}
797
798fn session_role_memberships_inner(
799 catalog: &CatalogState,
800 role_id: &RoleId,
801 visited: &mut BTreeSet<String>,
802) {
803 let role = catalog.get_role(role_id);
804 if !visited.insert(role.name.clone()) {
808 return;
809 }
810 for parent_role_id in role.membership.map.keys() {
811 session_role_memberships_inner(catalog, parent_role_id, visited);
812 }
813}