From 36e28eeed5f31c8f7f3f5e8c59bf9fde2ec9c2d9 Mon Sep 17 00:00:00 2001 From: "Alex Chi Z." <4198311+skyzh@users.noreply.github.com> Date: Sun, 3 Nov 2024 11:00:26 -0500 Subject: [PATCH] refactor(core): abstract memo table into a trait (#211) The current memo table implementation is refactored into a trait, and the implementation is called `NaiveMemo`. --------- Signed-off-by: Alex Chi --- optd-core/src/cascades.rs | 2 +- optd-core/src/cascades/memo.rs | 430 +++++++++++++------------ optd-core/src/cascades/optimizer.rs | 23 +- optd-datafusion-repr/src/memo_ext.rs | 17 +- optd-datafusion-repr/src/plan_nodes.rs | 9 +- 5 files changed, 242 insertions(+), 239 deletions(-) diff --git a/optd-core/src/cascades.rs b/optd-core/src/cascades.rs index cbeda5fe..e69486e1 100644 --- a/optd-core/src/cascades.rs +++ b/optd-core/src/cascades.rs @@ -4,6 +4,6 @@ mod memo; mod optimizer; mod tasks; -pub use memo::Memo; +pub use memo::{Memo, NaiveMemo}; pub use optimizer::{CascadesOptimizer, ExprId, GroupId, OptimizerProperties, RelNodeContext}; use tasks::Task; diff --git a/optd-core/src/cascades/memo.rs b/optd-core/src/cascades/memo.rs index 31f1588c..683544b6 100644 --- a/optd-core/src/cascades/memo.rs +++ b/optd-core/src/cascades/memo.rs @@ -18,7 +18,7 @@ use super::optimizer::{ExprId, GroupId}; pub type RelMemoNodeRef = Arc>; -/// Equivalent to MExpr in Columbia/Cascades. +/// The RelNode representation in the memo table. Store children as group IDs. Equivalent to MExpr in Columbia/Cascades. #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RelMemoNode { pub typ: T, @@ -98,13 +98,147 @@ pub struct GroupInfo { pub winner: Winner, } -pub(crate) struct Group { +pub struct Group { pub(crate) group_exprs: HashSet, pub(crate) info: GroupInfo, pub(crate) properties: Arc<[Box]>, } -pub struct Memo { +/// Trait for memo table implementations. +pub trait Memo { + /// Add an expression to the memo table. If the expression already exists, it will return the existing group id and + /// expr id. Otherwise, a new group and expr will be created. + fn add_new_expr(&mut self, rel_node: RelNodeRef) -> (GroupId, ExprId); + + /// Add a new expression to an existing gruop. If the expression is a group, it will merge the two groups. Otherwise, + /// it will add the expression to the group. Returns the expr id if the expression is not a group. + fn add_expr_to_group(&mut self, rel_node: RelNodeRef, group_id: GroupId) -> Option; + + /// Get the group id of an expression. + /// The group id is volatile, depending on whether the groups are merged. + fn get_group_id(&self, expr_id: ExprId) -> GroupId; + + /// Get the memoized representation of a node. + fn get_expr_memoed(&self, expr_id: ExprId) -> RelMemoNodeRef; + + /// Get all groups IDs in the memo table. + fn get_all_group_ids(&self) -> Vec; + + /// Get a group by ID + fn get_group(&self, group_id: GroupId) -> &Group; + + /// Update the group info. + fn update_group_info(&mut self, group_id: GroupId, group_info: GroupInfo); + + // The below functions can be overwritten by the memo table implementation if there + // are more efficient way to retrieve the information. + + /// Get all expressions in the group. + fn get_all_exprs_in_group(&self, group_id: GroupId) -> Vec { + let group = self.get_group(group_id); + let mut exprs = group.group_exprs.iter().copied().collect_vec(); + exprs.sort(); + exprs + } + + /// Get group info of a group. + fn get_group_info(&self, group_id: GroupId) -> &GroupInfo { + &self.get_group(group_id).info + } + + /// Get the best group binding based on the cost + fn get_best_group_binding( + &self, + group_id: GroupId, + mut post_process: impl FnMut(Arc>, GroupId, &WinnerInfo), + ) -> Result> { + get_best_group_binding_inner(self, group_id, &mut post_process) + } + + /// Get all bindings of a predicate group. Will panic if the group contains more than one bindings. + fn get_predicate_binding(&self, group_id: GroupId) -> Option> { + get_predicate_binding_group_inner(self, group_id, true) + } + + /// Get all bindings of a predicate group. Returns None if the group contains zero or more than one bindings. + fn try_get_predicate_binding(&self, group_id: GroupId) -> Option> { + get_predicate_binding_group_inner(self, group_id, false) + } +} + +fn get_best_group_binding_inner + ?Sized, T: RelNodeTyp>( + this: &M, + group_id: GroupId, + post_process: &mut impl FnMut(Arc>, GroupId, &WinnerInfo), +) -> Result> { + let info: &GroupInfo = this.get_group_info(group_id); + if let Winner::Full(info @ WinnerInfo { expr_id, .. }) = &info.winner { + let expr = this.get_expr_memoed(*expr_id); + let mut children = Vec::with_capacity(expr.children.len()); + for child in &expr.children { + children.push( + get_best_group_binding_inner(this, *child, post_process) + .with_context(|| format!("when processing expr {}", expr_id))?, + ); + } + let node = Arc::new(RelNode { + typ: expr.typ.clone(), + children, + data: expr.data.clone(), + }); + post_process(node.clone(), group_id, info); + return Ok(node); + } + bail!("no best group binding for group {}", group_id) +} + +fn get_predicate_binding_expr_inner + ?Sized, T: RelNodeTyp>( + this: &M, + expr_id: ExprId, + panic_on_invalid_group: bool, +) -> Option> { + let expr = this.get_expr_memoed(expr_id); + let mut children = Vec::with_capacity(expr.children.len()); + for child in expr.children.iter() { + if let Some(child) = get_predicate_binding_group_inner(this, *child, panic_on_invalid_group) + { + children.push(child); + } else { + return None; + } + } + Some(Arc::new(RelNode { + typ: expr.typ.clone(), + data: expr.data.clone(), + children, + })) +} + +fn get_predicate_binding_group_inner + ?Sized, T: RelNodeTyp>( + this: &M, + group_id: GroupId, + panic_on_invalid_group: bool, +) -> Option> { + let exprs = this.get_all_exprs_in_group(group_id); + match exprs.len() { + 0 => None, + 1 => get_predicate_binding_expr_inner( + this, + exprs.first().copied().unwrap(), + panic_on_invalid_group, + ), + len => { + if panic_on_invalid_group { + panic!("group {group_id} has {len} expressions") + } else { + None + } + } + } +} + +/// A naive, simple, and unoptimized memo table implementation. +pub struct NaiveMemo { // Source of truth. groups: HashMap, expr_id_to_expr_node: HashMap>, @@ -124,7 +258,81 @@ pub struct Memo { dup_expr_mapping: HashMap, } -impl Memo { +impl Memo for NaiveMemo { + fn add_new_expr(&mut self, rel_node: RelNodeRef) -> (GroupId, ExprId) { + let (group_id, expr_id) = self + .add_new_group_expr_inner(rel_node, None) + .expect("should not trigger merge group"); + self.verify_integrity(); + (group_id, expr_id) + } + + fn add_expr_to_group(&mut self, rel_node: RelNodeRef, group_id: GroupId) -> Option { + if let Some(input_group) = rel_node.typ.extract_group() { + let input_group = self.reduce_group(input_group); + let group_id = self.reduce_group(group_id); + self.merge_group_inner(input_group, group_id); + return None; + } + let reduced_group_id = self.reduce_group(group_id); + let (returned_group_id, expr_id) = self + .add_new_group_expr_inner(rel_node, Some(reduced_group_id)) + .unwrap(); + assert_eq!(returned_group_id, reduced_group_id); + self.verify_integrity(); + Some(expr_id) + } + + fn get_group_id(&self, mut expr_id: ExprId) -> GroupId { + while let Some(new_expr_id) = self.dup_expr_mapping.get(&expr_id) { + expr_id = *new_expr_id; + } + *self + .expr_id_to_group_id + .get(&expr_id) + .expect("expr not found in group mapping") + } + + fn get_expr_memoed(&self, mut expr_id: ExprId) -> RelMemoNodeRef { + while let Some(new_expr_id) = self.dup_expr_mapping.get(&expr_id) { + expr_id = *new_expr_id; + } + self.expr_id_to_expr_node + .get(&expr_id) + .expect("expr not found in expr mapping") + .clone() + } + + fn get_all_group_ids(&self) -> Vec { + let mut ids = self.groups.keys().copied().collect_vec(); + ids.sort(); + ids + } + + fn get_group(&self, group_id: GroupId) -> &Group { + let group_id = self.reduce_group(group_id); + self.groups.get(&group_id).as_ref().unwrap() + } + + fn update_group_info(&mut self, group_id: GroupId, group_info: GroupInfo) { + if let Winner::Full(WinnerInfo { + total_weighted_cost, + expr_id, + .. + }) = &group_info.winner + { + assert!( + *total_weighted_cost != 0.0, + "{}", + self.expr_id_to_expr_node[expr_id] + ); + } + let grp = self.groups.get_mut(&group_id); + grp.unwrap().info = group_info; + } +} + +impl NaiveMemo { pub fn new(property_builders: Arc<[Box>]>) -> Self { Self { expr_id_to_group_id: HashMap::new(), @@ -190,51 +398,6 @@ impl Memo { } } - #[allow(dead_code)] - fn merge_group(&mut self, group_a: GroupId, group_b: GroupId) -> GroupId { - use std::cmp::Ordering; - let group_a = self.reduce_group(group_a); - let group_b = self.reduce_group(group_b); - let (merge_into, merge_from) = match group_a.0.cmp(&group_b.0) { - Ordering::Less => (group_a, group_b), - Ordering::Equal => return group_a, - Ordering::Greater => (group_b, group_a), - }; - self.merge_group_inner(merge_into, merge_from); - self.verify_integrity(); - merge_into - } - - /// Add an expression into the memo, returns the group id and the expr id. - pub fn add_new_expr(&mut self, rel_node: RelNodeRef) -> (GroupId, ExprId) { - let (group_id, expr_id) = self - .add_new_group_expr_inner(rel_node, None) - .expect("should not trigger merge group"); - self.verify_integrity(); - (group_id, expr_id) - } - - /// Add an expression into the memo, returns the expr id if rel_node is NOT a group. - pub fn add_expr_to_group( - &mut self, - rel_node: RelNodeRef, - group_id: GroupId, - ) -> Option { - if let Some(input_group) = rel_node.typ.extract_group() { - let input_group = self.reduce_group(input_group); - let group_id = self.reduce_group(group_id); - self.merge_group_inner(input_group, group_id); - return None; - } - let reduced_group_id = self.reduce_group(group_id); - let (returned_group_id, expr_id) = self - .add_new_group_expr_inner(rel_node, Some(reduced_group_id)) - .unwrap(); - assert_eq!(returned_group_id, reduced_group_id); - self.verify_integrity(); - Some(expr_id) - } - fn reduce_group(&self, group_id: GroupId) -> GroupId { self.merged_group_mapping[&group_id] } @@ -360,8 +523,10 @@ impl Memo { Ok((group_id, expr_id)) } - /// This is inefficient: usually the optimizer should have a MemoRef instead of passing the full rel node. - pub fn get_expr_info(&self, rel_node: RelNodeRef) -> (GroupId, ExprId) { + /// This is inefficient: usually the optimizer should have a MemoRef instead of passing the full rel node. Should + /// be only used for debugging purpose. + #[cfg(test)] + pub(crate) fn get_expr_info(&self, rel_node: RelNodeRef) -> (GroupId, ExprId) { let children_group_ids = rel_node .children .iter() @@ -435,159 +600,6 @@ impl Memo { self.merged_group_mapping.insert(group_id, group_id); } - /// Get the group id of an expression. - /// The group id is volatile, depending on whether the groups are merged. - pub fn get_group_id(&self, mut expr_id: ExprId) -> GroupId { - while let Some(new_expr_id) = self.dup_expr_mapping.get(&expr_id) { - expr_id = *new_expr_id; - } - *self - .expr_id_to_group_id - .get(&expr_id) - .expect("expr not found in group mapping") - } - - /// Get the memoized representation of a node, only for debugging purpose - pub fn get_expr_memoed(&self, mut expr_id: ExprId) -> RelMemoNodeRef { - while let Some(new_expr_id) = self.dup_expr_mapping.get(&expr_id) { - expr_id = *new_expr_id; - } - self.expr_id_to_expr_node - .get(&expr_id) - .expect("expr not found in expr mapping") - .clone() - } - - pub fn get_all_exprs_in_group(&self, group_id: GroupId) -> Vec { - let group_id = self.reduce_group(group_id); - let group = self.groups.get(&group_id).expect("group not found"); - let mut exprs = group.group_exprs.iter().copied().collect_vec(); - exprs.sort(); - exprs - } - - pub(crate) fn get_all_group_ids(&self) -> Vec { - let mut ids = self.groups.keys().copied().collect_vec(); - ids.sort(); - ids - } - - pub(crate) fn get_group_info(&self, group_id: GroupId) -> GroupInfo { - let group_id = self.reduce_group(group_id); - self.groups.get(&group_id).as_ref().unwrap().info.clone() - } - - pub(crate) fn get_group(&self, group_id: GroupId) -> &Group { - let group_id = self.reduce_group(group_id); - self.groups.get(&group_id).as_ref().unwrap() - } - - pub fn update_group_info(&mut self, group_id: GroupId, group_info: GroupInfo) { - if let Winner::Full(WinnerInfo { - total_weighted_cost, - expr_id, - .. - }) = &group_info.winner - { - assert!( - *total_weighted_cost != 0.0, - "{}", - self.expr_id_to_expr_node[expr_id] - ); - } - let grp = self.groups.get_mut(&group_id); - grp.unwrap().info = group_info; - } - - /// Get all bindings of a predicate group. Will panic if the group contains more than one bindings. - pub fn get_predicate_binding(&self, group_id: GroupId) -> Option> { - self.get_predicate_binding_group_inner(group_id, true) - } - - /// Get all bindings of a predicate group. Will panic if the group contains more than one bindings. - pub fn try_get_predicate_binding(&self, group_id: GroupId) -> Option> { - self.get_predicate_binding_group_inner(group_id, false) - } - - fn get_predicate_binding_expr_inner( - &self, - expr_id: ExprId, - panic_on_invalid_group: bool, - ) -> Option> { - let expr = self.expr_id_to_expr_node[&expr_id].clone(); - let mut children = Vec::with_capacity(expr.children.len()); - for child in expr.children.iter() { - if let Some(child) = - self.get_predicate_binding_group_inner(*child, panic_on_invalid_group) - { - children.push(child); - } else { - return None; - } - } - Some(Arc::new(RelNode { - typ: expr.typ.clone(), - data: expr.data.clone(), - children, - })) - } - - fn get_predicate_binding_group_inner( - &self, - group_id: GroupId, - panic_on_invalid_group: bool, - ) -> Option> { - let exprs = &self.groups[&group_id].group_exprs; - match exprs.len() { - 0 => None, - 1 => self.get_predicate_binding_expr_inner( - *exprs.iter().next().unwrap(), - panic_on_invalid_group, - ), - len => { - if panic_on_invalid_group { - panic!("group {group_id} has {len} expressions") - } else { - None - } - } - } - } - - pub fn get_best_group_binding( - &self, - group_id: GroupId, - mut post_process: impl FnMut(Arc>, GroupId, &WinnerInfo), - ) -> Result> { - self.get_best_group_binding_inner(group_id, &mut post_process) - } - - fn get_best_group_binding_inner( - &self, - group_id: GroupId, - post_process: &mut impl FnMut(Arc>, GroupId, &WinnerInfo), - ) -> Result> { - let info = self.get_group_info(group_id); - if let Winner::Full(info @ WinnerInfo { expr_id, .. }) = info.winner { - let expr = self.expr_id_to_expr_node[&expr_id].clone(); - let mut children = Vec::with_capacity(expr.children.len()); - for child in &expr.children { - children.push( - self.get_best_group_binding_inner(*child, post_process) - .with_context(|| format!("when processing expr {}", expr_id))?, - ); - } - let node = Arc::new(RelNode { - typ: expr.typ.clone(), - children, - data: expr.data.clone(), - }); - post_process(node.clone(), group_id, &info); - return Ok(node); - } - bail!("no best group binding for group {}", group_id) - } - pub fn clear_winner(&mut self) { for group in self.groups.values_mut() { group.info.winner = Winner::Unknown; @@ -705,7 +717,7 @@ mod tests { #[test] fn group_merge_1() { - let mut memo = Memo::new(Arc::new([])); + let mut memo = NaiveMemo::new(Arc::new([])); let (group_id, _) = memo.add_new_expr(join(scan("t1"), scan("t2"), expr(Value::Bool(true))).into()); memo.add_expr_to_group( @@ -717,7 +729,7 @@ mod tests { #[test] fn group_merge_2() { - let mut memo = Memo::new(Arc::new([])); + let mut memo = NaiveMemo::new(Arc::new([])); let (group_id_1, _) = memo.add_new_expr( project( join(scan("t1"), scan("t2"), expr(Value::Bool(true))), @@ -737,7 +749,7 @@ mod tests { #[test] fn group_merge_3() { - let mut memo = Memo::new(Arc::new([])); + let mut memo = NaiveMemo::new(Arc::new([])); let expr1 = Arc::new(project(scan("t1"), list(vec![expr(Value::Int64(1))]))); let expr2 = Arc::new(project(scan("t1-alias"), list(vec![expr(Value::Int64(1))]))); memo.add_new_expr(expr1.clone()); @@ -752,7 +764,7 @@ mod tests { #[test] fn group_merge_4() { - let mut memo = Memo::new(Arc::new([])); + let mut memo = NaiveMemo::new(Arc::new([])); let expr1 = Arc::new(project( project(scan("t1"), list(vec![expr(Value::Int64(1))])), list(vec![expr(Value::Int64(2))]), @@ -776,7 +788,7 @@ mod tests { #[test] fn group_merge_5() { - let mut memo = Memo::new(Arc::new([])); + let mut memo = NaiveMemo::new(Arc::new([])); let expr1 = Arc::new(project( project(scan("t1"), list(vec![expr(Value::Int64(1))])), list(vec![expr(Value::Int64(2))]), diff --git a/optd-core/src/cascades/optimizer.rs b/optd-core/src/cascades/optimizer.rs index c25d7ba8..6fd2963d 100644 --- a/optd-core/src/cascades/optimizer.rs +++ b/optd-core/src/cascades/optimizer.rs @@ -17,9 +17,9 @@ use crate::{ }; use super::{ - memo::{GroupInfo, RelMemoNodeRef}, + memo::{GroupInfo, Memo, RelMemoNodeRef}, tasks::OptimizeGroupTask, - Memo, Task, + NaiveMemo, Task, }; pub type RuleId = usize; @@ -40,7 +40,7 @@ pub struct OptimizerProperties { } pub struct CascadesOptimizer { - memo: Memo, + memo: NaiveMemo, pub(super) tasks: VecDeque>>, explored_group: HashSet, explored_expr: HashSet, @@ -101,7 +101,7 @@ impl CascadesOptimizer { ) -> Self { let tasks = VecDeque::new(); let property_builders: Arc<[_]> = property_builders.into(); - let memo = Memo::new(property_builders.clone()); + let memo = NaiveMemo::new(property_builders.clone()); Self { memo, tasks, @@ -139,7 +139,7 @@ impl CascadesOptimizer { pub fn dump(&self) { for group_id in self.memo.get_all_group_ids() { - let winner_str = match self.memo.get_group_info(group_id).winner { + let winner_str = match &self.memo.get_group_info(group_id).winner { Winner::Impossible => "winner=".to_string(), Winner::Unknown => "winner=".to_string(), Winner::Full(winner) => { @@ -175,7 +175,7 @@ impl CascadesOptimizer { /// Clear the memo table and all optimizer states. pub fn step_clear(&mut self) { - self.memo = Memo::new(self.property_builders.clone()); + self.memo = NaiveMemo::new(self.property_builders.clone()); self.fired_rules.clear(); self.explored_group.clear(); self.explored_expr.clear(); @@ -274,18 +274,13 @@ impl CascadesOptimizer { if let Some(group_id) = T::extract_group(&root_rel.typ) { return group_id; } - let (group_id, _) = self.get_expr_info(root_rel); - group_id + panic!("This function is deprecated -- you should only pass group id instead of a full expression to this function.") } pub(super) fn get_all_exprs_in_group(&self, group_id: GroupId) -> Vec { self.memo.get_all_exprs_in_group(group_id) } - pub(super) fn get_expr_info(&self, expr: RelNodeRef) -> (GroupId, ExprId) { - self.memo.get_expr_info(expr) - } - pub fn add_new_expr(&mut self, rel_node: RelNodeRef) -> (GroupId, ExprId) { self.memo.add_new_expr(rel_node) } @@ -298,7 +293,7 @@ impl CascadesOptimizer { self.memo.add_expr_to_group(rel_node, group_id) } - pub(super) fn get_group_info(&self, group_id: GroupId) -> GroupInfo { + pub(super) fn get_group_info(&self, group_id: GroupId) -> &GroupInfo { self.memo.get_group_info(group_id) } @@ -367,7 +362,7 @@ impl CascadesOptimizer { .insert(rule_id); } - pub fn memo(&self) -> &Memo { + pub fn memo(&self) -> &NaiveMemo { &self.memo } } diff --git a/optd-datafusion-repr/src/memo_ext.rs b/optd-datafusion-repr/src/memo_ext.rs index 0075fda1..5ff4a1fa 100644 --- a/optd-datafusion-repr/src/memo_ext.rs +++ b/optd-datafusion-repr/src/memo_ext.rs @@ -34,8 +34,8 @@ pub trait MemoExt { fn enumerate_join_order(&self, entry: GroupId) -> Vec; } -fn enumerate_join_order_expr_inner( - memo: &Memo, +fn enumerate_join_order_expr_inner + ?Sized>( + memo: &M, current: ExprId, visited: &mut HashMap>, ) -> Vec { @@ -91,8 +91,8 @@ fn enumerate_join_order_expr_inner( } } -fn enumerate_join_order_group_inner( - memo: &Memo, +fn enumerate_join_order_group_inner + ?Sized>( + memo: &M, current: GroupId, visited: &mut HashMap>, ) -> Vec { @@ -115,7 +115,7 @@ fn enumerate_join_order_group_inner( res } -impl MemoExt for Memo { +impl> MemoExt for M { fn enumerate_join_order(&self, entry: GroupId) -> Vec { let mut visited = HashMap::new(); enumerate_join_order_group_inner(self, entry, &mut visited) @@ -124,7 +124,10 @@ impl MemoExt for Memo { #[cfg(test)] mod tests { - use optd_core::rel_node::{RelNode, Value}; + use optd_core::{ + cascades::NaiveMemo, + rel_node::{RelNode, Value}, + }; use crate::plan_nodes::{ ConstantExpr, ExprList, JoinType, LogicalJoin, LogicalProjection, PlanNode, @@ -134,7 +137,7 @@ mod tests { #[test] fn enumerate_join_orders() { - let mut memo = Memo::::new(Arc::new([])); + let mut memo = NaiveMemo::::new(Arc::new([])); let (group, _) = memo.add_new_expr( LogicalJoin::new( LogicalScan::new("t1".to_string()).into_plan_node(), diff --git a/optd-datafusion-repr/src/plan_nodes.rs b/optd-datafusion-repr/src/plan_nodes.rs index 782d1541..c0438de0 100644 --- a/optd-datafusion-repr/src/plan_nodes.rs +++ b/optd-datafusion-repr/src/plan_nodes.rs @@ -19,7 +19,7 @@ use std::sync::Arc; use arrow_schema::DataType; use itertools::Itertools; use optd_core::{ - cascades::{CascadesOptimizer, GroupId}, + cascades::GroupId, rel_node::{RelNode, RelNodeMeta, RelNodeMetaMap, RelNodeRef, RelNodeTyp}, }; @@ -40,8 +40,6 @@ pub use scan::{LogicalScan, PhysicalScan}; pub use sort::{LogicalSort, PhysicalSort}; pub use subquery::{DependentJoin, ExternColumnRefExpr, RawDependentJoin}; // Add missing import -use crate::properties::schema::{Schema, SchemaPropertyBuilder}; - /// OptRelNodeTyp FAQ: /// - The define_plan_node!() macro defines what the children of each join node are #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -242,11 +240,6 @@ impl PlanNode { self.0.typ.clone() } - pub fn schema(&self, optimizer: &CascadesOptimizer) -> Schema { - let group_id = optimizer.resolve_group_id(self.0.clone()); - optimizer.get_property_by_group::(group_id, 0) - } - pub fn from_group(rel_node: OptRelNodeRef) -> Self { Self(rel_node) }