linkrs_query/optimizer/factorization/
flatten_resolver.rs1use std::collections::{HashMap, HashSet};
2
3use linkrs_core::types::expr::ExpressionId;
4
5use crate::planning::plan::factorization::{FGroupPos, FactorizedSchema, INVALID_F_GROUP_POS};
6
7use super::group_dependency_analyzer::GroupDependencyAnalyzer;
8
9pub struct FlattenAllButOne;
14
15impl FlattenAllButOne {
16 pub fn get_groups_pos_to_flatten_for_exprs(
18 exprs: &[ExpressionId],
19 schema: &FactorizedSchema,
20 expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
21 ) -> HashSet<FGroupPos> {
22 let mut result = HashSet::new();
23 let mut dependent_groups = HashSet::new();
24
25 for expr_id in exprs {
26 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(schema, false, expr_store);
27 analyzer.visit(expr_id);
28 result.extend(analyzer.required_flat_groups().iter().copied());
29 dependent_groups.extend(analyzer.dependent_groups().iter().copied());
30 }
31
32 let mut candidates: Vec<FGroupPos> = dependent_groups
33 .into_iter()
34 .filter(|pos| {
35 schema
36 .get_group(*pos)
37 .map(|g| !g.is_flat() && !result.contains(pos))
38 .unwrap_or(false)
39 })
40 .collect();
41 candidates.sort_unstable();
43 for pos in candidates.iter().skip(1) {
44 result.insert(*pos);
45 }
46 result
47 }
48
49 pub fn get_groups_pos_to_flatten_for_exprs_with_leading(
50 exprs: &[ExpressionId],
51 schema: &FactorizedSchema,
52 expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
53 ) -> (FGroupPos, HashSet<FGroupPos>) {
54 let mut result = HashSet::new();
55 let mut dependent_groups = HashSet::new();
56
57 for expr_id in exprs {
58 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(schema, false, expr_store);
59 analyzer.visit(expr_id);
60 result.extend(analyzer.required_flat_groups().iter().copied());
61 dependent_groups.extend(analyzer.dependent_groups().iter().copied());
62 }
63
64 let mut candidates: Vec<FGroupPos> = dependent_groups
65 .iter()
66 .filter(|pos| {
67 schema
68 .get_group(**pos)
69 .map(|g| !g.is_flat() && !result.contains(*pos))
70 .unwrap_or(false)
71 })
72 .copied()
73 .collect();
74 candidates.sort_unstable();
75
76 for pos in candidates.iter().skip(1) {
77 result.insert(*pos);
78 }
79
80 if candidates.is_empty() {
81 (INVALID_F_GROUP_POS, result)
82 } else {
83 (candidates[0], result)
84 }
85 }
86
87 pub fn get_groups_pos_to_flatten_for_expr(
89 expr_id: &ExpressionId,
90 schema: &FactorizedSchema,
91 expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
92 ) -> HashSet<FGroupPos> {
93 Self::get_groups_pos_to_flatten_for_exprs(std::slice::from_ref(expr_id), schema, expr_store)
94 }
95
96 pub fn get_groups_pos_to_flatten_for_groups(
98 dependent_groups: &HashSet<FGroupPos>,
99 schema: &FactorizedSchema,
100 ) -> HashSet<FGroupPos> {
101 let mut candidates: Vec<FGroupPos> = dependent_groups
102 .iter()
103 .filter(|pos| {
104 schema
105 .get_group(**pos)
106 .map(|g| !g.is_flat())
107 .unwrap_or(false)
108 })
109 .copied()
110 .collect();
111 candidates.sort_unstable();
112 let mut result = HashSet::new();
113 for pos in candidates.iter().skip(1) {
114 result.insert(*pos);
115 }
116 result
117 }
118
119 pub fn get_groups_pos_to_flatten_for_groups_with_leading(
121 dependent_groups: &HashSet<FGroupPos>,
122 schema: &FactorizedSchema,
123 ) -> (FGroupPos, HashSet<FGroupPos>) {
124 let mut candidates: Vec<FGroupPos> = dependent_groups
125 .iter()
126 .filter(|pos| {
127 schema
128 .get_group(**pos)
129 .map(|g| !g.is_flat())
130 .unwrap_or(false)
131 })
132 .copied()
133 .collect();
134 candidates.sort_unstable();
135 let mut result = HashSet::new();
136 for pos in candidates.iter().skip(1) {
137 result.insert(*pos);
138 }
139 if candidates.is_empty() {
140 (INVALID_F_GROUP_POS, result)
141 } else {
142 (candidates[0], result)
143 }
144 }
145}
146
147pub struct FlattenAll;
149
150impl FlattenAll {
151 pub fn get_groups_pos_to_flatten_for_exprs(
152 exprs: &[ExpressionId],
153 schema: &FactorizedSchema,
154 expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
155 ) -> HashSet<FGroupPos> {
156 let mut result = HashSet::new();
157 for expr_id in exprs {
158 result.extend(Self::get_groups_pos_to_flatten_for_expr(
159 expr_id, schema, expr_store,
160 ));
161 }
162 result
163 }
164
165 pub fn get_groups_pos_to_flatten_for_expr(
166 expr_id: &ExpressionId,
167 schema: &FactorizedSchema,
168 expr_store: &HashMap<ExpressionId, linkrs_core::Expression>,
169 ) -> HashSet<FGroupPos> {
170 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(schema, false, expr_store);
175 analyzer.visit(expr_id);
176 Self::get_groups_pos_to_flatten_for_groups(analyzer.dependent_groups(), schema)
177 }
178
179 pub fn get_groups_pos_to_flatten_for_groups(
180 dependent_groups: &HashSet<FGroupPos>,
181 schema: &FactorizedSchema,
182 ) -> HashSet<FGroupPos> {
183 dependent_groups
184 .iter()
185 .filter(|pos| {
186 schema
187 .get_group(**pos)
188 .map(|g| !g.is_flat())
189 .unwrap_or(false)
190 })
191 .copied()
192 .collect()
193 }
194}
195
196pub fn aggregate_groups_to_flatten(
205 key_ids: &[ExpressionId],
206 key_store: &HashMap<ExpressionId, linkrs_core::Expression>,
207 aggregate_args: &[Vec<linkrs_core::Expression>],
208 aggregate_distinct: &[bool],
209 child_schema: &FactorizedSchema,
210) -> (FGroupPos, HashSet<FGroupPos>) {
211 let (leading, mut to_flatten) =
212 FlattenAllButOne::get_groups_pos_to_flatten_for_exprs_with_leading(
213 key_ids,
214 child_schema,
215 key_store,
216 );
217 if leading == INVALID_F_GROUP_POS {
218 return (leading, to_flatten);
219 }
220 for (idx, args) in aggregate_args.iter().enumerate() {
221 let is_distinct = aggregate_distinct.get(idx).copied().unwrap_or(false);
222 for payload in args {
223 let mut analyzer =
224 GroupDependencyAnalyzer::with_expr_store(child_schema, false, key_store);
225 analyzer.visit_expression(payload);
226 for pos in analyzer.required_flat_groups().iter().copied() {
227 if child_schema.get_group(pos).is_some_and(|g| !g.is_flat()) {
228 to_flatten.insert(pos);
229 }
230 }
231 if is_distinct {
232 for pos in analyzer.dependent_groups().iter().copied() {
233 if pos != leading && child_schema.get_group(pos).is_some_and(|g| !g.is_flat()) {
234 to_flatten.insert(pos);
235 }
236 }
237 }
238 }
239 }
240 (leading, to_flatten)
241}
242
243pub struct FlattenResolver;
245
246impl FlattenResolver {
247 pub fn flatten_all_but_one(
248 dependent_groups: &[FGroupPos],
249 schema: &FactorizedSchema,
250 ) -> (FGroupPos, Vec<FGroupPos>) {
251 let set: HashSet<FGroupPos> = dependent_groups.iter().copied().collect();
252 let (leading, to_flatten) =
253 FlattenAllButOne::get_groups_pos_to_flatten_for_groups_with_leading(&set, schema);
254 let mut vec: Vec<FGroupPos> = to_flatten.into_iter().collect();
255 vec.sort_unstable();
256 (leading, vec)
257 }
258
259 pub fn flatten_all(
260 dependent_groups: &[FGroupPos],
261 schema: &FactorizedSchema,
262 ) -> Vec<FGroupPos> {
263 let set: HashSet<FGroupPos> = dependent_groups.iter().copied().collect();
264 let mut vec: Vec<FGroupPos> =
265 FlattenAll::get_groups_pos_to_flatten_for_groups(&set, schema)
266 .into_iter()
267 .collect();
268 vec.sort_unstable();
269 vec
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276 use crate::planning::plan::factorization::FactorizedSchema;
277 use linkrs_core::types::expr::ExpressionId;
278 use linkrs_core::types::expr::FunctionArg;
279 use std::collections::HashSet;
280
281 fn expr(id: u64) -> ExpressionId {
282 ExpressionId::new(id)
283 }
284
285 #[test]
286 fn flatten_all_single_unflat() {
287 let mut schema = FactorizedSchema::new();
288 let g0 = schema.create_flat_group(false);
289 let g1 = schema.create_group();
290 schema.insert_to_group_and_scope(expr(1), g0).unwrap();
291 schema.insert_to_group_and_scope(expr(2), g1).unwrap();
292
293 let mut set = HashSet::new();
294 set.insert(g1);
295 let res = FlattenAll::get_groups_pos_to_flatten_for_groups(&set, &schema);
296 assert_eq!(res, HashSet::from([g1]));
297
298 let res2 = FlattenAllButOne::get_groups_pos_to_flatten_for_groups(&set, &schema);
299 assert!(
300 res2.is_empty(),
301 "single unflat should not be flattened in AllButOne"
302 );
303 }
304
305 #[test]
306 fn flatten_all_but_one_two_unflats() {
307 let mut schema = FactorizedSchema::new();
308 let g0 = schema.create_group();
309 let g1 = schema.create_group();
310 schema.insert_to_group_and_scope(expr(1), g0).unwrap();
311 schema.insert_to_group_and_scope(expr(2), g1).unwrap();
312 let mut set = HashSet::new();
313 set.insert(g0);
314 set.insert(g1);
315 let res = FlattenAllButOne::get_groups_pos_to_flatten_for_groups(&set, &schema);
316 assert_eq!(res.len(), 1);
317 let (_leading, to_flatten) =
318 FlattenAllButOne::get_groups_pos_to_flatten_for_groups_with_leading(&set, &schema);
319 assert_eq!(to_flatten.len(), 1);
320 }
321
322 #[test]
323 fn flatten_resolver_api() {
324 let mut schema = FactorizedSchema::new();
325 let g0 = schema.create_group();
326 let g1 = schema.create_group();
327 schema.insert_to_group_and_scope(expr(10), g0).unwrap();
328 schema.insert_to_group_and_scope(expr(20), g1).unwrap();
329 let (leading, to_flatten) = FlattenResolver::flatten_all_but_one(&[g0, g1], &schema);
330 assert_ne!(leading, INVALID_F_GROUP_POS);
331 assert_eq!(to_flatten.len(), 1);
332 let all = FlattenResolver::flatten_all(&[g0, g1], &schema);
333 assert_eq!(all.len(), 2);
334 }
335
336 #[test]
337 fn flatten_all_expr_matches_dependent_only() {
338 use crate::optimizer::factorization::GroupDependencyAnalyzer;
339 let mut schema = FactorizedSchema::new();
340 let g0 = schema.create_flat_group(false);
341 let g1 = schema.create_group();
342 schema
343 .insert_to_group_and_scope_with_name(expr(10), Some("a".to_string()), g0)
344 .unwrap();
345 schema
346 .insert_to_group_and_scope_with_name(expr(20), Some("x".to_string()), g1)
347 .unwrap();
348 let the_expr = linkrs_core::Expression::Function {
352 name: "list_extract".to_string(),
353 args: vec![
354 FunctionArg::Positional(linkrs_core::Expression::List(vec![
355 linkrs_core::Expression::Literal(linkrs_core::Value::BigInt(1)),
356 ])),
357 FunctionArg::Positional(linkrs_core::Expression::Variable("x".to_string())),
358 ],
359 };
360 let mut store = HashMap::new();
361 let fake_id = expr(999);
362 store.insert(fake_id.clone(), the_expr);
363 let mut analyzer = GroupDependencyAnalyzer::with_expr_store(&schema, false, &store);
364 analyzer.visit(&fake_id);
365 let expected =
366 FlattenAll::get_groups_pos_to_flatten_for_groups(analyzer.dependent_groups(), &schema);
367 let all = FlattenAll::get_groups_pos_to_flatten_for_expr(&fake_id, &schema, &store);
368 assert_eq!(all, expected, "FlattenAll(expr) must equal dependent-only");
369 assert!(all.contains(&g1));
370 let but_one =
371 FlattenAllButOne::get_groups_pos_to_flatten_for_expr(&fake_id, &schema, &store);
372 assert!(
373 but_one.contains(&g1),
374 "required_flat group g1 should be flattened via AllButOne, got {:?}",
375 but_one
376 );
377 }
378}