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