use std::hash::BuildHasher;
use hashbrown::hash_map::RawEntryMut;
use polars_core::CHEAP_SERIES_HASH_LIMIT;
use polars_core::config::verbose;
use polars_core::prelude::PlHashMap;
use polars_core::schema::Schema;
use polars_error::PolarsResult;
use polars_utils::aliases::PlFixedStateQuality;
use polars_utils::arena::{Arena, Node};
use polars_utils::format_pl_smallstr;
use polars_utils::hashing::_boost_hash_combine;
use polars_utils::pl_str::PlSmallStr;
use polars_utils::vec::CapacityByFactor;
use crate::constants::CSE_REPLACED;
use crate::plans::aexpr::is_inherently_nondeterministic_top_level;
use crate::plans::visitor::{
IRNode, IRNodeArena, RewriteRecursion, RewritingVisitor, TreeWalker as _, VisitRecursion,
Visitor,
};
use crate::plans::{AExpr, ExprIR, IR, IRBuilder, IRFunctionExpr, LiteralValue, OutputName};
use crate::prelude::ProjectionOptions;
use crate::prelude::visitor::AexprNode;
type Accepted = Option<(VisitRecursion, bool)>;
const REFUSE_NO_MEMBER: Accepted = Some((VisitRecursion::Continue, false));
const REFUSE_ALLOW_MEMBER: Accepted = Some((VisitRecursion::Continue, true));
const REFUSE_SKIP: Accepted = Some((VisitRecursion::Skip, false));
const ACCEPT: Accepted = None;
fn refused_by_cse_due_to_nondeterminism(ae: &AExpr) -> bool {
if matches!(
ae,
AExpr::AnonymousFunction { .. } | AExpr::AnonymousAgg { .. }
) {
return false;
}
is_inherently_nondeterministic_top_level(ae)
}
#[derive(Debug, Clone)]
struct ProjectionExprs {
expr: Vec<ExprIR>,
common_sub_offset: usize,
}
impl ProjectionExprs {
fn default_exprs(&self) -> &[ExprIR] {
&self.expr[..self.expr.len() - self.common_sub_offset]
}
fn cse_exprs(&self) -> &[ExprIR] {
&self.expr[self.expr.len() - self.common_sub_offset..]
}
fn new_with_cse(expr: Vec<ExprIR>, common_sub_offset: usize) -> Self {
Self {
expr,
common_sub_offset,
}
}
}
#[derive(Clone, Debug)]
pub(super) struct Identifier {
inner: Option<u64>,
last_node: Option<AexprNode>,
hb: PlFixedStateQuality,
}
impl Identifier {
fn new() -> Self {
Self {
inner: None,
last_node: None,
hb: PlFixedStateQuality::with_seed(0),
}
}
fn hash(&self) -> u64 {
self.inner.unwrap_or(0)
}
fn ae_node(&self) -> AexprNode {
self.last_node.unwrap()
}
fn is_equal(&self, other: &Self, arena: &Arena<AExpr>) -> bool {
self.inner == other.inner
&& self.last_node.map(|v| v.hashable_and_cmp(arena))
== other.last_node.map(|v| v.hashable_and_cmp(arena))
}
fn is_valid(&self) -> bool {
self.inner.is_some()
}
fn materialize(&self) -> PlSmallStr {
format_pl_smallstr!("{}{:#x}", CSE_REPLACED, self.materialized_hash())
}
fn materialized_hash(&self) -> u64 {
self.inner.unwrap_or(0)
}
fn combine(&mut self, other: &Identifier) {
let inner = match (self.inner, other.inner) {
(Some(l), Some(r)) => _boost_hash_combine(l, r),
(None, Some(r)) => r,
(Some(l), None) => l,
_ => return,
};
self.inner = Some(inner);
}
fn add_ae_node(&self, ae: &AexprNode, arena: &Arena<AExpr>) -> Self {
let hashed = self.hb.hash_one(ae.to_aexpr(arena));
let inner = Some(
self.inner
.map_or(hashed, |l| _boost_hash_combine(l, hashed)),
);
Self {
inner,
last_node: Some(*ae),
hb: self.hb.clone(),
}
}
}
#[derive(Default)]
struct IdentifierMap<V> {
inner: PlHashMap<Identifier, V>,
}
impl<V> IdentifierMap<V> {
fn get(&self, id: &Identifier, arena: &Arena<AExpr>) -> Option<&V> {
self.inner
.raw_entry()
.from_hash(id.hash(), |k| k.is_equal(id, arena))
.map(|(_k, v)| v)
}
fn entry<'a, F: FnOnce() -> V>(
&'a mut self,
id: Identifier,
v: F,
arena: &Arena<AExpr>,
) -> &'a mut V {
let h = id.hash();
match self
.inner
.raw_entry_mut()
.from_hash(h, |k| k.is_equal(&id, arena))
{
RawEntryMut::Occupied(entry) => entry.into_mut(),
RawEntryMut::Vacant(entry) => {
let (_, v) = entry.insert_with_hasher(h, id, v(), |id| id.hash());
v
},
}
}
fn insert(&mut self, id: Identifier, v: V, arena: &Arena<AExpr>) {
self.entry(id, || v, arena);
}
fn iter(&self) -> impl Iterator<Item = (&Identifier, &V)> {
self.inner.iter()
}
}
#[derive(Default)]
pub struct NaiveExprMerger {
node_to_uniq_id: PlHashMap<Node, u32>,
uniq_id_to_node: Vec<Node>,
identifier_to_uniq_id: IdentifierMap<u32>,
arg_stack: Vec<Option<Identifier>>,
}
impl NaiveExprMerger {
pub fn add_expr(&mut self, node: Node, arena: &Arena<AExpr>) {
AexprNode::new(node).visit(self, arena).unwrap();
}
pub fn add_and_get_uniq_id(&mut self, node: Node, arena: &Arena<AExpr>) -> u32 {
let aexpr_node = AexprNode::new(node);
aexpr_node.visit(self, arena).unwrap();
*self.node_to_uniq_id.get(&node).unwrap()
}
pub fn get_uniq_id(&self, node: Node) -> Option<u32> {
self.node_to_uniq_id.get(&node).copied()
}
pub fn get_node(&self, uniq_id: u32) -> Option<Node> {
self.uniq_id_to_node.get(uniq_id as usize).copied()
}
}
impl Visitor for NaiveExprMerger {
type Node = AexprNode;
type Arena = Arena<AExpr>;
fn pre_visit(
&mut self,
_node: &Self::Node,
_arena: &Self::Arena,
) -> PolarsResult<VisitRecursion> {
self.arg_stack.push(None);
Ok(VisitRecursion::Continue)
}
fn post_visit(
&mut self,
node: &Self::Node,
arena: &Self::Arena,
) -> PolarsResult<VisitRecursion> {
let mut identifier = Identifier::new();
while let Some(Some(arg)) = self.arg_stack.pop() {
identifier.combine(&arg);
}
identifier = identifier.add_ae_node(node, arena);
let uniq_id = *self.identifier_to_uniq_id.entry(
identifier,
|| {
let uniq_id = self.uniq_id_to_node.len() as u32;
self.uniq_id_to_node.push(node.node());
uniq_id
},
arena,
);
self.node_to_uniq_id.insert(node.node(), uniq_id);
Ok(VisitRecursion::Continue)
}
}
type SubExprCount = IdentifierMap<(Node, u32)>;
type IdentifierArray = Vec<(usize, Identifier)>;
#[derive(Debug)]
enum VisitRecord {
Entered(usize),
SubExprId(Identifier, bool),
}
fn skip_pre_visit(ae: &AExpr, is_groupby: bool, element_wise_select_only: bool) -> bool {
match ae {
#[cfg(feature = "dynamic_group_by")]
AExpr::Rolling { .. } => true,
AExpr::Over { .. } => true,
#[cfg(feature = "dtype-struct")]
AExpr::Ternary { .. } => is_groupby,
ae => {
if element_wise_select_only {
if is_groupby {
true
} else {
!ae.is_elementwise_top_level()
}
} else {
false
}
},
}
}
struct ExprIdentifierVisitor<'a> {
se_count: &'a mut SubExprCount,
name_validation: &'a mut PlHashMap<u64, u32>,
identifier_array: &'a mut IdentifierArray,
pre_visit_idx: usize,
post_visit_idx: usize,
visit_stack: &'a mut Vec<VisitRecord>,
id_array_offset: usize,
has_sub_expr: bool,
is_group_by: bool,
element_wise_only: bool,
}
impl ExprIdentifierVisitor<'_> {
fn new<'a>(
se_count: &'a mut SubExprCount,
identifier_array: &'a mut IdentifierArray,
visit_stack: &'a mut Vec<VisitRecord>,
is_group_by: bool,
name_validation: &'a mut PlHashMap<u64, u32>,
element_wise_select_only: bool,
) -> ExprIdentifierVisitor<'a> {
let id_array_offset = identifier_array.len();
ExprIdentifierVisitor {
se_count,
name_validation,
identifier_array,
pre_visit_idx: 0,
post_visit_idx: 0,
visit_stack,
id_array_offset,
has_sub_expr: false,
is_group_by,
element_wise_only: element_wise_select_only,
}
}
fn pop_until_entered(&mut self) -> (usize, Identifier, bool) {
let mut id = Identifier::new();
let mut is_valid_accumulated = true;
while let Some(item) = self.visit_stack.pop() {
match item {
VisitRecord::Entered(idx) => return (idx, id, is_valid_accumulated),
VisitRecord::SubExprId(s, valid) => {
id.combine(&s);
is_valid_accumulated &= valid
},
}
}
unreachable!()
}
fn accept_node_post_visit(&self, ae: &AExpr) -> Accepted {
match ae {
#[cfg(feature = "dynamic_group_by")]
AExpr::Rolling { .. } => REFUSE_SKIP,
AExpr::Over { .. } => REFUSE_SKIP,
AExpr::Literal(LiteralValue::Scalar(sc)) if sc.is_null() => REFUSE_NO_MEMBER,
AExpr::Literal(s) => {
match s {
LiteralValue::Series(s) => {
let dtype = s.dtype();
let allow = !(dtype.is_nested() | dtype.is_object());
if s.len() < CHEAP_SERIES_HASH_LIMIT && allow {
REFUSE_ALLOW_MEMBER
} else {
REFUSE_NO_MEMBER
}
},
_ => REFUSE_ALLOW_MEMBER,
}
},
AExpr::Column(_) => REFUSE_ALLOW_MEMBER,
AExpr::Len => {
if self.is_group_by {
REFUSE_NO_MEMBER
} else {
REFUSE_ALLOW_MEMBER
}
},
ae if refused_by_cse_due_to_nondeterminism(ae) => REFUSE_NO_MEMBER,
#[cfg(feature = "rolling_window")]
AExpr::Function {
function: IRFunctionExpr::RollingExpr { .. },
..
} => REFUSE_NO_MEMBER,
_ => {
if self.is_group_by {
if !ae.is_elementwise_top_level() {
return REFUSE_NO_MEMBER;
}
match ae {
AExpr::Cast { .. } => REFUSE_ALLOW_MEMBER,
_ => ACCEPT,
}
} else {
ACCEPT
}
},
}
}
}
impl Visitor for ExprIdentifierVisitor<'_> {
type Node = AexprNode;
type Arena = Arena<AExpr>;
fn pre_visit(
&mut self,
node: &Self::Node,
arena: &Self::Arena,
) -> PolarsResult<VisitRecursion> {
if skip_pre_visit(
node.to_aexpr(arena),
self.is_group_by,
self.element_wise_only,
) {
self.visit_stack
.push(VisitRecord::SubExprId(Identifier::new(), false));
return Ok(VisitRecursion::Skip);
}
self.visit_stack
.push(VisitRecord::Entered(self.pre_visit_idx));
self.pre_visit_idx += 1;
self.identifier_array
.push((self.id_array_offset, Identifier::new()));
Ok(VisitRecursion::Continue)
}
fn post_visit(
&mut self,
node: &Self::Node,
arena: &Self::Arena,
) -> PolarsResult<VisitRecursion> {
let ae = node.to_aexpr(arena);
self.post_visit_idx += 1;
let (pre_visit_idx, sub_expr_id, is_valid_accumulated) = self.pop_until_entered();
let id: Identifier = sub_expr_id.add_ae_node(node, arena);
if !is_valid_accumulated {
self.identifier_array[pre_visit_idx + self.id_array_offset].0 = self.post_visit_idx;
self.visit_stack.push(VisitRecord::SubExprId(id, false));
return Ok(VisitRecursion::Continue);
}
if let Some((recurse, local_is_valid)) = self.accept_node_post_visit(ae) {
self.identifier_array[pre_visit_idx + self.id_array_offset].0 = self.post_visit_idx;
self.visit_stack
.push(VisitRecord::SubExprId(id, local_is_valid));
return Ok(recurse);
}
self.identifier_array[pre_visit_idx + self.id_array_offset] =
(self.post_visit_idx, id.clone());
self.visit_stack
.push(VisitRecord::SubExprId(id.clone(), true));
let mat_h = id.materialized_hash();
let (_, se_count) = self.se_count.entry(id, || (node.node(), 0), arena);
*se_count += 1;
*self.name_validation.entry(mat_h).or_insert(0) += 1;
self.has_sub_expr |= *se_count > 1;
Ok(VisitRecursion::Continue)
}
}
struct CommonSubExprRewriter<'a> {
sub_expr_map: &'a SubExprCount,
identifier_array: &'a IdentifierArray,
replaced_identifiers: &'a mut IdentifierMap<()>,
max_post_visit_idx: usize,
visited_idx: usize,
id_array_offset: usize,
rewritten: bool,
is_group_by: bool,
is_element_wise_select_only: bool,
}
impl<'a> CommonSubExprRewriter<'a> {
fn new(
sub_expr_map: &'a SubExprCount,
identifier_array: &'a IdentifierArray,
replaced_identifiers: &'a mut IdentifierMap<()>,
id_array_offset: usize,
is_group_by: bool,
is_element_wise_select_only: bool,
) -> Self {
Self {
sub_expr_map,
identifier_array,
replaced_identifiers,
max_post_visit_idx: 0,
visited_idx: 0,
id_array_offset,
rewritten: false,
is_group_by,
is_element_wise_select_only,
}
}
}
impl RewritingVisitor for CommonSubExprRewriter<'_> {
type Node = AexprNode;
type Arena = Arena<AExpr>;
fn pre_visit(
&mut self,
ae_node: &Self::Node,
arena: &mut Self::Arena,
) -> PolarsResult<RewriteRecursion> {
let ae = ae_node.to_aexpr(arena);
if self.visited_idx + self.id_array_offset >= self.identifier_array.len()
|| self.max_post_visit_idx
> self.identifier_array[self.visited_idx + self.id_array_offset].0
|| skip_pre_visit(ae, self.is_group_by, self.is_element_wise_select_only)
{
return Ok(RewriteRecursion::Stop);
}
let id = &self.identifier_array[self.visited_idx + self.id_array_offset].1;
if !id.is_valid() {
self.visited_idx += 1;
let recurse = if ae_node.is_leaf(arena) {
RewriteRecursion::Stop
} else {
RewriteRecursion::NoMutateAndContinue
};
return Ok(recurse);
}
let Some((_, count)) = self.sub_expr_map.get(id, arena) else {
self.visited_idx += 1;
return Ok(RewriteRecursion::NoMutateAndContinue);
};
if *count > 1 {
self.replaced_identifiers.insert(id.clone(), (), arena);
Ok(RewriteRecursion::MutateAndStop)
} else {
self.visited_idx += 1;
Ok(RewriteRecursion::NoMutateAndContinue)
}
}
fn mutate(
&mut self,
mut node: Self::Node,
arena: &mut Self::Arena,
) -> PolarsResult<Self::Node> {
let (post_visit_count, id) =
&self.identifier_array[self.visited_idx + self.id_array_offset];
self.visited_idx += 1;
if *post_visit_count < self.max_post_visit_idx {
return Ok(node);
}
self.max_post_visit_idx = *post_visit_count;
while self.visited_idx < self.identifier_array.len() - self.id_array_offset
&& *post_visit_count > self.identifier_array[self.visited_idx + self.id_array_offset].0
{
self.visited_idx += 1;
}
debug_assert_eq!(
node.hashable_and_cmp(arena),
id.ae_node().hashable_and_cmp(arena)
);
let name = id.materialize();
node.assign(AExpr::col(name), arena);
self.rewritten = true;
Ok(node)
}
}
pub(crate) struct CommonSubExprOptimizer {
se_count: SubExprCount,
id_array: IdentifierArray,
id_array_offsets: Vec<u32>,
replaced_identifiers: IdentifierMap<()>,
visit_stack: Vec<VisitRecord>,
name_validation: PlHashMap<u64, u32>,
element_wise_select_only: bool,
}
impl CommonSubExprOptimizer {
pub(crate) fn new(element_wise_select_only: bool) -> Self {
Self {
se_count: Default::default(),
id_array: Default::default(),
visit_stack: Default::default(),
id_array_offsets: Default::default(),
replaced_identifiers: Default::default(),
name_validation: Default::default(),
element_wise_select_only,
}
}
fn visit_expression(
&mut self,
ae_node: AexprNode,
is_group_by: bool,
expr_arena: &mut Arena<AExpr>,
element_wise_select_only: bool,
) -> PolarsResult<(usize, bool)> {
let mut visitor = ExprIdentifierVisitor::new(
&mut self.se_count,
&mut self.id_array,
&mut self.visit_stack,
is_group_by,
&mut self.name_validation,
element_wise_select_only,
);
ae_node.visit(&mut visitor, expr_arena).map(|_| ())?;
Ok((visitor.id_array_offset, visitor.has_sub_expr))
}
fn mutate_expression(
&mut self,
ae_node: AexprNode,
id_array_offset: usize,
is_group_by: bool,
expr_arena: &mut Arena<AExpr>,
element_wise_select_only: bool,
) -> PolarsResult<(AexprNode, bool)> {
let mut rewriter = CommonSubExprRewriter::new(
&self.se_count,
&self.id_array,
&mut self.replaced_identifiers,
id_array_offset,
is_group_by,
element_wise_select_only,
);
ae_node
.rewrite(&mut rewriter, expr_arena)
.map(|out| (out, rewriter.rewritten))
}
fn find_cse(
&mut self,
expr: &[ExprIR],
expr_arena: &mut Arena<AExpr>,
id_array_offsets: &mut Vec<u32>,
is_group_by: bool,
schema: &Schema,
element_wise_select_only: bool,
) -> PolarsResult<Option<ProjectionExprs>> {
let mut has_sub_expr = false;
for e in expr {
self.visit_stack.clear();
let ae_node = AexprNode::new(e.node());
let (id_array_offset, this_expr_has_se) =
self.visit_expression(ae_node, is_group_by, expr_arena, element_wise_select_only)?;
id_array_offsets.push(id_array_offset as u32);
has_sub_expr |= this_expr_has_se;
}
for (id, (_, count)) in self.se_count.iter() {
let mat_h = id.materialized_hash();
let valid = if let Some(name_count) = self.name_validation.get(&mat_h) {
*name_count == *count
} else {
false
};
if !valid {
if verbose() {
eprintln!(
"materialized names collided in common subexpression elimination.\n backtrace and run without CSE"
)
}
return Ok(None);
}
}
if has_sub_expr {
let mut new_expr = Vec::with_capacity_by_factor(expr.len(), 1.3);
for (e, offset) in expr.iter().zip(id_array_offsets.iter()) {
let ae_node = AexprNode::new(e.node());
let (out, rewritten) = self.mutate_expression(
ae_node,
*offset as usize,
is_group_by,
expr_arena,
element_wise_select_only,
)?;
let out_node = out.node();
let mut out_e = e.clone();
let new_node = if !rewritten {
out_e
} else {
out_e.set_node(out_node);
let mut scratch = vec![];
let mut stack = vec![(e.node(), out_node)];
while let Some((original, new)) = stack.pop() {
if original == new {
continue;
}
scratch.clear();
let aes = expr_arena.get_disjoint_mut([original, new]);
if std::mem::discriminant(aes[0]) != std::mem::discriminant(aes[1]) {
continue;
}
aes[0].inputs_rev(&mut scratch);
let offset = scratch.len();
aes[1].inputs_rev(&mut scratch);
if scratch.len() != offset * 2 {
continue;
}
for i in 0..scratch.len() / 2 {
stack.push((scratch[i], scratch[i + offset]));
}
match expr_arena.get_disjoint_mut([original, new]) {
[
AExpr::Function {
input: input_original,
..
},
AExpr::Function {
input: input_new, ..
},
] => {
for (new, original) in input_new.iter_mut().zip(input_original) {
new.set_alias(original.output_name().clone());
}
},
[
AExpr::AnonymousFunction {
input: input_original,
..
},
AExpr::AnonymousFunction {
input: input_new, ..
},
] => {
for (new, original) in input_new.iter_mut().zip(input_original) {
new.set_alias(original.output_name().clone());
}
},
_ => {},
}
}
if !e.has_alias() {
let name = ae_node.to_field(schema, expr_arena)?.name;
out_e.set_alias(name.clone());
}
out_e
};
new_expr.push(new_node)
}
for id in self.replaced_identifiers.inner.keys() {
let (node, _count) = self.se_count.get(id, expr_arena).unwrap();
let name = id.materialize();
let out_e = ExprIR::new(*node, OutputName::Alias(name));
new_expr.push(out_e)
}
let expr =
ProjectionExprs::new_with_cse(new_expr, self.replaced_identifiers.inner.len());
Ok(Some(expr))
} else {
Ok(None)
}
}
}
impl RewritingVisitor for CommonSubExprOptimizer {
type Node = IRNode;
type Arena = IRNodeArena;
fn pre_visit(
&mut self,
node: &Self::Node,
arena: &mut Self::Arena,
) -> PolarsResult<RewriteRecursion> {
use IR::*;
Ok(match node.to_alp(&arena.0) {
Select { .. } | HStack { .. } | GroupBy { .. } => RewriteRecursion::MutateAndContinue,
_ => RewriteRecursion::NoMutateAndContinue,
})
}
fn mutate(&mut self, node: Self::Node, arena: &mut Self::Arena) -> PolarsResult<Self::Node> {
let mut id_array_offsets = std::mem::take(&mut self.id_array_offsets);
self.se_count.inner.clear();
self.name_validation.clear();
self.id_array.clear();
id_array_offsets.clear();
self.replaced_identifiers.inner.clear();
let arena_idx = node.node();
let alp = arena.0.get(arena_idx);
match alp {
IR::Select {
input,
expr,
schema,
options,
} => {
let input_schema = arena.0.get(*input).schema(&arena.0);
if let Some(expr) = self.find_cse(
expr,
&mut arena.1,
&mut id_array_offsets,
false,
input_schema.as_ref().as_ref(),
self.element_wise_select_only,
)? {
let schema = schema.clone();
let options = *options;
let lp = IRBuilder::new(*input, &mut arena.1, &mut arena.0)
.with_columns(
expr.cse_exprs().to_vec(),
ProjectionOptions {
run_parallel: options.run_parallel,
duplicate_check: options.duplicate_check,
should_broadcast: false,
},
)
.build();
let input = arena.0.add(lp);
let lp = IR::Select {
input,
expr: expr.default_exprs().to_vec(),
schema,
options,
};
arena.0.replace(arena_idx, lp);
}
},
IR::HStack {
input,
exprs,
schema,
options,
} => {
let input_schema = arena.0.get(*input).schema(&arena.0);
if let Some(exprs) = self.find_cse(
exprs,
&mut arena.1,
&mut id_array_offsets,
false,
input_schema.as_ref().as_ref(),
self.element_wise_select_only,
)? {
let schema = schema.clone();
let options = *options;
let input = *input;
let lp = IRBuilder::new(input, &mut arena.1, &mut arena.0)
.with_columns(
exprs.cse_exprs().to_vec(),
ProjectionOptions {
run_parallel: options.run_parallel,
duplicate_check: options.duplicate_check,
should_broadcast: false,
},
)
.with_columns(exprs.default_exprs().to_vec(), options)
.build();
let input = arena.0.add(lp);
let lp = IR::SimpleProjection {
input,
columns: schema,
};
arena.0.replace(arena_idx, lp);
}
},
IR::GroupBy {
input,
keys,
aggs,
options,
maintain_order,
apply,
schema,
} if !self.element_wise_select_only => {
let input_schema = arena.0.get(*input).schema(&arena.0);
if let Some(aggs) = self.find_cse(
aggs,
&mut arena.1,
&mut id_array_offsets,
true,
input_schema.as_ref().as_ref(),
self.element_wise_select_only,
)? {
let keys = keys.clone();
let options = options.clone();
let schema = schema.clone();
let apply = apply.clone();
let maintain_order = *maintain_order;
let input = *input;
let lp = IRBuilder::new(input, &mut arena.1, &mut arena.0)
.with_columns(aggs.cse_exprs().to_vec(), Default::default())
.build();
let input = arena.0.add(lp);
let lp = IR::GroupBy {
input,
keys,
aggs: aggs.default_exprs().to_vec(),
options,
schema,
maintain_order,
apply,
};
arena.0.replace(arena_idx, lp);
}
},
_ => {},
}
self.id_array_offsets = id_array_offsets;
Ok(node)
}
}