1use std::collections::{BTreeSet, HashMap, HashSet};
37
38use rudb_common::Result;
39use rudb_plan::{Arm, ColumnBinding, Expr, ExprRef, Node, NodeRef, Plan, Slice};
40
41use crate::pass::{Context, Pass, top_down};
42
43#[derive(Debug, Clone, Copy)]
45pub struct UnusedColumns;
46
47impl Pass for UnusedColumns {
48 fn name(&self) -> &'static str {
49 "unused_columns"
50 }
51
52 fn run(&self, plan: &mut Plan, _context: &Context) -> Result<()> {
53 prune(plan);
54 Ok(())
55 }
56}
57
58pub fn prune(plan: &mut Plan) {
63 let order = top_down(plan);
64 let untouched = untouched(plan, &order);
65 let mut moved: HashMap<u32, Vec<u32>> = HashMap::new();
68 let mut read: HashMap<u32, BTreeSet<u32>> = HashMap::new();
69 let mut found = Found::default();
70
71 for node in order {
74 if !untouched.contains(&node) {
75 narrow(plan, node, &read, &mut moved);
76 }
77 let mark = found.order.len();
78 expressions(plan, node, &mut found);
79 for &expr in &found.order[mark..] {
80 if let Expr::Column(binding) = *plan.expr(expr) {
81 read.entry(binding.table).or_default().insert(binding.column);
82 }
83 }
84 }
85
86 if moved.is_empty() {
87 return;
88 }
89 for &expr in &found.order {
90 let Expr::Column(binding) = *plan.expr(expr) else { continue };
91 let Some(positions) = moved.get(&binding.table) else { continue };
92 let to = positions[binding.column as usize];
93 plan.rebind(expr, ColumnBinding::new(binding.table, to));
94 }
95}
96
97fn narrow(
103 plan: &mut Plan,
104 node: NodeRef,
105 read: &HashMap<u32, BTreeSet<u32>>,
106 moved: &mut HashMap<u32, Vec<u32>>,
107) {
108 let empty = BTreeSet::new();
109 match *plan.node(node) {
110 Node::Get { index, columns, .. } | Node::TableFunction { index, columns, .. } => {
115 let wanted = read.get(&index).unwrap_or(&empty);
116 let held = plan.field_list(columns).len();
117 if wanted.len() == held {
118 return;
119 }
120 let kept: Vec<_> = wanted
121 .iter()
122 .filter_map(|&at| plan.field_list(columns).get(at as usize).cloned())
123 .collect();
124 if kept.len() != wanted.len() {
125 return;
126 }
127 let narrowed = plan.add_fields(&kept);
128 match plan.node_mut(node) {
129 Node::Get { columns, .. } | Node::TableFunction { columns, .. } => {
130 *columns = narrowed;
131 }
132 _ => unreachable!("the node was one of these two a moment ago"),
133 }
134 moved.insert(index, positions(wanted, held));
135 }
136 Node::Project { index, exprs, names, .. } => {
137 let wanted = read.get(&index).unwrap_or(&empty);
138 let held = plan.expr_list(exprs).len();
139 if wanted.len() == held {
140 return;
141 }
142 let kept: Vec<_> = wanted
143 .iter()
144 .filter_map(|&at| plan.expr_list(exprs).get(at as usize).copied())
145 .collect();
146 let labels: Vec<_> = wanted
147 .iter()
148 .filter_map(|&at| plan.name_list(names).get(at as usize).copied())
149 .collect();
150 if kept.len() != wanted.len() || labels.len() != wanted.len() {
151 return;
152 }
153 let narrowed = plan.add_expr_list(&kept);
154 let renamed = plan.add_name_list(&labels);
155 match plan.node_mut(node) {
156 Node::Project { exprs, names, .. } => {
157 *exprs = narrowed;
158 *names = renamed;
159 }
160 _ => unreachable!("the node was a projection a moment ago"),
161 }
162 moved.insert(index, positions(wanted, held));
163 }
164 _ => {}
165 }
166}
167
168fn positions(wanted: &BTreeSet<u32>, held: usize) -> Vec<u32> {
175 let mut positions = vec![0; held];
176 for (new, &old) in wanted.iter().enumerate() {
177 positions[old as usize] = new as u32;
178 }
179 positions
180}
181
182fn untouched(plan: &Plan, order: &[NodeRef]) -> HashSet<NodeRef> {
201 let mut found = HashSet::new();
202 let mut pending = vec![plan.root()];
203 while let Some(node) = pending.pop() {
204 if !found.insert(node) {
205 continue;
206 }
207 if plan.node(node).table_index().is_some() {
208 continue;
209 }
210 pending.extend(plan.node(node).children().into_iter().flatten());
211 }
212 for &node in order {
213 match *plan.node(node) {
214 Node::SetOp { left, right, .. } => {
215 found.insert(left);
216 found.insert(right);
217 }
218 Node::Distinct { input, on } if on.is_empty() => {
219 found.insert(input);
220 }
221 _ => {}
222 }
223 }
224 found
225}
226
227fn expressions(plan: &Plan, node: NodeRef, found: &mut Found) {
229 match *plan.node(node) {
230 Node::Get { .. } | Node::Dummy | Node::SetOp { .. } | Node::CrossProduct { .. } => {}
231 Node::Values { rows, .. } => {
232 for &row in plan.row_list(rows) {
233 list(plan, row, found);
234 }
235 }
236 Node::TableFunction { args, .. } => list(plan, args, found),
237 Node::Filter { predicate, .. } => walk(plan, predicate, found),
238 Node::Project { exprs, .. } => list(plan, exprs, found),
239 Node::Aggregate { groups, aggregates, .. } => {
240 list(plan, groups, found);
241 list(plan, aggregates, found);
242 }
243 Node::Sort { keys, .. } | Node::TopN { keys, .. } => {
244 for key in plan.sort_key_list(keys) {
245 walk(plan, key.expr, found);
246 }
247 }
248 Node::Limit { .. } => {}
249 Node::Distinct { on, .. } => list(plan, on, found),
250 Node::Join { conditions, .. } => list(plan, conditions, found),
251 }
252}
253
254#[derive(Debug, Default)]
260struct Found {
261 order: Vec<ExprRef>,
262 seen: HashSet<ExprRef>,
263}
264
265fn list(plan: &Plan, slice: Slice, found: &mut Found) {
266 for &expr in plan.expr_list(slice) {
267 walk(plan, expr, found);
268 }
269}
270
271fn walk(plan: &Plan, expr: ExprRef, found: &mut Found) {
273 if !found.seen.insert(expr) {
274 return;
275 }
276 found.order.push(expr);
277 match *plan.expr(expr) {
278 Expr::Column(_) | Expr::Constant(_) => {}
279 Expr::Cast { input, .. } => walk(plan, input, found),
280 Expr::Compare { left, right, .. } => {
281 walk(plan, left, found);
282 walk(plan, right, found);
283 }
284 Expr::Conjunction { children, .. } => list(plan, children, found),
285 Expr::Function { args, .. } => list(plan, args, found),
286 Expr::Aggregate { args, filter, .. } => {
287 list(plan, args, found);
288 if let Some(filter) = filter {
289 walk(plan, filter, found);
290 }
291 }
292 Expr::Case { arms, otherwise } => {
293 for &Arm { when, then } in plan.arm_list(arms) {
294 walk(plan, when, found);
295 walk(plan, then, found);
296 }
297 if let Some(otherwise) = otherwise {
298 walk(plan, otherwise, found);
299 }
300 }
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn pruned(text: &str) -> String {
310 let mut plan =
311 Plan::parse(text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"));
312 prune(&mut plan);
313 plan.validate().unwrap_or_else(|error| panic!("{text} pruned to a bad plan: {error}"));
314 plan.to_string()
315 }
316
317 #[test]
318 fn a_scan_of_a_column_nobody_reads_loses_it() {
319 let before = "Project #1 [#0.0::INTEGER AS a]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
320 let after = "Project #1 [#0.0::INTEGER AS a]\n Get memory.main.t AS t #0 [a::INTEGER]\n";
321 assert_eq!(pruned(before), after);
322 }
323
324 #[test]
325 fn the_columns_that_stay_are_read_from_where_they_moved_to() {
326 let before = "Project #1 [#0.2::VARCHAR AS c]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR, c::VARCHAR]\n";
329 let after = "Project #1 [#0.0::VARCHAR AS c]\n Get memory.main.t AS t #0 [c::VARCHAR]\n";
330 assert_eq!(pruned(before), after);
331 }
332
333 #[test]
334 fn a_column_read_only_by_a_filter_is_kept_and_one_read_by_nothing_is_not() {
335 let before = "Project #1 [#0.0::INTEGER AS a]\n Filter (#0.1::INTEGER > 1::INTEGER)::BOOLEAN\n Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER, c::INTEGER]\n";
336 let after = "Project #1 [#0.0::INTEGER AS a]\n Filter (#0.1::INTEGER > 1::INTEGER)::BOOLEAN\n Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n";
337 assert_eq!(pruned(before), after);
338 }
339
340 #[test]
341 fn counting_the_rows_reads_no_columns_at_all() {
342 let before = "Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
344 let after = "Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n Get memory.main.t AS t #0 []\n";
345 assert_eq!(pruned(before), after);
346 }
347
348 #[test]
349 fn a_table_function_is_narrowed_the_same_way_a_table_is() {
350 let before = "Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n TableFunction read_parquet args=['f.parquet'::VARCHAR] #0 [a::INTEGER, b::VARCHAR]\n";
351 let after = "Aggregate #1 groups=[] aggregates=[count_star()::BIGINT]\n TableFunction read_parquet args=['f.parquet'::VARCHAR] #0 []\n";
352 assert_eq!(pruned(before), after);
353 }
354
355 #[test]
356 fn each_side_of_a_join_is_narrowed_to_what_that_side_is_read_for() {
357 let before = "Project #2 [#0.0::INTEGER AS a]\n Join INNER on=[(#0.0::INTEGER = #1.1::INTEGER)::BOOLEAN]\n Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER]\n Get memory.main.u AS u #1 [x::INTEGER, y::INTEGER]\n";
358 let after = "Project #2 [#0.0::INTEGER AS a]\n Join INNER on=[(#0.0::INTEGER = #1.0::INTEGER)::BOOLEAN]\n Get memory.main.t AS t #0 [a::INTEGER]\n Get memory.main.u AS u #1 [y::INTEGER]\n";
359 assert_eq!(pruned(before), after);
360 }
361
362 #[test]
363 fn a_scan_whose_columns_are_the_answer_is_left_alone() {
364 let text = "Limit 1 offset 0\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
367 assert_eq!(pruned(text), text);
368 }
369
370 #[test]
371 fn a_scan_that_is_already_narrow_is_not_touched() {
372 let text = "Project #1 [#0.0::INTEGER AS a]\n Get memory.main.t AS t #0 [a::INTEGER]\n";
373 assert_eq!(pruned(text), text);
374 }
375
376 #[test]
377 fn pruning_twice_is_pruning_once() {
378 let before = "Project #1 [#0.1::VARCHAR AS b]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
379 let once = pruned(before);
380 assert_eq!(pruned(&once), once);
381 }
382
383 #[test]
384 fn the_columns_that_stay_keep_the_order_the_scan_had_them_in() {
385 let before = "Project #1 [#0.3::VARCHAR AS d, #0.1::INTEGER AS b]\n Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER, c::INTEGER, d::VARCHAR]\n";
389 let after = "Project #1 [#0.1::VARCHAR AS d, #0.0::INTEGER AS b]\n Get memory.main.t AS t #0 [b::INTEGER, d::VARCHAR]\n";
390 assert_eq!(pruned(before), after);
391 }
392
393 #[test]
394 fn a_column_only_a_sort_key_reads_is_kept() {
395 let before = "Project #1 [#0.0::INTEGER AS a]\n Sort [#0.2::INTEGER DESC NULLS LAST]\n Get memory.main.t AS t #0 [a::INTEGER, b::INTEGER, c::INTEGER]\n";
398 let after = "Project #1 [#0.0::INTEGER AS a]\n Sort [#0.1::INTEGER DESC NULLS LAST]\n Get memory.main.t AS t #0 [a::INTEGER, c::INTEGER]\n";
399 assert_eq!(pruned(before), after);
400 }
401
402 #[test]
403 fn a_column_buried_inside_an_expression_is_found_the_same_as_a_bare_one() {
404 let before = "Project #1 [upper(CASE WHEN (#0.2::INTEGER > 3::INTEGER)::BOOLEAN THEN #0.0::VARCHAR ELSE ''::VARCHAR END::VARCHAR)::VARCHAR AS a]\n Get memory.main.t AS t #0 [a::VARCHAR, b::VARCHAR, c::INTEGER]\n";
407 let after = "Project #1 [upper(CASE WHEN (#0.1::INTEGER > 3::INTEGER)::BOOLEAN THEN #0.0::VARCHAR ELSE ''::VARCHAR END::VARCHAR)::VARCHAR AS a]\n Get memory.main.t AS t #0 [a::VARCHAR, c::INTEGER]\n";
408 assert_eq!(pruned(before), after);
409 }
410
411 #[test]
412 fn a_projection_in_the_middle_loses_the_expressions_nothing_above_it_reads() {
413 let before = "Project #2 [#1.1::VARCHAR AS b]\n Project #1 [#0.0::INTEGER AS a, #0.1::VARCHAR AS b, #0.2::INTEGER AS c]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR, c::INTEGER]\n";
417 let after = "Project #2 [#1.0::VARCHAR AS b]\n Project #1 [#0.0::VARCHAR AS b]\n Get memory.main.t AS t #0 [b::VARCHAR]\n";
418 assert_eq!(pruned(before), after);
419 }
420
421 #[test]
422 fn counting_the_rows_through_a_projection_reads_no_columns_either() {
423 let before = "Aggregate #2 groups=[] aggregates=[count_star()::BIGINT]\n Project #1 [#0.0::INTEGER AS a, #0.1::VARCHAR AS b]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
426 let after = "Aggregate #2 groups=[] aggregates=[count_star()::BIGINT]\n Project #1 []\n Get memory.main.t AS t #0 []\n";
427 assert_eq!(pruned(before), after);
428 }
429
430 #[test]
431 fn pruning_a_projection_twice_is_pruning_it_once() {
432 let before = "Project #2 [#1.1::VARCHAR AS b]\n Project #1 [#0.0::INTEGER AS a, #0.1::VARCHAR AS b]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
433 let once = pruned(before);
434 assert_eq!(pruned(&once), once);
435 }
436
437 #[test]
438 fn neither_side_of_a_set_operation_is_narrowed() {
439 let text = "Aggregate #3 groups=[] aggregates=[count_star()::BIGINT]\n SetOp UNION ALL #2\n Get memory.main.t AS t #0 [a::INTEGER]\n Get memory.main.u AS u #1 [x::INTEGER]\n";
443 assert_eq!(pruned(text), text);
444 }
445
446 #[test]
447 fn a_distinct_that_names_no_columns_keeps_the_ones_it_is_distinct_on() {
448 let text = "Aggregate #2 groups=[] aggregates=[count_star()::BIGINT]\n Distinct on=[]\n Project #1 [#0.1::VARCHAR AS b]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR]\n";
454 let after = "Aggregate #2 groups=[] aggregates=[count_star()::BIGINT]\n Distinct on=[]\n Project #1 [#0.0::VARCHAR AS b]\n Get memory.main.t AS t #0 [b::VARCHAR]\n";
455 assert_eq!(pruned(text), after);
456 }
457
458 #[test]
459 fn a_distinct_on_named_columns_narrows_underneath_like_anything_else() {
460 let before = "Project #2 [#1.0::VARCHAR AS b]\n Distinct on=[#1.0::VARCHAR]\n Project #1 [#0.1::VARCHAR AS b, #0.2::INTEGER AS c]\n Get memory.main.t AS t #0 [a::INTEGER, b::VARCHAR, c::INTEGER]\n";
463 let after = "Project #2 [#1.0::VARCHAR AS b]\n Distinct on=[#1.0::VARCHAR]\n Project #1 [#0.0::VARCHAR AS b]\n Get memory.main.t AS t #0 [b::VARCHAR]\n";
464 assert_eq!(pruned(before), after);
465 }
466
467 #[test]
468 fn a_values_list_keeps_its_columns_even_when_nothing_reads_them() {
469 let text = "Project #1 [#0.0::BIGINT AS a]\n Values #0 [a::BIGINT, b::BIGINT] rows=[[1::BIGINT, 2::BIGINT], [3::BIGINT, 4::BIGINT]]\n";
472 assert_eq!(pruned(text), text);
473 }
474}