use crate::function::{AggFunc, Func}; use crate::schema::{Column, PseudoTable, Schema, Table}; use crate::sqlite3_ondisk::DatabaseHeader; use crate::translate::expr::{analyze_columns, maybe_apply_affinity, translate_expr}; use crate::translate::where_clause::{ process_where, translate_processed_where, translate_tableless_where, ProcessedWhereClause, }; use crate::translate::{normalize_ident, Insn}; use crate::types::{OwnedRecord, OwnedValue}; use crate::vdbe::{builder::ProgramBuilder, BranchOffset, Program}; use crate::Result; use sqlite3_parser::ast::{self, JoinOperator, JoinType, ResultColumn}; use std::cell::RefCell; use std::rc::Rc; /// A representation of a `SELECT` statement that has all the information /// needed for code generation. pub struct Select<'a> { /// Information about each column. pub column_info: Vec>, /// The tables we are retrieving data from, including tables mentioned /// in `FROM` and `JOIN` clauses. pub src_tables: Vec>, /// The `LIMIT` clause. pub limit: &'a Option, /// The `ORDER BY` clause. pub order_by: &'a Option>, /// Whether the query contains an aggregation function. pub exist_aggregation: bool, /// The `WHERE` clause. pub where_clause: &'a Option, } #[derive(Debug)] pub struct SrcTable<'a> { pub table: Table, pub identifier: String, pub join_info: Option<&'a ast::JoinedSelectTable>, } impl SrcTable<'_> { pub fn is_outer_join(&self) -> bool { if let Some(ast::JoinedSelectTable { operator: JoinOperator::TypedJoin(Some(join_type)), .. }) = self.join_info { if *join_type == JoinType::LEFT | JoinType::OUTER { true } else if *join_type == JoinType::RIGHT | JoinType::OUTER { true } else { false } } else { false } } } #[derive(Debug)] pub struct ColumnInfo<'a> { pub raw_column: &'a ast::ResultColumn, pub func: Option, pub args: &'a Option>, pub columns_to_allocate: usize, /* number of result columns this col will result on */ } impl<'a> ColumnInfo<'a> { pub fn new(raw_column: &'a ast::ResultColumn) -> Self { Self { raw_column, func: None, args: &None, columns_to_allocate: 1, } } pub fn is_aggregation_function(&self) -> bool { matches!(self.func, Some(Func::Agg(_))) } } #[derive(Debug)] pub struct LeftJoinBookkeeping { // integer register that holds a flag that is set to true if the current row has a match for the left join pub match_flag_register: usize, // label for the instruction that sets the match flag to true pub set_match_flag_true_label: BranchOffset, // label for the instruction that checks if the match flag is true pub check_match_flag_label: BranchOffset, // label for the instruction where the program jumps to if the current row has a match for the left join pub on_match_jump_to_label: BranchOffset, } /// Represents a single loop in an ordered list of opened read table loops. /// /// The list is used to generate inner loops like this: /// /// cursor 0 = open table 0 /// for each row in cursor 0 /// cursor 1 = open table 1 /// for each row in cursor 1 /// ... /// end cursor 1 /// end cursor 0 #[derive(Debug)] pub struct LoopInfo { // The table or table alias that we are looping over pub identifier: String, // Metadata about a left join, if any pub left_join_maybe: Option, // The label for the instruction that reads the next row for this table pub next_row_label: BranchOffset, // The label for the instruction that rewinds the cursor for this table pub rewind_label: BranchOffset, // The label for the instruction that is jumped to in the Rewind instruction if the table is empty pub rewind_on_empty_label: BranchOffset, // The ID of the cursor that is opened for this table pub open_cursor: usize, } struct LimitInfo { limit_reg: usize, num: i64, goto_label: BranchOffset, } #[derive(Debug)] struct SortInfo { sorter_cursor: usize, sorter_reg: usize, count: usize, } pub fn prepare_select<'a>(schema: &Schema, select: &'a ast::Select) -> Result> { match &select.body.select { ast::OneSelect::Select { columns, from: Some(from), where_clause, .. } => { let (table_name, maybe_alias) = match &from.select { Some(select_table) => match select_table.as_ref() { ast::SelectTable::Table(name, alias, ..) => ( &name.name, alias.as_ref().map(|als| match als { ast::As::As(alias) => alias, // users as u ast::As::Elided(alias) => alias, // users u }), ), _ => todo!(), }, None => todo!(), }; let table_name = &table_name.0; let maybe_alias = maybe_alias.map(|als| &als.0); let table = match schema.get_table(table_name) { Some(table) => table, None => crate::bail_parse_error!("no such table: {}", table_name), }; let identifier = normalize_ident(maybe_alias.unwrap_or(table_name)); let mut joins = Vec::new(); joins.push(SrcTable { table: Table::BTree(table.clone()), identifier, join_info: None, }); if let Some(selected_joins) = &from.joins { for join in selected_joins { let (table_name, maybe_alias) = match &join.table { ast::SelectTable::Table(name, alias, ..) => ( &name.name, alias.as_ref().map(|als| match als { ast::As::As(alias) => alias, // users as u ast::As::Elided(alias) => alias, // users u }), ), _ => todo!(), }; let table_name = &table_name.0; let maybe_alias = maybe_alias.as_ref().map(|als| &als.0); let table = match schema.get_table(table_name) { Some(table) => table, None => { crate::bail_parse_error!("no such table: {}", table_name) } }; let identifier = normalize_ident(maybe_alias.unwrap_or(table_name)); joins.push(SrcTable { table: Table::BTree(table), identifier, join_info: Some(join), }); } } let _table = Table::BTree(table); let column_info = analyze_columns(columns, &joins); let exist_aggregation = column_info .iter() .any(|info| info.is_aggregation_function()); Ok(Select { column_info, src_tables: joins, limit: &select.limit, order_by: &select.order_by, exist_aggregation, where_clause, }) } ast::OneSelect::Select { columns, from: None, where_clause, .. } => { let column_info = analyze_columns(columns, &Vec::new()); let exist_aggregation = column_info .iter() .any(|info| info.is_aggregation_function()); Ok(Select { column_info, src_tables: Vec::new(), limit: &select.limit, order_by: &select.order_by, where_clause, exist_aggregation, }) } _ => todo!(), } } /// Generate code for a SELECT statement. pub fn translate_select( mut select: Select, database_header: Rc>, ) -> Result { let mut program = ProgramBuilder::new(); let init_label = program.allocate_label(); let early_terminate_label = program.allocate_label(); program.emit_insn_with_label_dependency( Insn::Init { target_pc: init_label, }, init_label, ); let start_offset = program.offset(); let mut sort_info = if let Some(order_by) = select.order_by { let sorter_cursor = program.alloc_cursor_id(None, None); let mut order = Vec::new(); for col in order_by { order.push(OwnedValue::Integer(if let Some(ord) = col.order { ord as i64 } else { 0 })); } program.emit_insn(Insn::SorterOpen { cursor_id: sorter_cursor, order: OwnedRecord::new(order), columns: select.column_info.len() + 1, // +1 for the key }); Some(SortInfo { sorter_cursor, sorter_reg: 0, // will be overwritten later count: 0, // will be overwritten later }) } else { None }; let limit_info = if let Some(limit) = &select.limit { assert!(limit.offset.is_none()); let target_register = program.alloc_register(); let limit_reg = translate_expr( &mut program, Some(&select), &limit.expr, target_register, None, )?; let num = if let ast::Expr::Literal(ast::Literal::Numeric(num)) = &limit.expr { num.parse::()? } else { todo!(); }; let goto_label = program.allocate_label(); if num == 0 { program.emit_insn_with_label_dependency( Insn::Goto { target_pc: goto_label, }, goto_label, ); } Some(LimitInfo { limit_reg, num, goto_label, }) } else { None }; if !select.src_tables.is_empty() { let loops = translate_tables_begin(&mut program, &mut select, early_terminate_label)?; let (register_start, column_count) = if let Some(sort_columns) = select.order_by { let start = program.next_free_register(); for col in sort_columns.iter() { let target = program.alloc_register(); // if the ORDER BY expression is a number, interpret it as an 1-indexed column number // otherwise, interpret it normally as an expression let sort_col_expr = if let ast::Expr::Literal(ast::Literal::Numeric(num)) = &col.expr { let column_number = num.parse::()?; if column_number == 0 { crate::bail_parse_error!("invalid column index: {}", column_number); } let maybe_result_column = select .column_info .get(column_number - 1) .map(|col| &col.raw_column); match maybe_result_column { Some(ResultColumn::Expr(expr, _)) => expr, None => crate::bail_parse_error!("invalid column index: {}", column_number), _ => todo!(), } } else { &col.expr }; translate_expr(&mut program, Some(&select), sort_col_expr, target, None)?; } let (_, result_cols_count) = translate_columns(&mut program, &select, None)?; sort_info .as_mut() .map(|inner| inner.count = result_cols_count + sort_columns.len() + 1); // +1 for the key (start, result_cols_count + sort_columns.len()) } else { translate_columns(&mut program, &select, None)? }; if !select.exist_aggregation { if let Some(ref mut sort_info) = sort_info { let dest = program.alloc_register(); program.emit_insn(Insn::MakeRecord { start_reg: register_start, count: column_count, dest_reg: dest, }); program.emit_insn(Insn::SorterInsert { cursor_id: sort_info.sorter_cursor, record_reg: dest, }); sort_info.sorter_reg = register_start; } else { program.emit_insn(Insn::ResultRow { start_reg: register_start, count: column_count, }); emit_limit_insn(&limit_info, &mut program); } } translate_tables_end(&mut program, &loops); if select.exist_aggregation { program.resolve_label(early_terminate_label, program.offset()); let mut target = register_start; for info in &select.column_info { if let Some(Func::Agg(func)) = &info.func { program.emit_insn(Insn::AggFinal { register: target, func: func.clone(), }); } target += info.columns_to_allocate; } // only one result row program.emit_insn(Insn::ResultRow { start_reg: register_start, count: column_count, }); emit_limit_insn(&limit_info, &mut program); } } else { assert!(!select.exist_aggregation); assert!(sort_info.is_none()); let where_maybe = translate_tableless_where(&select, &mut program, early_terminate_label)?; let (register_start, count) = translate_columns(&mut program, &select, None)?; if let Some(where_clause_label) = where_maybe { program.resolve_label(where_clause_label, program.offset() + 1); } program.emit_insn(Insn::ResultRow { start_reg: register_start, count, }); emit_limit_insn(&limit_info, &mut program); }; // now do the sort for ORDER BY if select.order_by.is_some() { let _ = translate_sorter(&select, &mut program, &sort_info.unwrap(), &limit_info); } if !select.exist_aggregation { program.resolve_label(early_terminate_label, program.offset()); } program.emit_insn(Insn::Halt); let halt_offset = program.offset() - 1; if let Some(limit_info) = limit_info { if limit_info.goto_label < 0 { program.resolve_label(limit_info.goto_label, halt_offset); } } program.resolve_label(init_label, program.offset()); program.emit_insn(Insn::Transaction); program.emit_constant_insns(); program.emit_insn(Insn::Goto { target_pc: start_offset, }); program.resolve_deferred_labels(); Ok(program.build(database_header)) } fn emit_limit_insn(limit_info: &Option, program: &mut ProgramBuilder) { if limit_info.is_none() { return; } let limit_info = limit_info.as_ref().unwrap(); if limit_info.num > 0 { program.emit_insn_with_label_dependency( Insn::DecrJumpZero { reg: limit_info.limit_reg, target_pc: limit_info.goto_label, }, limit_info.goto_label, ); } } fn translate_sorter( select: &Select, program: &mut ProgramBuilder, sort_info: &SortInfo, limit_info: &Option, ) -> Result<()> { assert!(sort_info.count > 0); let mut pseudo_columns = Vec::new(); for col in select.column_info.iter() { match col.raw_column { ast::ResultColumn::Expr(expr, _) => match expr { ast::Expr::Id(ident) => { pseudo_columns.push(Column { name: normalize_ident(&ident.0), primary_key: false, ty: crate::schema::Type::Null, }); } ast::Expr::Qualified(table_name, ident) => { pseudo_columns.push(Column { name: normalize_ident(format!("{}.{}", table_name.0, ident.0).as_str()), primary_key: false, ty: crate::schema::Type::Null, }); } other => { todo!("translate_sorter: {:?}", other); } }, ast::ResultColumn::Star => {} ast::ResultColumn::TableStar(_) => {} } } let pseudo_cursor = program.alloc_cursor_id( None, Some(Table::Pseudo(Rc::new(PseudoTable { columns: pseudo_columns, }))), ); let pseudo_content_reg = program.alloc_register(); program.emit_insn(Insn::OpenPseudo { cursor_id: pseudo_cursor, content_reg: pseudo_content_reg, num_fields: sort_info.count, }); let label = program.allocate_label(); program.emit_insn_with_label_dependency( Insn::SorterSort { cursor_id: sort_info.sorter_cursor, pc_if_empty: label, }, label, ); let sorter_data_offset = program.offset(); program.emit_insn(Insn::SorterData { cursor_id: sort_info.sorter_cursor, dest_reg: pseudo_content_reg, pseudo_cursor, }); let (register_start, count) = translate_columns(program, select, Some(pseudo_cursor))?; program.emit_insn(Insn::ResultRow { start_reg: register_start, count, }); emit_limit_insn(limit_info, program); program.emit_insn(Insn::SorterNext { cursor_id: sort_info.sorter_cursor, pc_if_next: sorter_data_offset, }); program.resolve_label(label, program.offset()); Ok(()) } fn translate_tables_begin( program: &mut ProgramBuilder, select: &mut Select, early_terminate_label: BranchOffset, ) -> Result> { let processed_where = process_where(select)?; let mut loops = Vec::with_capacity(select.src_tables.len()); for idx in &processed_where.loop_order { let join = select .src_tables .get(*idx) .expect("loop order out of bounds"); let loop_info = translate_table_open_cursor(program, join); loops.push(loop_info); } for loop_info in &loops { // early_terminate_label decides where to jump _IF_ there exists a condition on this loop that is always false. // this is part of a constant folding optimization where we can skip the loop entirely if we know it will never produce any rows. let current_loop_early_terminate_label = if let Some(left_join) = &loop_info.left_join_maybe { // If there exists a condition on the LEFT JOIN that is always false, e.g.: // 'SELECT * FROM x LEFT JOIN y ON false' // then we can't jump to e.g. Halt, but instead we need to still emit all rows from the 'x' table, with NULLs for the 'y' table. // 'check_match_flag_label' is the label that checks if the left join match flag has been set to true, and if not (which it by default isn't), // sets the 'y' cursor's "pseudo null bit" on, which means any Insn::Column after that will return NULL for the 'y' table. left_join.check_match_flag_label } else { // If there exists a condition in an INNER JOIN (or WHERE) that is always false, then the query will not produce any rows. // Example: 'SELECT * FROM x JOIN y ON false' or 'SELECT * FROM x WHERE false' // Here we should jump to Halt (or e.g. AggFinal in case we have an aggregation expression like count() that should produce a 0 on empty input. early_terminate_label }; translate_table_open_loop( program, select, loop_info, &processed_where, current_loop_early_terminate_label, )?; } Ok(loops) } fn translate_tables_end(program: &mut ProgramBuilder, loops: &[LoopInfo]) { // iterate in reverse order as we open cursors in order for table_loop in loops.iter().rev() { let cursor_id = table_loop.open_cursor; program.resolve_label(table_loop.next_row_label, program.offset()); program.emit_insn(Insn::NextAsync { cursor_id }); program.emit_insn_with_label_dependency( Insn::NextAwait { cursor_id, pc_if_next: table_loop.rewind_label, }, table_loop.rewind_label, ); if let Some(left_join) = &table_loop.left_join_maybe { left_join_match_flag_check(program, left_join, cursor_id); } } } fn translate_table_open_cursor(program: &mut ProgramBuilder, table: &SrcTable) -> LoopInfo { let cursor_id = program.alloc_cursor_id(Some(table.identifier.clone()), Some(table.table.clone())); let root_page = match &table.table { Table::BTree(btree) => btree.root_page, Table::Pseudo(_) => todo!(), }; program.emit_insn(Insn::OpenReadAsync { cursor_id, root_page, }); program.emit_insn(Insn::OpenReadAwait); LoopInfo { identifier: table.identifier.clone(), left_join_maybe: if table.is_outer_join() { Some(LeftJoinBookkeeping { match_flag_register: program.alloc_register(), on_match_jump_to_label: program.allocate_label(), check_match_flag_label: program.allocate_label(), set_match_flag_true_label: program.allocate_label(), }) } else { None }, open_cursor: cursor_id, next_row_label: program.allocate_label(), rewind_label: program.allocate_label(), rewind_on_empty_label: program.allocate_label(), } } /** * initialize left join match flag to false * if condition checks pass, it will eventually be set to true */ fn left_join_match_flag_initialize(program: &mut ProgramBuilder, left_join: &LeftJoinBookkeeping) { program.emit_insn(Insn::Integer { value: 0, dest: left_join.match_flag_register, }); } /** * after the relevant conditional jumps have been emitted, set the left join match flag to true */ fn left_join_match_flag_set_true(program: &mut ProgramBuilder, left_join: &LeftJoinBookkeeping) { program.defer_label_resolution( left_join.set_match_flag_true_label, program.offset() as usize, ); program.emit_insn(Insn::Integer { value: 1, dest: left_join.match_flag_register, }); } /** * check if the left join match flag is set to true * if it is, jump to the next row on the outer table * if not, set the right table cursor's "pseudo null bit" on * then jump to setting the left join match flag to true again, * which will effectively emit all nulls for the right table. */ fn left_join_match_flag_check( program: &mut ProgramBuilder, left_join: &LeftJoinBookkeeping, cursor_id: usize, ) { // If the left join match flag has been set to 1, we jump to the next row on the outer table (result row has been emitted already) program.resolve_label(left_join.check_match_flag_label, program.offset()); program.emit_insn_with_label_dependency( Insn::IfPos { reg: left_join.match_flag_register, target_pc: left_join.on_match_jump_to_label, decrement_by: 0, }, left_join.on_match_jump_to_label, ); // If not, we set the right table cursor's "pseudo null bit" on, which means any Insn::Column will return NULL program.emit_insn(Insn::NullRow { cursor_id }); // Jump to setting the left join match flag to 1 again, but this time the right table cursor will set everything to null program.emit_insn_with_label_dependency( Insn::Goto { target_pc: left_join.set_match_flag_true_label, }, left_join.set_match_flag_true_label, ); // This points to the NextAsync instruction of the next table in the loop // (i.e. the outer table, since we're iterating in reverse order) program.resolve_label(left_join.on_match_jump_to_label, program.offset()); } fn translate_table_open_loop( program: &mut ProgramBuilder, select: &Select, loop_info: &LoopInfo, w: &ProcessedWhereClause, early_terminate_label: BranchOffset, ) -> Result<()> { if let Some(left_join) = loop_info.left_join_maybe.as_ref() { left_join_match_flag_initialize(program, left_join); } program.emit_insn(Insn::RewindAsync { cursor_id: loop_info.open_cursor, }); program.defer_label_resolution(loop_info.rewind_label, program.offset() as usize); program.emit_insn_with_label_dependency( Insn::RewindAwait { cursor_id: loop_info.open_cursor, pc_if_empty: loop_info.rewind_on_empty_label, }, loop_info.rewind_on_empty_label, ); translate_processed_where(program, select, loop_info, w, early_terminate_label, None)?; if let Some(left_join) = loop_info.left_join_maybe.as_ref() { left_join_match_flag_set_true(program, left_join); } Ok(()) } fn translate_columns( program: &mut ProgramBuilder, select: &Select, cursor_hint: Option, ) -> Result<(usize, usize)> { let register_start = program.next_free_register(); // allocate one register as output for each col let registers: usize = select .column_info .iter() .map(|col| col.columns_to_allocate) .sum(); program.alloc_registers(registers); let count = program.next_free_register() - register_start; let mut target = register_start; for info in select.column_info.iter() { translate_column(program, select, info.raw_column, info, target, cursor_hint)?; target += info.columns_to_allocate; } Ok((register_start, count)) } fn translate_column( program: &mut ProgramBuilder, select: &Select, col: &ast::ResultColumn, info: &ColumnInfo, target_register: usize, // where to store the result, in case of star it will be the start of registers added cursor_hint: Option, ) -> Result<()> { match col { ast::ResultColumn::Expr(expr, _) => { if info.is_aggregation_function() { let _ = translate_aggregation( program, select, expr, info, target_register, cursor_hint, )?; } else { let _ = translate_expr(program, Some(select), expr, target_register, cursor_hint)?; } } ast::ResultColumn::Star => { let mut target_register = target_register; for join in &select.src_tables { translate_table_star(join, program, target_register, cursor_hint); target_register += &join.table.columns().len(); } } ast::ResultColumn::TableStar(_) => todo!(), } Ok(()) } fn translate_table_star( table: &SrcTable, program: &mut ProgramBuilder, target_register: usize, cursor_hint: Option, ) { let table_cursor = program.resolve_cursor_id(&table.identifier, cursor_hint); let table = &table.table; for (i, col) in table.columns().iter().enumerate() { let col_target_register = target_register + i; if table.column_is_rowid_alias(col) { program.emit_insn(Insn::RowId { cursor_id: table_cursor, dest: col_target_register, }); } else { program.emit_insn(Insn::Column { column: i, dest: col_target_register, cursor_id: table_cursor, }); maybe_apply_affinity(col.ty, col_target_register, program); } } } fn translate_aggregation( program: &mut ProgramBuilder, select: &Select, expr: &ast::Expr, info: &ColumnInfo, target_register: usize, cursor_hint: Option, ) -> Result { let _ = expr; assert!(info.func.is_some()); let func = info.func.as_ref().unwrap(); let empty_args = &Vec::::new(); let args = info.args.as_ref().unwrap_or(empty_args); let dest = match func { Func::Scalar(_) => { crate::bail_parse_error!("single row function in aggregation") } Func::Agg(agg_func) => match agg_func { AggFunc::Avg => { if args.len() != 1 { crate::bail_parse_error!("avg bad number of arguments"); } let expr = &args[0]; let expr_reg = program.alloc_register(); let _ = translate_expr(program, Some(select), expr, expr_reg, cursor_hint)?; program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: 0, func: AggFunc::Avg, }); target_register } AggFunc::Count => { let expr_reg = if args.is_empty() { program.alloc_register() } else { let expr = &args[0]; let expr_reg = program.alloc_register(); let _ = translate_expr(program, Some(select), expr, expr_reg, cursor_hint); expr_reg }; program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: 0, func: AggFunc::Count, }); target_register } AggFunc::GroupConcat => { if args.len() != 1 && args.len() != 2 { crate::bail_parse_error!("group_concat bad number of arguments"); } let expr_reg = program.alloc_register(); let delimiter_reg = program.alloc_register(); let expr = &args[0]; let delimiter_expr: ast::Expr; if args.len() == 2 { match &args[1] { ast::Expr::Id(ident) => { if ident.0.starts_with('"') { delimiter_expr = ast::Expr::Literal(ast::Literal::String(ident.0.to_string())); } else { delimiter_expr = args[1].clone(); } } ast::Expr::Literal(ast::Literal::String(s)) => { delimiter_expr = ast::Expr::Literal(ast::Literal::String(s.to_string())); } _ => crate::bail_parse_error!("Incorrect delimiter parameter"), }; } else { delimiter_expr = ast::Expr::Literal(ast::Literal::String(String::from("\",\""))); } translate_expr(program, Some(select), expr, expr_reg, cursor_hint)?; translate_expr( program, Some(select), &delimiter_expr, delimiter_reg, cursor_hint, )?; program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: delimiter_reg, func: AggFunc::GroupConcat, }); target_register } AggFunc::Max => { if args.len() != 1 { crate::bail_parse_error!("max bad number of arguments"); } let expr = &args[0]; let expr_reg = program.alloc_register(); let _ = translate_expr(program, Some(select), expr, expr_reg, cursor_hint); program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: 0, func: AggFunc::Max, }); target_register } AggFunc::Min => { if args.len() != 1 { crate::bail_parse_error!("min bad number of arguments"); } let expr = &args[0]; let expr_reg = program.alloc_register(); let _ = translate_expr(program, Some(select), expr, expr_reg, cursor_hint); program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: 0, func: AggFunc::Min, }); target_register } AggFunc::StringAgg => { if args.len() != 2 { crate::bail_parse_error!("string_agg bad number of arguments"); } let expr_reg = program.alloc_register(); let delimiter_reg = program.alloc_register(); let expr = &args[0]; let delimiter_expr: ast::Expr; match &args[1] { ast::Expr::Id(ident) => { if ident.0.starts_with('"') { crate::bail_parse_error!("no such column: \",\" - should this be a string literal in single-quotes?"); } else { delimiter_expr = args[1].clone(); } } ast::Expr::Literal(ast::Literal::String(s)) => { delimiter_expr = ast::Expr::Literal(ast::Literal::String(s.to_string())); } _ => crate::bail_parse_error!("Incorrect delimiter parameter"), }; translate_expr(program, Some(select), expr, expr_reg, cursor_hint)?; translate_expr( program, Some(select), &delimiter_expr, delimiter_reg, cursor_hint, )?; program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: delimiter_reg, func: AggFunc::StringAgg, }); target_register } AggFunc::Sum => { if args.len() != 1 { crate::bail_parse_error!("sum bad number of arguments"); } let expr = &args[0]; let expr_reg = program.alloc_register(); let _ = translate_expr(program, Some(select), expr, expr_reg, cursor_hint)?; program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: 0, func: AggFunc::Sum, }); target_register } AggFunc::Total => { if args.len() != 1 { crate::bail_parse_error!("total bad number of arguments"); } let expr = &args[0]; let expr_reg = program.alloc_register(); let _ = translate_expr(program, Some(select), expr, expr_reg, cursor_hint)?; program.emit_insn(Insn::AggStep { acc_reg: target_register, col: expr_reg, delimiter: 0, func: AggFunc::Total, }); target_register } }, }; Ok(dest) }