use std::collections::{HashMap, HashSet};
use linkrs_core::types::expr::ExpressionId;
use crate::planning::plan::factorization::{FGroupPos, FactorizedSchema, INVALID_F_GROUP_POS};
use super::group_dependency_analyzer::GroupDependencyAnalyzer;
pub struct FlattenAllButOne;
impl FlattenAllButOne {
pub fn get_groups_pos_to_flatten_for_exprs(
exprs: &[ExpressionId],
schema: &FactorizedSchema,
expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
) -> HashSet<FGroupPos> {
let mut result = HashSet::new();
let mut dependent_groups = HashSet::new();
for expr_id in exprs {
let mut analyzer = GroupDependencyAnalyzer::with_expr_store(schema, false, expr_store);
analyzer.visit(expr_id);
result.extend(analyzer.required_flat_groups().iter().copied());
dependent_groups.extend(analyzer.dependent_groups().iter().copied());
}
let mut candidates: Vec<FGroupPos> = dependent_groups
.into_iter()
.filter(|pos| {
schema
.get_group(*pos)
.map(|g| !g.is_flat() && !result.contains(pos))
.unwrap_or(false)
})
.collect();
candidates.sort_unstable();
for pos in candidates.iter().skip(1) {
result.insert(*pos);
}
result
}
pub fn get_groups_pos_to_flatten_for_exprs_with_leading(
exprs: &[ExpressionId],
schema: &FactorizedSchema,
expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
) -> (FGroupPos, HashSet<FGroupPos>) {
let mut result = HashSet::new();
let mut dependent_groups = HashSet::new();
for expr_id in exprs {
let mut analyzer = GroupDependencyAnalyzer::with_expr_store(schema, false, expr_store);
analyzer.visit(expr_id);
result.extend(analyzer.required_flat_groups().iter().copied());
dependent_groups.extend(analyzer.dependent_groups().iter().copied());
}
let mut candidates: Vec<FGroupPos> = dependent_groups
.iter()
.filter(|pos| {
schema
.get_group(**pos)
.map(|g| !g.is_flat() && !result.contains(*pos))
.unwrap_or(false)
})
.copied()
.collect();
candidates.sort_unstable();
for pos in candidates.iter().skip(1) {
result.insert(*pos);
}
if candidates.is_empty() {
(INVALID_F_GROUP_POS, result)
} else {
(candidates[0], result)
}
}
pub fn get_groups_pos_to_flatten_for_expr(
expr_id: &ExpressionId,
schema: &FactorizedSchema,
expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
) -> HashSet<FGroupPos> {
Self::get_groups_pos_to_flatten_for_exprs(std::slice::from_ref(expr_id), schema, expr_store)
}
pub fn get_groups_pos_to_flatten_for_groups(
dependent_groups: &HashSet<FGroupPos>,
schema: &FactorizedSchema,
) -> HashSet<FGroupPos> {
let mut candidates: Vec<FGroupPos> = dependent_groups
.iter()
.filter(|pos| {
schema
.get_group(**pos)
.map(|g| !g.is_flat())
.unwrap_or(false)
})
.copied()
.collect();
candidates.sort_unstable();
let mut result = HashSet::new();
for pos in candidates.iter().skip(1) {
result.insert(*pos);
}
result
}
pub fn get_groups_pos_to_flatten_for_groups_with_leading(
dependent_groups: &HashSet<FGroupPos>,
schema: &FactorizedSchema,
) -> (FGroupPos, HashSet<FGroupPos>) {
let mut candidates: Vec<FGroupPos> = dependent_groups
.iter()
.filter(|pos| {
schema
.get_group(**pos)
.map(|g| !g.is_flat())
.unwrap_or(false)
})
.copied()
.collect();
candidates.sort_unstable();
let mut result = HashSet::new();
for pos in candidates.iter().skip(1) {
result.insert(*pos);
}
if candidates.is_empty() {
(INVALID_F_GROUP_POS, result)
} else {
(candidates[0], result)
}
}
}
pub struct FlattenAll;
impl FlattenAll {
pub fn get_groups_pos_to_flatten_for_exprs(
exprs: &[ExpressionId],
schema: &FactorizedSchema,
expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
) -> HashSet<FGroupPos> {
let mut result = HashSet::new();
for expr_id in exprs {
result.extend(Self::get_groups_pos_to_flatten_for_expr(
expr_id, schema, expr_store,
));
}
result
}
pub fn get_groups_pos_to_flatten_for_expr(
expr_id: &ExpressionId,
schema: &FactorizedSchema,
expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
) -> HashSet<FGroupPos> {
let mut analyzer = GroupDependencyAnalyzer::with_expr_store(schema, false, expr_store);
analyzer.visit(expr_id);
Self::get_groups_pos_to_flatten_for_groups(analyzer.dependent_groups(), schema)
}
pub fn get_groups_pos_to_flatten_for_groups(
dependent_groups: &HashSet<FGroupPos>,
schema: &FactorizedSchema,
) -> HashSet<FGroupPos> {
dependent_groups
.iter()
.filter(|pos| {
schema
.get_group(**pos)
.map(|g| !g.is_flat())
.unwrap_or(false)
})
.copied()
.collect()
}
}
pub fn aggregate_groups_to_flatten(
key_ids: &[ExpressionId],
key_store: &HashMap<ExpressionId, linkrs_core::Expression>,
aggregate_args: &[Vec<linkrs_core::Expression>],
aggregate_distinct: &[bool],
child_schema: &FactorizedSchema,
) -> (FGroupPos, HashSet<FGroupPos>) {
let (leading, mut to_flatten) =
FlattenAllButOne::get_groups_pos_to_flatten_for_exprs_with_leading(
key_ids,
child_schema,
key_store,
);
if leading == INVALID_F_GROUP_POS {
return (leading, to_flatten);
}
for (idx, args) in aggregate_args.iter().enumerate() {
let is_distinct = aggregate_distinct.get(idx).copied().unwrap_or(false);
for payload in args {
let mut analyzer =
GroupDependencyAnalyzer::with_expr_store(child_schema, false, key_store);
analyzer.visit_expression(payload);
for pos in analyzer.required_flat_groups().iter().copied() {
if child_schema.get_group(pos).is_some_and(|g| !g.is_flat()) {
to_flatten.insert(pos);
}
}
if is_distinct {
for pos in analyzer.dependent_groups().iter().copied() {
if pos != leading && child_schema.get_group(pos).is_some_and(|g| !g.is_flat()) {
to_flatten.insert(pos);
}
}
}
}
}
(leading, to_flatten)
}
pub struct FlattenResolver;
impl FlattenResolver {
pub fn flatten_all_but_one(
dependent_groups: &[FGroupPos],
schema: &FactorizedSchema,
) -> (FGroupPos, Vec<FGroupPos>) {
let set: HashSet<FGroupPos> = dependent_groups.iter().copied().collect();
let (leading, to_flatten) =
FlattenAllButOne::get_groups_pos_to_flatten_for_groups_with_leading(&set, schema);
let mut vec: Vec<FGroupPos> = to_flatten.into_iter().collect();
vec.sort_unstable();
(leading, vec)
}
pub fn flatten_all(
dependent_groups: &[FGroupPos],
schema: &FactorizedSchema,
) -> Vec<FGroupPos> {
let set: HashSet<FGroupPos> = dependent_groups.iter().copied().collect();
let mut vec: Vec<FGroupPos> =
FlattenAll::get_groups_pos_to_flatten_for_groups(&set, schema)
.into_iter()
.collect();
vec.sort_unstable();
vec
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::planning::plan::factorization::FactorizedSchema;
use linkrs_core::types::expr::ExpressionId;
use linkrs_core::types::expr::FunctionArg;
use std::collections::HashSet;
fn expr(id: u64) -> ExpressionId {
ExpressionId::new(id)
}
#[test]
fn flatten_all_single_unflat() {
let mut schema = FactorizedSchema::new();
let g0 = schema.create_flat_group(false);
let g1 = schema.create_group();
schema.insert_to_group_and_scope(expr(1), g0).unwrap();
schema.insert_to_group_and_scope(expr(2), g1).unwrap();
let mut set = HashSet::new();
set.insert(g1);
let res = FlattenAll::get_groups_pos_to_flatten_for_groups(&set, &schema);
assert_eq!(res, HashSet::from([g1]));
let res2 = FlattenAllButOne::get_groups_pos_to_flatten_for_groups(&set, &schema);
assert!(
res2.is_empty(),
"single unflat should not be flattened in AllButOne"
);
}
#[test]
fn flatten_all_but_one_two_unflats() {
let mut schema = FactorizedSchema::new();
let g0 = schema.create_group();
let g1 = schema.create_group();
schema.insert_to_group_and_scope(expr(1), g0).unwrap();
schema.insert_to_group_and_scope(expr(2), g1).unwrap();
let mut set = HashSet::new();
set.insert(g0);
set.insert(g1);
let res = FlattenAllButOne::get_groups_pos_to_flatten_for_groups(&set, &schema);
assert_eq!(res.len(), 1);
let (_leading, to_flatten) =
FlattenAllButOne::get_groups_pos_to_flatten_for_groups_with_leading(&set, &schema);
assert_eq!(to_flatten.len(), 1);
}
#[test]
fn flatten_resolver_api() {
let mut schema = FactorizedSchema::new();
let g0 = schema.create_group();
let g1 = schema.create_group();
schema.insert_to_group_and_scope(expr(10), g0).unwrap();
schema.insert_to_group_and_scope(expr(20), g1).unwrap();
let (leading, to_flatten) = FlattenResolver::flatten_all_but_one(&[g0, g1], &schema);
assert_ne!(leading, INVALID_F_GROUP_POS);
assert_eq!(to_flatten.len(), 1);
let all = FlattenResolver::flatten_all(&[g0, g1], &schema);
assert_eq!(all.len(), 2);
}
#[test]
fn flatten_all_expr_matches_dependent_only() {
use crate::optimizer::factorization::GroupDependencyAnalyzer;
let mut schema = FactorizedSchema::new();
let g0 = schema.create_flat_group(false);
let g1 = schema.create_group();
schema
.insert_to_group_and_scope_with_name(expr(10), Some("a".to_string()), g0)
.unwrap();
schema
.insert_to_group_and_scope_with_name(expr(20), Some("x".to_string()), g1)
.unwrap();
let the_expr = linkrs_core::Expression::Function {
name: "list_extract".to_string(),
args: vec![
FunctionArg::Positional(linkrs_core::Expression::List(vec![
linkrs_core::Expression::Literal(linkrs_core::Value::BigInt(1)),
])),
FunctionArg::Positional(linkrs_core::Expression::Variable("x".to_string())),
],
};
let mut store = HashMap::new();
let fake_id = expr(999);
store.insert(fake_id.clone(), the_expr);
let mut analyzer = GroupDependencyAnalyzer::with_expr_store(&schema, false, &store);
analyzer.visit(&fake_id);
let expected =
FlattenAll::get_groups_pos_to_flatten_for_groups(analyzer.dependent_groups(), &schema);
let all = FlattenAll::get_groups_pos_to_flatten_for_expr(&fake_id, &schema, &store);
assert_eq!(all, expected, "FlattenAll(expr) must equal dependent-only");
assert!(all.contains(&g1));
let but_one =
FlattenAllButOne::get_groups_pos_to_flatten_for_expr(&fake_id, &schema, &store);
assert!(
but_one.contains(&g1),
"required_flat group g1 should be flattened via AllButOne, got {:?}",
but_one
);
}
}