gitql_engine/
engine_executor.rs

1use std::cmp;
2use std::collections::HashMap;
3
4use gitql_ast::expression::Expr;
5use gitql_ast::expression::ExprKind;
6use gitql_ast::statement::AggregateValue;
7use gitql_ast::statement::AggregationsStatement;
8use gitql_ast::statement::GroupByStatement;
9use gitql_ast::statement::HavingStatement;
10use gitql_ast::statement::IntoStatement;
11use gitql_ast::statement::LimitStatement;
12use gitql_ast::statement::OffsetStatement;
13use gitql_ast::statement::OrderByStatement;
14use gitql_ast::statement::QualifyStatement;
15use gitql_ast::statement::SelectStatement;
16use gitql_ast::statement::Statement;
17use gitql_ast::statement::StatementKind::*;
18use gitql_ast::statement::WhereStatement;
19use gitql_ast::statement::WindowFunctionsStatement;
20use gitql_core::environment::Environment;
21use gitql_core::object::GitQLObject;
22use gitql_core::object::Group;
23use gitql_core::object::Row;
24use gitql_core::values::null::NullValue;
25use gitql_core::values::Value;
26
27use crate::data_provider::DataProvider;
28use crate::engine_evaluator::evaluate_expression;
29use crate::engine_filter::apply_filter_operation;
30use crate::engine_group::execute_group_by_statement;
31use crate::engine_join::apply_join_operation;
32use crate::engine_ordering::execute_order_by_statement;
33use crate::engine_output_into::execute_into_statement;
34use crate::engine_window_functions::execute_window_functions_statement;
35
36#[allow(clippy::borrowed_box)]
37pub fn execute_statement(
38    env: &mut Environment,
39    statement: &Box<dyn Statement>,
40    data_provider: &Box<dyn DataProvider>,
41    gitql_object: &mut GitQLObject,
42    alias_table: &mut HashMap<String, String>,
43    hidden_selection: &HashMap<String, Vec<String>>,
44    has_group_by_statement: bool,
45) -> Result<(), String> {
46    match statement.kind() {
47        Select => {
48            let statement = statement
49                .as_any()
50                .downcast_ref::<SelectStatement>()
51                .unwrap();
52
53            execute_select_statement(
54                env,
55                statement,
56                alias_table,
57                data_provider,
58                gitql_object,
59                hidden_selection,
60            )
61        }
62        Where => {
63            let statement = statement.as_any().downcast_ref::<WhereStatement>().unwrap();
64            execute_where_statement(env, statement, gitql_object)
65        }
66        Having => {
67            let statement = statement
68                .as_any()
69                .downcast_ref::<HavingStatement>()
70                .unwrap();
71            execute_having_statement(env, statement, gitql_object)
72        }
73        Qualify => {
74            let statement = statement
75                .as_any()
76                .downcast_ref::<QualifyStatement>()
77                .unwrap();
78            execute_qualify_statement(env, statement, gitql_object)
79        }
80        Limit => {
81            let statement = statement.as_any().downcast_ref::<LimitStatement>().unwrap();
82            execute_limit_statement(statement, gitql_object)
83        }
84        Offset => {
85            let statement = statement
86                .as_any()
87                .downcast_ref::<OffsetStatement>()
88                .unwrap();
89            execute_offset_statement(env, statement, gitql_object)
90        }
91        OrderBy => {
92            let statement = statement
93                .as_any()
94                .downcast_ref::<OrderByStatement>()
95                .unwrap();
96
97            if gitql_object.len() > 1 {
98                gitql_object.flat();
99            }
100
101            let main_group_index = 0;
102            execute_order_by_statement(env, statement, gitql_object, main_group_index)
103        }
104        GroupBy => {
105            let statement = statement
106                .as_any()
107                .downcast_ref::<GroupByStatement>()
108                .unwrap();
109            execute_group_by_statement(env, statement, gitql_object)
110        }
111        AggregateFunction => {
112            let statement = statement
113                .as_any()
114                .downcast_ref::<AggregationsStatement>()
115                .unwrap();
116            execute_aggregation_functions_statement(
117                env,
118                statement,
119                gitql_object,
120                alias_table,
121                has_group_by_statement,
122            )
123        }
124        WindowFunction => {
125            let statement = statement
126                .as_any()
127                .downcast_ref::<WindowFunctionsStatement>()
128                .unwrap();
129            execute_window_functions_statement(env, statement, gitql_object, alias_table)
130        }
131        Into => {
132            let statement = statement.as_any().downcast_ref::<IntoStatement>().unwrap();
133            execute_into_statement(statement, gitql_object)
134        }
135    }
136}
137
138#[allow(clippy::borrowed_box)]
139fn execute_select_statement(
140    env: &mut Environment,
141    statement: &SelectStatement,
142    alias_table: &HashMap<String, String>,
143    data_provider: &Box<dyn DataProvider>,
144    gitql_object: &mut GitQLObject,
145    hidden_selections: &HashMap<String, Vec<String>>,
146) -> Result<(), String> {
147    let mut selected_rows_per_table: HashMap<String, Vec<Row>> = HashMap::new();
148    let mut hidden_selection_count_per_table: HashMap<String, usize> = HashMap::new();
149
150    let mut titles: Vec<String> = vec![];
151    let mut hidden_sum = 0;
152
153    for table_selection in &statement.table_selections {
154        // Select objects from the target table
155        let table_name = &table_selection.table_name;
156        let selected_columns = &mut table_selection.columns_names.to_owned();
157
158        // Insert Hidden selection items for this table first
159        let mut hidden_selection_count = 0;
160        if let Some(table_hidden_selection) = hidden_selections.get(table_name) {
161            for hidden_selection in table_hidden_selection {
162                if !selected_columns.contains(hidden_selection) {
163                    selected_columns.insert(0, hidden_selection.to_string());
164                    hidden_selection_count += 1;
165                }
166            }
167        }
168
169        hidden_selection_count_per_table.insert(table_name.to_string(), hidden_selection_count);
170
171        // Calculate list of titles once per table
172        let mut table_titles = vec![];
173        for selected_column in selected_columns.iter_mut() {
174            table_titles.push(resolve_actual_column_name(alias_table, selected_column));
175        }
176
177        // Call the provider only if table name is not empty
178        let selected_rows: Vec<Row> = if table_name.is_empty() {
179            vec![Row { values: vec![] }]
180        } else {
181            data_provider.provide(table_name, selected_columns)?
182        };
183
184        selected_rows_per_table.insert(table_name.to_string(), selected_rows);
185
186        // Append hidden selection in the right position
187        // at the end all hidden selections will be first
188        let hidden_selection_titles = &table_titles[..hidden_selection_count];
189        titles.splice(hidden_sum..hidden_sum, hidden_selection_titles.to_vec());
190
191        // Non hidden selection should be inserted at the end
192        let selection_titles = &table_titles[hidden_selection_count..];
193        titles.extend_from_slice(selection_titles);
194        hidden_sum += hidden_selection_count;
195    }
196
197    gitql_object.titles.append(&mut titles);
198
199    // Apply joins operations if exists
200    let mut selected_rows: Vec<Row> = vec![];
201    apply_join_operation(
202        env,
203        &mut selected_rows,
204        &statement.joins,
205        &statement.table_selections,
206        &mut selected_rows_per_table,
207        &hidden_selection_count_per_table,
208        &gitql_object.titles,
209    )?;
210
211    // Execute Selected expressions if exists
212    if !statement.selected_expr.is_empty() {
213        execute_expression_selection(
214            env,
215            &mut selected_rows,
216            &gitql_object.titles,
217            &statement.selected_expr_titles,
218            &statement.selected_expr,
219        )?;
220    }
221
222    let main_group = Group {
223        rows: selected_rows,
224    };
225
226    gitql_object.groups.push(main_group);
227
228    Ok(())
229}
230
231#[inline(always)]
232fn execute_expression_selection(
233    env: &mut Environment,
234    selected_rows: &mut [Row],
235    object_titles: &[String],
236    selected_expr_titles: &[String],
237    selected_expr: &[Box<dyn Expr>],
238) -> Result<(), String> {
239    // Cache the index of each expression position to provide fast insertion
240    let mut titles_index_map: HashMap<String, usize> = HashMap::new();
241    for expr_column_title in selected_expr_titles {
242        let expr_title_index = object_titles
243            .iter()
244            .position(|r| r.eq(expr_column_title))
245            .unwrap();
246        titles_index_map.insert(expr_column_title.to_string(), expr_title_index);
247    }
248
249    for row in selected_rows.iter_mut() {
250        for (index, expr) in selected_expr.iter().enumerate() {
251            let expr_title = &selected_expr_titles[index];
252            let value_index = *titles_index_map.get(expr_title).unwrap();
253
254            if index < row.values.len() && !row.values[value_index].is_null() {
255                continue;
256            }
257
258            // Ignore evaluating expression if it symbol, that mean it a reference to aggregated value or function
259            let value = if expr.kind() == ExprKind::Symbol {
260                Box::new(NullValue)
261            } else {
262                evaluate_expression(env, expr, object_titles, &row.values)?
263            };
264
265            if index >= row.values.len() {
266                row.values.push(value);
267            } else {
268                row.values[value_index] = value;
269            }
270        }
271    }
272    Ok(())
273}
274
275fn execute_where_statement(
276    env: &mut Environment,
277    statement: &WhereStatement,
278    gitql_object: &mut GitQLObject,
279) -> Result<(), String> {
280    if gitql_object.is_empty() {
281        return Ok(());
282    }
283
284    apply_filter_operation(
285        env,
286        &statement.condition,
287        &gitql_object.titles,
288        &mut gitql_object.groups[0].rows,
289    )?;
290
291    Ok(())
292}
293
294fn execute_having_statement(
295    env: &mut Environment,
296    statement: &HavingStatement,
297    gitql_object: &mut GitQLObject,
298) -> Result<(), String> {
299    if gitql_object.is_empty() {
300        return Ok(());
301    }
302
303    if gitql_object.len() > 1 {
304        gitql_object.flat()
305    }
306
307    // Perform where command only on the first group
308    // because group by command not executed yet
309    apply_filter_operation(
310        env,
311        &statement.condition,
312        &gitql_object.titles,
313        &mut gitql_object.groups[0].rows,
314    )?;
315
316    Ok(())
317}
318
319fn execute_qualify_statement(
320    env: &mut Environment,
321    statement: &QualifyStatement,
322    gitql_object: &mut GitQLObject,
323) -> Result<(), String> {
324    if gitql_object.is_empty() {
325        return Ok(());
326    }
327
328    apply_filter_operation(
329        env,
330        &statement.condition,
331        &gitql_object.titles,
332        &mut gitql_object.groups[0].rows,
333    )?;
334
335    Ok(())
336}
337
338fn execute_limit_statement(
339    statement: &LimitStatement,
340    gitql_object: &mut GitQLObject,
341) -> Result<(), String> {
342    if gitql_object.is_empty() {
343        return Ok(());
344    }
345
346    if gitql_object.len() > 1 {
347        gitql_object.flat()
348    }
349
350    let main_group: &mut Group = &mut gitql_object.groups[0];
351    if statement.count <= main_group.len() {
352        main_group.rows.drain(statement.count..main_group.len());
353    }
354
355    Ok(())
356}
357
358fn execute_offset_statement(
359    env: &mut Environment,
360    statement: &OffsetStatement,
361    gitql_object: &mut GitQLObject,
362) -> Result<(), String> {
363    if gitql_object.is_empty() {
364        return Ok(());
365    }
366
367    if gitql_object.len() > 1 {
368        gitql_object.flat()
369    }
370
371    let main_group: &mut Group = &mut gitql_object.groups[0];
372    let start = &evaluate_expression(
373        env,
374        &statement.start,
375        &gitql_object.titles,
376        &main_group.rows[0].values,
377    )?;
378
379    // If start evaluates to NULL, it is treated the same as OFFSET 0
380    if start.is_null() {
381        return Ok(());
382    }
383
384    let offset = start.as_int().unwrap() as usize;
385    let main_group: &mut Group = &mut gitql_object.groups[0];
386    main_group.rows.drain(0..cmp::min(offset, main_group.len()));
387
388    Ok(())
389}
390
391fn execute_aggregation_functions_statement(
392    env: &mut Environment,
393    statement: &AggregationsStatement,
394    gitql_object: &mut GitQLObject,
395    alias_table: &HashMap<String, String>,
396    is_query_has_group_by: bool,
397) -> Result<(), String> {
398    // Make sure you have at least one aggregation function to calculate
399    let aggregations_map = &statement.aggregations;
400    if aggregations_map.is_empty() {
401        return Ok(());
402    }
403
404    // We should run aggregation function for each group
405    for group in &mut gitql_object.groups {
406        // No need to apply all aggregation if there is no selected elements
407        if group.is_empty() {
408            continue;
409        }
410
411        // Resolve all aggregations functions first
412        for (result_column_name, aggregation) in aggregations_map {
413            if let AggregateValue::Function(function, arguments) = aggregation {
414                // Get alias name if exists or column name by default
415                let column_name = resolve_actual_column_name(alias_table, result_column_name);
416                let column_index = gitql_object
417                    .titles
418                    .iter()
419                    .position(|r| r.eq(&column_name))
420                    .unwrap();
421
422                // Evaluate the Arguments to Values
423                let mut group_arguments: Vec<Vec<Box<dyn Value>>> =
424                    Vec::with_capacity(group.rows.len());
425                for object in &mut group.rows {
426                    let mut row_values: Vec<Box<dyn Value>> =
427                        Vec::with_capacity(object.values.len());
428                    for argument in arguments {
429                        let value = evaluate_expression(
430                            env,
431                            argument,
432                            &gitql_object.titles,
433                            &object.values,
434                        )?;
435
436                        row_values.push(value);
437                    }
438
439                    group_arguments.push(row_values);
440                }
441
442                // Get the target aggregation function
443                let aggregation_function = env.aggregation_function(function).unwrap();
444                let result = &aggregation_function(&group_arguments);
445
446                // Insert the calculated value in the group objects
447                for object in &mut group.rows {
448                    if column_index < object.values.len() {
449                        object.values[column_index] = result.clone();
450                    } else {
451                        object.values.push(result.clone());
452                    }
453                }
454            }
455        }
456
457        // Resolve aggregations expressions
458        for (result_column_name, aggregation) in aggregations_map {
459            if let AggregateValue::Expression(expr) = aggregation {
460                // Get alias name if exists or column name by default
461                let column_name = resolve_actual_column_name(alias_table, result_column_name);
462                let column_index = gitql_object
463                    .titles
464                    .iter()
465                    .position(|r| r.eq(&column_name))
466                    .unwrap();
467
468                // Insert the calculated value in the group objects
469                for object in group.rows.iter_mut() {
470                    let result =
471                        evaluate_expression(env, expr, &gitql_object.titles, &object.values)?;
472                    if column_index < object.values.len() {
473                        object.values[column_index] = result.clone();
474                    } else {
475                        object.values.push(result.clone());
476                    }
477                }
478            }
479        }
480
481        // In case of group by statement is executed
482        // Remove all elements expect the first one
483        if is_query_has_group_by {
484            group.rows.drain(1..);
485        }
486    }
487
488    Ok(())
489}
490
491#[inline(always)]
492pub fn resolve_actual_column_name(alias_table: &HashMap<String, String>, name: &str) -> String {
493    if let Some(column_name) = alias_table.get(name) {
494        return column_name.to_string();
495    }
496
497    name.to_string()
498}