diff --git a/core/translate/aggregation.rs b/core/translate/aggregation.rs index 52a19962b..d55a8927a 100644 --- a/core/translate/aggregation.rs +++ b/core/translate/aggregation.rs @@ -38,10 +38,10 @@ pub fn emit_ungrouped_aggregation<'a>( // we need to call translate_expr on each result column, but replace the expr with a register copy in case any part of the // result column expression matches a) a group by column or b) an aggregation result. for (i, agg) in plan.aggregates.iter().enumerate() { - t_ctx - .resolver - .expr_to_reg_cache - .push((&agg.original_expr, agg_start_reg + i)); + t_ctx.resolver.expr_to_reg_cache.push(( + std::borrow::Cow::Borrowed(&agg.original_expr), + agg_start_reg + i, + )); } t_ctx.resolver.enable_expr_to_reg_cache(); diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 49e2f485e..cd962562d 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -1,7 +1,6 @@ // This module contains code for emitting bytecode instructions for SQL query execution. // It handles translating high-level SQL operations into low-level bytecode that can be executed by the virtual machine. -use std::collections::HashSet; use std::num::NonZeroUsize; use std::sync::Arc; @@ -24,18 +23,20 @@ use super::select::emit_simple_count; use super::subquery::emit_from_clause_subqueries; use crate::error::SQLITE_CONSTRAINT_PRIMARYKEY; use crate::function::Func; -use crate::schema::{BTreeTable, Column, Schema, Table, ROWID_SENTINEL}; +use crate::schema::{BTreeTable, Column, Index, Schema, Table, ROWID_SENTINEL}; use crate::translate::compound_select::emit_program_for_compound_select; use crate::translate::expr::{ emit_returning_results, translate_expr_no_constant_opt, walk_expr_mut, NoConstantOptReason, - ReturningValueRegisters, WalkControl, + WalkControl, }; use crate::translate::fkeys::{ build_index_affinity_string, emit_fk_child_update_counters, emit_fk_delete_parent_existence_checks, emit_guarded_fk_decrement, emit_parent_key_change_checks, open_read_index, open_read_table, stabilize_new_row_for_fk, }; -use crate::translate::plan::{DeletePlan, EvalAt, JoinedTable, Plan, QueryDestination, Search}; +use crate::translate::plan::{ + DeletePlan, EvalAt, JoinedTable, Plan, QueryDestination, ResultSetColumn, Search, +}; use crate::translate::planner::ROWID_STRS; use crate::translate::subquery::emit_non_from_clause_subquery; use crate::translate::values::emit_values; @@ -51,7 +52,7 @@ pub struct Resolver<'a> { pub schema: &'a Schema, pub symbol_table: &'a SymbolTable, pub expr_to_reg_cache_enabled: bool, - pub expr_to_reg_cache: Vec<(&'a ast::Expr, usize)>, + pub expr_to_reg_cache: Vec<(std::borrow::Cow<'a, ast::Expr>, usize)>, } impl<'a> Resolver<'a> { @@ -665,12 +666,12 @@ pub fn emit_fk_child_decrement_on_delete( Ok(()) } -fn emit_delete_insns( +fn emit_delete_insns<'a>( connection: &Arc, program: &mut ProgramBuilder, - t_ctx: &mut TranslateCtx, + t_ctx: &mut TranslateCtx<'a>, table_references: &mut TableReferences, - result_columns: &[super::plan::ResultSetColumn], + result_columns: &'a [super::plan::ResultSetColumn], ) -> Result<()> { // we can either use this obviously safe raw pointer or we can clone it let table_reference: *const JoinedTable = table_references.joined_tables().first().unwrap(); @@ -873,13 +874,14 @@ fn emit_delete_insns( } // Emit RETURNING results using the values we just read - let value_registers = ReturningValueRegisters { - rowid_register: rowid_reg, - columns_start_register: columns_start_reg, - num_columns: cols_len, - }; - - emit_returning_results(program, result_columns, &value_registers)?; + emit_returning_results( + program, + table_references, + result_columns, + columns_start_reg, + rowid_reg, + &mut t_ctx.resolver, + )?; } program.emit_insn(Insn::Delete { @@ -1042,8 +1044,13 @@ fn emit_program_for_update( // Emit update instructions emit_update_insns( connection, - &mut plan, - &t_ctx, + &mut plan.table_references, + &plan.set_clauses, + plan.cdc_update_alter_statement.as_deref(), + &plan.indexes_to_update, + plan.returning.as_ref(), + plan.ephemeral_plan.as_ref(), + &mut t_ctx, program, index_cursors, iteration_cursor_id, @@ -1077,10 +1084,16 @@ fn emit_program_for_update( /// `target_table_cursor_id` is the cursor id of the table that is being updated. /// /// `target_table` is the table that is being updated. -fn emit_update_insns( +#[allow(clippy::too_many_arguments)] +fn emit_update_insns<'a>( connection: &Arc, - plan: &mut UpdatePlan, - t_ctx: &TranslateCtx, + table_references: &mut TableReferences, + set_clauses: &[(usize, Box)], + cdc_update_alter_statement: Option<&str>, + indexes_to_update: &[Arc], + returning: Option<&'a Vec>, + ephemeral_plan: Option<&SelectPlan>, + t_ctx: &mut TranslateCtx<'a>, program: &mut ProgramBuilder, index_cursors: Vec<(usize, usize)>, iteration_cursor_id: usize, @@ -1089,7 +1102,7 @@ fn emit_update_insns( ) -> crate::Result<()> { let internal_id = target_table.internal_id; let loop_labels = t_ctx.labels_main_loop.first().unwrap(); - let source_table = plan.table_references.joined_tables().first().unwrap(); + let source_table = table_references.joined_tables().first().unwrap(); let (index, is_virtual) = match &source_table.op { Operation::Scan(Scan::BTreeTable { index, .. }) => ( index.as_ref().map(|index| { @@ -1138,13 +1151,10 @@ fn emit_update_insns( .iter() .position(|c| c.is_rowid_alias()); - let has_direct_rowid_update = plan - .set_clauses - .iter() - .any(|(idx, _)| *idx == ROWID_SENTINEL); + let has_direct_rowid_update = set_clauses.iter().any(|(idx, _)| *idx == ROWID_SENTINEL); let has_user_provided_rowid = if let Some(index) = rowid_alias_index { - plan.set_clauses.iter().any(|(idx, _)| *idx == index) + set_clauses.iter().any(|(idx, _)| *idx == index) } else { has_direct_rowid_update }; @@ -1210,11 +1220,11 @@ fn emit_update_insns( let start = if is_virtual { beg + 2 } else { beg + 1 }; if has_direct_rowid_update { - if let Some((_, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == ROWID_SENTINEL) { + if let Some((_, expr)) = set_clauses.iter().find(|(i, _)| *i == ROWID_SENTINEL) { let rowid_set_clause_reg = rowid_set_clause_reg.unwrap(); translate_expr( program, - Some(&plan.table_references), + Some(table_references), expr, rowid_set_clause_reg, &t_ctx.resolver, @@ -1226,7 +1236,7 @@ fn emit_update_insns( } for (idx, table_column) in target_table.table.columns().iter().enumerate() { let target_reg = start + idx; - if let Some((col_idx, expr)) = plan.set_clauses.iter().find(|(i, _)| *i == idx) { + if let Some((col_idx, expr)) = set_clauses.iter().find(|(i, _)| *i == idx) { // Skip if this is the sentinel value if *col_idx == ROWID_SENTINEL { continue; @@ -1238,7 +1248,7 @@ fn emit_update_insns( let rowid_set_clause_reg = rowid_set_clause_reg.unwrap(); translate_expr( program, - Some(&plan.table_references), + Some(table_references), expr, rowid_set_clause_reg, &t_ctx.resolver, @@ -1252,7 +1262,7 @@ fn emit_update_insns( } else { translate_expr( program, - Some(&plan.table_references), + Some(table_references), expr, target_reg, &t_ctx.resolver, @@ -1280,9 +1290,9 @@ fn emit_update_insns( program.emit_bool(true, change_reg); program.mark_last_insn_constant(); let mut updated = false; - if let Some(ddl_query_for_cdc_update) = &plan.cdc_update_alter_statement { + if let Some(ddl_query_for_cdc_update) = &cdc_update_alter_statement { if table_column.name.as_deref() == Some("sql") { - program.emit_string8(ddl_query_for_cdc_update.clone(), value_reg); + program.emit_string8(ddl_query_for_cdc_update.to_string(), value_reg); updated = true; } } @@ -1346,7 +1356,7 @@ fn emit_update_insns( stabilize_new_row_for_fk( program, &table_btree, - &plan.set_clauses, + set_clauses, target_table_cursor_id, start, rowid_new_reg, @@ -1362,11 +1372,10 @@ fn emit_update_insns( target_table_cursor_id, start, rowid_new_reg, - &plan - .set_clauses + &set_clauses .iter() .map(|(i, _)| *i) - .collect::>(), + .collect::>(), )?; } // Parent-side checks: @@ -1382,19 +1391,19 @@ fn emit_update_insns( program, &t_ctx.resolver, &table_btree, - plan.indexes_to_update.iter(), + indexes_to_update.iter(), target_table_cursor_id, beg, start, rowid_new_reg, rowid_set_clause_reg, - &plan.set_clauses, + set_clauses, )?; } } } - for (index, (idx_cursor_id, record_reg)) in plan.indexes_to_update.iter().zip(&index_cursors) { + for (index, (idx_cursor_id, record_reg)) in indexes_to_update.iter().zip(&index_cursors) { // We need to know whether or not the OLD values satisfied the predicate on the // partial index, so we can know whether or not to delete the old index entry, // as well as whether or not the NEW values satisfy the predicate, to determine whether @@ -1403,12 +1412,12 @@ fn emit_update_insns( // This means that we need to bind the column references to a copy of the index Expr, // so we can emit Insn::Column instructions and refer to the old values. let where_clause = index - .bind_where_expr(Some(&mut plan.table_references), connection) + .bind_where_expr(Some(table_references), connection) .expect("where clause to exist"); let old_satisfied_reg = program.alloc_register(); translate_expr_no_constant_opt( program, - Some(&plan.table_references), + Some(table_references), &where_clause, old_satisfied_reg, &t_ctx.resolver, @@ -1744,15 +1753,16 @@ fn emit_update_insns( }); // Emit RETURNING results if specified - if let Some(returning_columns) = &plan.returning { + if let Some(returning_columns) = &returning { if !returning_columns.is_empty() { - let value_registers = ReturningValueRegisters { - rowid_register: rowid_set_clause_reg.unwrap_or(beg), - columns_start_register: start, - num_columns: col_len, - }; - - emit_returning_results(program, returning_columns, &value_registers)?; + emit_returning_results( + program, + table_references, + returning_columns, + start, + rowid_set_clause_reg.unwrap_or(beg), + &mut t_ctx.resolver, + )?; } } @@ -1814,7 +1824,7 @@ fn emit_update_insns( emit_cdc_insns( program, &t_ctx.resolver, - OperationMode::UPDATE(if plan.ephemeral_plan.is_some() { + OperationMode::UPDATE(if ephemeral_plan.is_some() { UpdateRowSource::PrebuiltEphemeralTable { ephemeral_table_cursor_id: iteration_cursor_id, target_table: target_table.clone(), diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 2f0f9a4a9..fbda8897d 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use tracing::{instrument, Level}; -use turso_parser::ast::{self, As, Expr, SubqueryType, UnaryOperator}; +use turso_parser::ast::{self, Expr, SubqueryType, UnaryOperator}; use super::emitter::Resolver; use super::optimizer::Optimizable; @@ -22,7 +22,7 @@ use crate::vdbe::{ insn::{CmpInsFlags, Insn}, BranchOffset, }; -use crate::{Result, Value}; +use crate::{turso_assert, Result, Value}; use super::collate::CollationSeq; @@ -34,16 +34,6 @@ pub struct ConditionMetadata { pub jump_target_when_null: BranchOffset, } -/// Container for register locations of values that can be referenced in RETURNING expressions -pub struct ReturningValueRegisters { - /// Register containing the rowid/primary key - pub rowid_register: usize, - /// Starting register for column values (in column order) - pub columns_start_register: usize, - /// Number of columns available - pub num_columns: usize, -} - #[instrument(skip_all, level = Level::DEBUG)] fn emit_cond_jump(program: &mut ProgramBuilder, cond_meta: ConditionMetadata, reg: usize) { if cond_meta.jump_if_condition_is_true { @@ -4143,106 +4133,6 @@ pub fn compare_affinity( } } -/// Evaluate a RETURNING expression using register-based evaluation instead of cursor-based. -/// This is used for RETURNING clauses where we have register values instead of cursor data. -pub fn translate_expr_for_returning( - program: &mut ProgramBuilder, - expr: &Expr, - value_registers: &ReturningValueRegisters, - target_register: usize, -) -> Result { - match expr { - Expr::Column { - column, - is_rowid_alias, - .. - } => { - if *is_rowid_alias { - // For rowid references, copy from the rowid register - program.emit_insn(Insn::Copy { - src_reg: value_registers.rowid_register, - dst_reg: target_register, - extra_amount: 0, - }); - } else { - // For regular column references, copy from the appropriate column register - let column_idx = *column; - if column_idx < value_registers.num_columns { - let column_reg = value_registers.columns_start_register + column_idx; - program.emit_insn(Insn::Copy { - src_reg: column_reg, - dst_reg: target_register, - extra_amount: 0, - }); - } else { - crate::bail_parse_error!("Column index out of bounds in RETURNING clause"); - } - } - Ok(target_register) - } - Expr::RowId { .. } => { - // For ROWID expressions, copy from the rowid register - program.emit_insn(Insn::Copy { - src_reg: value_registers.rowid_register, - dst_reg: target_register, - extra_amount: 0, - }); - Ok(target_register) - } - Expr::Literal(literal) => emit_literal(program, literal, target_register), - Expr::Binary(lhs, op, rhs) => { - let lhs_reg = program.alloc_register(); - let rhs_reg = program.alloc_register(); - - // Recursively evaluate left-hand side - translate_expr_for_returning(program, lhs, value_registers, lhs_reg)?; - - // Recursively evaluate right-hand side - translate_expr_for_returning(program, rhs, value_registers, rhs_reg)?; - - // Use the shared emit_binary_insn function - emit_binary_insn( - program, - op, - lhs_reg, - rhs_reg, - target_register, - lhs, - rhs, - None, // No table references needed for RETURNING - None, // No condition metadata needed for RETURNING - )?; - - Ok(target_register) - } - Expr::FunctionCall { name, args, .. } => { - // Evaluate arguments into registers - let mut arg_regs = Vec::new(); - for arg in args.iter() { - let arg_reg = program.alloc_register(); - translate_expr_for_returning(program, arg, value_registers, arg_reg)?; - arg_regs.push(arg_reg); - } - - // Resolve and call the function using shared helper - let func = Func::resolve_function(name.as_str(), arg_regs.len())?; - let func_ctx = FuncCtx { - func, - arg_count: arg_regs.len(), - }; - - emit_function_call(program, func_ctx, &arg_regs, target_register)?; - Ok(target_register) - } - _ => { - crate::bail_parse_error!( - "Unsupported expression type in RETURNING clause: {:?}", - expr - ); - } - } -} - /// Emit literal values - shared between regular and RETURNING expression evaluation pub fn emit_literal( program: &mut ProgramBuilder, @@ -4363,56 +4253,40 @@ pub fn emit_function_call( /// with proper column binding and alias handling. pub fn process_returning_clause( returning: &mut [ast::ResultColumn], - table: &Table, - table_name: &str, - program: &mut ProgramBuilder, + table_references: &mut TableReferences, connection: &std::sync::Arc, -) -> Result<( - Vec, - super::plan::TableReferences, -)> { - use super::plan::{ColumnUsedMask, JoinedTable, Operation, ResultSetColumn, TableReferences}; +) -> Result> { + let mut result_columns = Vec::with_capacity(returning.len()); - let mut result_columns = vec![]; - - let internal_id = program.table_reference_counter.next(); - let mut table_references = TableReferences::new( - vec![JoinedTable { - table: match table { - Table::Virtual(vtab) => Table::Virtual(vtab.clone()), - Table::BTree(btree_table) => Table::BTree(btree_table.clone()), - _ => unreachable!(), - }, - identifier: table_name.to_string(), - internal_id, - op: Operation::default_scan_for(table), - join_info: None, - col_used_mask: ColumnUsedMask::default(), - database_id: 0, - }], - vec![], - ); + let alias_to_string = |alias: &ast::As| match alias { + ast::As::Elided(alias) => alias.as_str().to_string(), + ast::As::As(alias) => alias.as_str().to_string(), + }; for rc in returning.iter_mut() { match rc { ast::ResultColumn::Expr(expr, alias) => { bind_and_rewrite_expr( expr, - Some(&mut table_references), + Some(table_references), None, connection, BindingBehavior::TryResultColumnsFirst, )?; - let column_alias = determine_column_alias(expr, alias, table); - result_columns.push(ResultSetColumn { - expr: *expr.clone(), - alias: column_alias, + expr: expr.as_ref().clone(), + alias: alias.as_ref().map(alias_to_string), contains_aggregates: false, }); } ast::ResultColumn::Star => { + let table = table_references + .joined_tables() + .first() + .expect("RETURNING clause must reference at least one table"); + let internal_id = table.internal_id; + // Handle RETURNING * by expanding to all table columns // Use the shared internal_id for all columns for (column_index, column) in table.columns().iter().enumerate() { @@ -4420,7 +4294,7 @@ pub fn process_returning_clause( database: None, table: internal_id, column: column_index, - is_rowid_alias: false, + is_rowid_alias: column.is_rowid_alias(), }; result_columns.push(ResultSetColumn { @@ -4430,91 +4304,81 @@ pub fn process_returning_clause( }); } } - ast::ResultColumn::TableStar(_table_name) => { - // Handle RETURNING table.* by expanding to all table columns - // For single table RETURNING, this is equivalent to * - for (column_index, column) in table.columns().iter().enumerate() { - let column_expr = Expr::Column { - database: None, - table: internal_id, - column: column_index, - is_rowid_alias: false, - }; - - result_columns.push(ResultSetColumn { - expr: column_expr, - alias: column.name.clone(), - contains_aggregates: false, - }); - } + ast::ResultColumn::TableStar(_) => { + crate::bail_parse_error!("RETURNING may not use \"TABLE.*\" wildcards"); } } } - Ok((result_columns, table_references)) -} - -/// Determine the appropriate alias for a RETURNING column expression -fn determine_column_alias( - expr: &Expr, - explicit_alias: &Option, - table: &Table, -) -> Option { - // First check for explicit alias - if let Some(As::As(name)) = explicit_alias { - return Some(name.as_str().to_string()); - } - - // For ROWID expressions, use "rowid" as the alias - if let Expr::RowId { .. } = expr { - return Some("rowid".to_string()); - } - - // For column references, always use the column name from the table - if let Expr::Column { - column, - is_rowid_alias, - .. - } = expr - { - if let Some(name) = table - .columns() - .get(*column) - .and_then(|col| col.name.clone()) - { - return Some(name); - } else if *is_rowid_alias { - // If it's a rowid alias, return "rowid" - return Some("rowid".to_string()); - } else { - return None; - } - } - - // For other expressions, use the expression string representation - Some(expr.to_string()) + Ok(result_columns) } /// Emit bytecode to evaluate RETURNING expressions and produce result rows. -/// This function handles the actual evaluation of expressions using the values -/// from the DML operation. -pub(crate) fn emit_returning_results( +/// RETURNING result expressions are otherwise evaluated as normal, but the columns of the target table +/// are added to [Resolver::expr_to_reg_cache], meaning a reference to e.g tbl.col will effectively +/// refer to a register where the OLD/NEW value of tbl.col is stored after an INSERT/UPDATE/DELETE. +pub(crate) fn emit_returning_results<'a>( program: &mut ProgramBuilder, + table_references: &TableReferences, result_columns: &[super::plan::ResultSetColumn], - value_registers: &ReturningValueRegisters, + reg_columns_start: usize, + rowid_reg: usize, + resolver: &mut Resolver<'a>, ) -> Result<()> { if result_columns.is_empty() { return Ok(()); } + turso_assert!(table_references.joined_tables().len() == 1, "RETURNING is only used with INSERT, UPDATE, or DELETE statements, which target a single table"); + let table = table_references.joined_tables().first().unwrap(); + + resolver.enable_expr_to_reg_cache(); + let expr = Expr::RowId { + database: None, + table: table.internal_id, + }; + let cache_len = resolver.expr_to_reg_cache.len(); + resolver + .expr_to_reg_cache + .push((std::borrow::Cow::Owned(expr), rowid_reg)); + for (i, column) in table.columns().iter().enumerate() { + let reg = if column.is_rowid_alias() { + rowid_reg + } else { + reg_columns_start + i + }; + let expr = Expr::Column { + database: None, + table: table.internal_id, + column: i, + is_rowid_alias: column.is_rowid_alias(), + }; + resolver + .expr_to_reg_cache + .push((std::borrow::Cow::Owned(expr), reg)); + } + let result_start_reg = program.alloc_registers(result_columns.len()); for (i, result_column) in result_columns.iter().enumerate() { let reg = result_start_reg + i; - - translate_expr_for_returning(program, &result_column.expr, value_registers, reg)?; + translate_expr_no_constant_opt( + program, + Some(table_references), + &result_column.expr, + reg, + resolver, + NoConstantOptReason::RegisterReuse, + )?; } + // Bit of a hack: this is required in case of e.g. INSERT ... ON CONFLICT DO UPDATE ... RETURNING + // where the result column values may either be the ones that were inserted, or the ones that were updated, + // depending on the row in question. + // meaning: emit_returning_results() may be called twice during translation and the cached expression values + // must be distinct for each call. + resolver.expr_to_reg_cache.truncate(cache_len); + program.emit_insn(Insn::ResultRow { start_reg: result_start_reg, count: result_columns.len(), diff --git a/core/translate/group_by.rs b/core/translate/group_by.rs index aa5e5dcdc..4d0c12a18 100644 --- a/core/translate/group_by.rs +++ b/core/translate/group_by.rs @@ -646,7 +646,10 @@ pub fn group_by_process_single_group( { if *in_result { program.emit_column_or_rowid(*pseudo_cursor, sorter_column_index, next_reg); - t_ctx.resolver.expr_to_reg_cache.push((expr, next_reg)); + t_ctx + .resolver + .expr_to_reg_cache + .push((std::borrow::Cow::Borrowed(expr), next_reg)); next_reg += 1; } } @@ -669,7 +672,10 @@ pub fn group_by_process_single_group( dest_reg, &t_ctx.resolver, )?; - t_ctx.resolver.expr_to_reg_cache.push((expr, dest_reg)); + t_ctx + .resolver + .expr_to_reg_cache + .push((std::borrow::Cow::Borrowed(expr), dest_reg)); } } } @@ -792,10 +798,10 @@ pub fn group_by_emit_row_phase<'a>( register: agg_result_reg, func: agg.func.clone(), }); - t_ctx - .resolver - .expr_to_reg_cache - .push((&agg.original_expr, agg_result_reg)); + t_ctx.resolver.expr_to_reg_cache.push(( + std::borrow::Cow::Borrowed(&agg.original_expr), + agg_result_reg, + )); } t_ctx.resolver.enable_expr_to_reg_cache(); diff --git a/core/translate/insert.rs b/core/translate/insert.rs index 880e47399..a336f330c 100644 --- a/core/translate/insert.rs +++ b/core/translate/insert.rs @@ -13,13 +13,15 @@ use crate::translate::emitter::{ }; use crate::translate::expr::{ bind_and_rewrite_expr, emit_returning_results, process_returning_clause, walk_expr_mut, - BindingBehavior, ReturningValueRegisters, WalkControl, + BindingBehavior, WalkControl, }; use crate::translate::fkeys::{ build_index_affinity_string, emit_fk_violation, emit_guarded_fk_decrement, index_probe, open_read_index, open_read_table, }; -use crate::translate::plan::{ResultSetColumn, TableReferences}; +use crate::translate::plan::{ + ColumnUsedMask, JoinedTable, Operation, ResultSetColumn, TableReferences, +}; use crate::translate::planner::ROWID_STRS; use crate::translate::upsert::{ collect_set_clauses_for_upsert, emit_upsert, resolve_upsert_target, ResolvedUpsertTarget, @@ -94,7 +96,7 @@ pub struct InsertEmitCtx<'a> { /// Index cursors we need to populate for this table /// (idx name, root_page, idx cursor id) - pub idx_cursors: Vec<(&'a String, i64, usize)>, + pub idx_cursors: Vec<(String, i64, usize)>, /// Context for if the insert values are materialized first /// into a temporary table @@ -134,7 +136,7 @@ pub struct InsertEmitCtx<'a> { impl<'a> InsertEmitCtx<'a> { fn new( program: &mut ProgramBuilder, - resolver: &'a Resolver, + resolver: &Resolver, table: &'a Arc, on_conflict: Option, cdc_table: Option<(usize, Arc)>, @@ -146,7 +148,7 @@ impl<'a> InsertEmitCtx<'a> { let mut idx_cursors = Vec::new(); for idx in indices { idx_cursors.push(( - &idx.name, + idx.name.clone(), idx.root_page, program.alloc_cursor_index(None, idx)?, )); @@ -181,7 +183,7 @@ impl<'a> InsertEmitCtx<'a> { #[allow(clippy::too_many_arguments)] pub fn translate_insert( - resolver: &Resolver, + resolver: &mut Resolver, on_conflict: Option, tbl_name: QualifiedName, columns: Vec, @@ -240,14 +242,26 @@ pub fn translate_insert( let cdc_table = prepare_cdc_if_necessary(&mut program, resolver.schema, table.get_name())?; + let mut table_references = TableReferences::new( + vec![JoinedTable { + table: Table::BTree( + table + .btree() + .expect("we shouldn't have got here without a BTree table"), + ), + identifier: table_name.to_string(), + internal_id: program.table_reference_counter.next(), + op: Operation::default_scan_for(&table), + join_info: None, + col_used_mask: ColumnUsedMask::default(), + database_id: 0, + }], + vec![], + ); + // Process RETURNING clause using shared module - let (mut result_columns, _) = process_returning_clause( - &mut returning, - &table, - table_name.as_str(), - &mut program, - connection, - )?; + let mut result_columns = + process_returning_clause(&mut returning, &mut table_references, connection)?; let has_fks = fk_enabled && (resolver.schema.has_child_fks(table_name.as_str()) || resolver @@ -460,32 +474,37 @@ pub fn translate_insert( // Emit RETURNING results if specified if !result_columns.is_empty() { - let value_registers = ReturningValueRegisters { - rowid_register: insertion.key_register(), - columns_start_register: insertion.first_col_register(), - num_columns: table.columns().len(), - }; - - emit_returning_results(&mut program, &result_columns, &value_registers)?; + emit_returning_results( + &mut program, + &table_references, + &result_columns, + insertion.first_col_register(), + insertion.key_register(), + resolver, + )?; } program.emit_insn(Insn::Goto { target_pc: ctx.row_done_label, }); - - resolve_upserts( - &mut program, - resolver, - &mut upsert_actions, - &ctx, - &insertion, - &table, - &mut result_columns, - connection, - )?; + if !upsert_actions.is_empty() { + resolve_upserts( + &mut program, + resolver, + &mut upsert_actions, + &ctx, + &insertion, + &table, + &mut result_columns, + connection, + &table_references, + )?; + } emit_epilogue(&mut program, &ctx, inserting_multiple_rows); program.set_needs_stmt_subtransactions(true); + program.result_columns = result_columns; + program.table_references.extend(table_references); Ok(program) } @@ -544,7 +563,7 @@ fn emit_commit_phase( let idx_cursor_id = ctx .idx_cursors .iter() - .find(|(name, _, _)| *name == &index.name) + .find(|(name, _, _)| name == &index.name) .map(|(_, _, c_id)| *c_id) .expect("no cursor found for index"); @@ -739,13 +758,14 @@ fn emit_rowid_generation( #[allow(clippy::too_many_arguments)] fn resolve_upserts( program: &mut ProgramBuilder, - resolver: &Resolver, + resolver: &mut Resolver, upsert_actions: &mut [(ResolvedUpsertTarget, BranchOffset, Box)], ctx: &InsertEmitCtx, insertion: &Insertion, table: &Table, result_columns: &mut [ResultSetColumn], connection: &Arc, + table_references: &TableReferences, ) -> Result<()> { for (_, label, upsert) in upsert_actions { program.preassign_label_to_next_insn(*label); @@ -768,6 +788,7 @@ fn resolve_upserts( resolver, result_columns, connection, + table_references, )?; } else { // UpsertDo::Nothing case @@ -1708,7 +1729,7 @@ fn emit_preflight_constraint_checks( let idx_cursor_id = ctx .idx_cursors .iter() - .find(|(name, _, _)| *name == &index.name) + .find(|(name, _, _)| name == &index.name) .map(|(_, _, c_id)| *c_id) .expect("no cursor found for index"); diff --git a/core/translate/mod.rs b/core/translate/mod.rs index 5d9b902ae..76e0cdddb 100644 --- a/core/translate/mod.rs +++ b/core/translate/mod.rs @@ -93,14 +93,14 @@ pub fn translate( ); program.prologue(); - let resolver = Resolver::new(schema, syms); + let mut resolver = Resolver::new(schema, syms); program = match stmt { // There can be no nesting with pragma, so lift it up here ast::Stmt::Pragma { name, body } => { pragma::translate_pragma(&resolver, &name, body, pager, connection.clone(), program)? } - stmt => translate_inner(stmt, &resolver, program, &connection, input)?, + stmt => translate_inner(stmt, &mut resolver, program, &connection, input)?, }; program.epilogue(schema); @@ -113,7 +113,7 @@ pub fn translate( /// Translate SQL statement into bytecode program. pub fn translate_inner( stmt: ast::Stmt, - resolver: &Resolver, + resolver: &mut Resolver, program: ProgramBuilder, connection: &Arc, input: &str, diff --git a/core/translate/update.rs b/core/translate/update.rs index 635e41051..281cb32f3 100644 --- a/core/translate/update.rs +++ b/core/translate/update.rs @@ -266,13 +266,8 @@ pub fn prepare_update_plan( } } - let (result_columns, _table_references) = process_returning_clause( - &mut body.returning, - &table, - body.tbl_name.name.as_str(), - program, - connection, - )?; + let result_columns = + process_returning_clause(&mut body.returning, &mut table_references, connection)?; let order_by = body .order_by diff --git a/core/translate/upsert.rs b/core/translate/upsert.rs index e37a03f0d..69ced5b81 100644 --- a/core/translate/upsert.rs +++ b/core/translate/upsert.rs @@ -10,6 +10,7 @@ use crate::translate::emitter::UpdateRowSource; use crate::translate::expr::{walk_expr, WalkControl}; use crate::translate::fkeys::{emit_fk_child_update_counters, emit_parent_key_change_checks}; use crate::translate::insert::{format_unique_violation_desc, InsertEmitCtx}; +use crate::translate::plan::TableReferences; use crate::translate::planner::ROWID_STRS; use crate::vdbe::insn::CmpInsFlags; use crate::Connection; @@ -23,7 +24,7 @@ use crate::{ }, expr::{ emit_returning_results, translate_expr, translate_expr_no_constant_opt, walk_expr_mut, - NoConstantOptReason, ReturningValueRegisters, + NoConstantOptReason, }, insert::Insertion, plan::ResultSetColumn, @@ -332,6 +333,7 @@ pub fn resolve_upsert_target( /// Semantics reference: https://sqlite.org/lang_upsert.html /// Column references in the DO UPDATE expressions refer to the original /// (unchanged) row. To refer to would-be inserted values, use `excluded.x`. +#[allow(clippy::too_many_arguments)] pub fn emit_upsert( program: &mut ProgramBuilder, table: &Table, @@ -339,9 +341,10 @@ pub fn emit_upsert( insertion: &Insertion, set_pairs: &mut [(usize, Box)], where_clause: &mut Option>, - resolver: &Resolver, + resolver: &mut Resolver, returning: &mut [ResultSetColumn], connection: &Arc, + table_references: &TableReferences, ) -> crate::Result<()> { // Seek & snapshot CURRENT program.emit_insn(Insn::SeekRowid { @@ -823,12 +826,14 @@ pub fn emit_upsert( // RETURNING from NEW image + final rowid if !returning.is_empty() { - let regs = ReturningValueRegisters { - rowid_register: new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg), - columns_start_register: new_start, - num_columns: num_cols, - }; - emit_returning_results(program, returning, ®s)?; + emit_returning_results( + program, + table_references, + returning, + new_start, + new_rowid_reg.unwrap_or(ctx.conflict_rowid_reg), + resolver, + )?; } program.emit_insn(Insn::Goto { diff --git a/core/translate/window.rs b/core/translate/window.rs index ad62e31fc..8a2bae2b3 100644 --- a/core/translate/window.rs +++ b/core/translate/window.rs @@ -526,10 +526,10 @@ pub fn init_window<'a>( let reg_acc_start = program.alloc_registers(window_function_count); let reg_acc_result_start = program.alloc_registers(window_function_count); for (i, func) in window.functions.iter().enumerate() { - t_ctx - .resolver - .expr_to_reg_cache - .push((&func.original_expr, reg_acc_result_start + i)); + t_ctx.resolver.expr_to_reg_cache.push(( + std::borrow::Cow::Borrowed(&func.original_expr), + reg_acc_result_start + i, + )); } // The same approach applies to expressions referencing the subquery (columns). @@ -543,7 +543,7 @@ pub fn init_window<'a>( t_ctx .resolver .expr_to_reg_cache - .push((expr, reg_col_start + i)); + .push((std::borrow::Cow::Borrowed(expr), reg_col_start + i)); } t_ctx.meta_window = Some(WindowMetadata { diff --git a/testing/all.test b/testing/all.test index 602174abf..52098ce45 100755 --- a/testing/all.test +++ b/testing/all.test @@ -48,3 +48,4 @@ source $testdir/upsert.test source $testdir/window.test source $testdir/partial_idx.test source $testdir/foreign_keys.test +source $testdir/returning.test diff --git a/testing/returning.test b/testing/returning.test new file mode 100755 index 000000000..23a9382c3 --- /dev/null +++ b/testing/returning.test @@ -0,0 +1,1012 @@ +#!/usr/bin/env tclsh +set testdir [file dirname $argv0] +source $testdir/tester.tcl +source $testdir/sqlite3/tester.tcl + +# ============================================================================ +# INSERT RETURNING tests +# ============================================================================ + +# Basic column references +do_execsql_test_on_specific_db {:memory:} insert-returning-single-column { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING id; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING id, name; +} {1|test} + +do_execsql_test_on_specific_db {:memory:} insert-returning-all-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING *; +} {1|test|10.5} + +# Table-qualified column references +do_execsql_test_on_specific_db {:memory:} insert-returning-table-qualified { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t.id, t.name; +} {1|test} + +# Arbitrary expressions not referencing columns +do_execsql_test_on_specific_db {:memory:} insert-returning-literal { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 42; +} {42} + +do_execsql_test_on_specific_db {:memory:} insert-returning-constant-expression { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 2 + 3 * 4; +} {14} + +do_execsql_test_on_specific_db {:memory:} insert-returning-string-literal { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 'hello world'; +} {"hello world"} + +# Expressions referencing result columns +do_execsql_test_on_specific_db {:memory:} insert-returning-column-arithmetic { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10) RETURNING 2 * value; +} {20} + +do_execsql_test_on_specific_db {:memory:} insert-returning-complex-expression { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 3) RETURNING x + y * 2; +} {11} + +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-column-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4) RETURNING a * b + c; +} {10} + +# Function calls +do_execsql_test_on_specific_db {:memory:} insert-returning-function-call { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello') RETURNING upper(name); +} {HELLO} + +do_execsql_test_on_specific_db {:memory:} insert-returning-function-multiple-columns { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'john', 'doe') RETURNING upper(first) || ' ' || upper(last); +} {"JOHN DOE"} + +do_execsql_test_on_specific_db {:memory:} insert-returning-function-with-expression { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 3) RETURNING abs(x - y); +} {2} + +# Mixed expressions +do_execsql_test_on_specific_db {:memory:} insert-returning-mixed-expressions { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 10) RETURNING id, upper(name), value * 3, 42; +} {1|TEST|30|42} + +# Multiple rows +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-rows { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'first'), (2, 'second'), (3, 'third') RETURNING id, name; +} {1|first +2|second +3|third} + +do_execsql_test_on_specific_db {:memory:} insert-returning-multiple-rows-expressions { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10), (2, 20), (3, 30) RETURNING id, value * 2; +} {1|20 +2|40 +3|60} + +# NULL handling +do_execsql_test_on_specific_db {:memory:} insert-returning-null-values { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, NULL, NULL) RETURNING id, name, value; +} {1||} + +do_execsql_test_on_specific_db {:memory:} insert-returning-null-expression { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, NULL) RETURNING coalesce(name, 'default'); +} {default} + +# Rowid +do_execsql_test_on_specific_db {:memory:} insert-returning-rowid { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test') RETURNING rowid, name; +} {1|test} + +do_execsql_test_on_specific_db {:memory:} insert-returning-rowid-expression { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test') RETURNING rowid * 2; +} {2} + +# Auto-increment +do_execsql_test_on_specific_db {:memory:} insert-returning-autoincrement { + CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + INSERT INTO t (name) VALUES ('test') RETURNING id, name; +} {1|test} + +# Complex nested expressions +do_execsql_test_on_specific_db {:memory:} insert-returning-nested-expressions { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER, z INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4) RETURNING (x + y) * (z - 1); +} {15} + +do_execsql_test_on_specific_db {:memory:} insert-returning-case-expression { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING CASE WHEN value > 10 THEN 'high' WHEN value > 0 THEN 'low' ELSE 'zero' END; +} {low} + +# String operations +do_execsql_test_on_specific_db {:memory:} insert-returning-string-concat { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'John', 'Doe') RETURNING first || ' ' || last; +} {"John Doe"} + +do_execsql_test_on_specific_db {:memory:} insert-returning-substring { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING substr(name, 1, 5); +} {hello} + +# ============================================================================ +# INSERT ON CONFLICT RETURNING tests +# ============================================================================ + +# RETURNING on INSERT path (no conflict) +do_execsql_test_on_specific_db {:memory:} upsert-returning-insert-path { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'new') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, name; +} {1|new} + +# RETURNING on UPDATE path (conflict occurs) +do_execsql_test_on_specific_db {:memory:} upsert-returning-update-path { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + INSERT INTO t VALUES (1, 'new') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, name; +} {1|new} + +# RETURNING on DO NOTHING (should return empty) +do_execsql_test_on_specific_db {:memory:} upsert-returning-do-nothing { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + INSERT INTO t VALUES (1, 'ignored') + ON CONFLICT DO NOTHING + RETURNING id, name; +} {} + +# RETURNING with table-qualified columns +do_execsql_test_on_specific_db {:memory:} upsert-returning-table-qualified { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + INSERT INTO t VALUES (1, 'new') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING t.id, t.name; +} {1|new} + +# RETURNING with expressions on INSERT path +do_execsql_test_on_specific_db {:memory:} upsert-returning-expression-insert { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 10) + ON CONFLICT DO UPDATE SET value = excluded.value + RETURNING id, value * 2; +} {1|20} + +# RETURNING with expressions on UPDATE path +do_execsql_test_on_specific_db {:memory:} upsert-returning-expression-update { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 5); + INSERT INTO t VALUES (1, 10) + ON CONFLICT DO UPDATE SET value = excluded.value + RETURNING id, value * 3; +} {1|30} + +# RETURNING with complex expressions +do_execsql_test_on_specific_db {:memory:} upsert-returning-complex-expression { + CREATE TABLE t (id INTEGER PRIMARY KEY, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 2, 3); + INSERT INTO t VALUES (1, 5, 7) + ON CONFLICT DO UPDATE SET x = excluded.x, y = excluded.y + RETURNING id, x + y * 2; +} {1|19} + +# RETURNING with function calls +do_execsql_test_on_specific_db {:memory:} upsert-returning-function { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'hello'); + INSERT INTO t VALUES (1, 'world') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, upper(name); +} {1|WORLD} + +# RETURNING with multiple rows (mixed insert/update) +do_execsql_test_on_specific_db {:memory:} upsert-returning-multiple-rows { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old1'), (2, 'old2'); + INSERT INTO t VALUES (1, 'new1'), (2, 'new2'), (3, 'new3') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING id, name; +} {1|new1 +2|new2 +3|new3} + +# RETURNING with WHERE clause in DO UPDATE +do_execsql_test_on_specific_db {:memory:} upsert-returning-where-update { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 5); + INSERT INTO t VALUES (1, 10) + ON CONFLICT DO UPDATE SET value = excluded.value WHERE excluded.value > t.value + RETURNING id, value; +} {1|10} + +do_execsql_test_on_specific_db {:memory:} upsert-returning-where-no-update { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 10); + INSERT INTO t VALUES (1, 5) + ON CONFLICT DO UPDATE SET value = excluded.value WHERE excluded.value > t.value + RETURNING id, value; +} {} + +# RETURNING with arbitrary expressions not referencing columns +do_execsql_test_on_specific_db {:memory:} upsert-returning-literal { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'test') + ON CONFLICT DO UPDATE SET name = excluded.name + RETURNING 42; +} {42} + +# RETURNING with mixed insert/update in single statement (first one doesn't conflict, second one does) +do_execsql_test_on_specific_db {:memory:} upsert-returning-mixed-operations { + CREATE TABLE t (id INTEGER PRIMARY KEY, value INTEGER); + INSERT INTO t VALUES (1, 5); + INSERT INTO t VALUES (1, 10), (2, 20) + ON CONFLICT DO UPDATE SET value = excluded.value + t.value + RETURNING id, value; +} {1|15 +2|20} + +# ============================================================================ +# INSERT ... SELECT ... RETURNING tests +# ============================================================================ + +# Basic INSERT ... SELECT with RETURNING +do_execsql_test_on_specific_db {:memory:} insert-select-returning-basic { + CREATE TABLE u (a INTEGER, b INTEGER PRIMARY KEY, c TEXT); + CREATE TABLE t (a TEXT, b INTEGER, c INTEGER PRIMARY KEY); + INSERT INTO u VALUES (1, 2, 'test'), (2, 4, 'data'), (3, 6, 'more'); + INSERT INTO t SELECT c, b, b * 2 FROM u RETURNING *; +} {test|2|4 +data|4|8 +more|6|12} + +# INSERT ... SELECT with WHERE clause and RETURNING +do_execsql_test_on_specific_db {:memory:} insert-select-returning-where { + CREATE TABLE u (a INTEGER, b INTEGER PRIMARY KEY, c TEXT); + CREATE TABLE t (a TEXT, b INTEGER, c INTEGER PRIMARY KEY); + INSERT INTO u SELECT value, value * 2, 'kekkers' FROM generate_series(1, 10); + INSERT INTO t SELECT concat(c, '-lollers') as lul, b, b * 2 FROM u WHERE a > 5 RETURNING *; +} {kekkers-lollers|12|24 +kekkers-lollers|14|28 +kekkers-lollers|16|32 +kekkers-lollers|18|36 +kekkers-lollers|20|40} + +# INSERT ... SELECT with RETURNING specific columns +do_execsql_test_on_specific_db {:memory:} insert-select-returning-columns { + CREATE TABLE u (a INTEGER, b INTEGER, c TEXT); + CREATE TABLE t (a TEXT, b INTEGER, c INTEGER); + INSERT INTO u VALUES (1, 10, 'x'), (2, 20, 'y'), (3, 30, 'z'); + INSERT INTO t SELECT c, b, a FROM u RETURNING a, c; +} {x|1 +y|2 +z|3} + +# INSERT ... SELECT with RETURNING expressions +do_execsql_test_on_specific_db {:memory:} insert-select-returning-expressions { + CREATE TABLE u (a INTEGER, b INTEGER); + CREATE TABLE t (x INTEGER, y INTEGER); + INSERT INTO u VALUES (5, 10), (15, 20); + INSERT INTO t SELECT a, b FROM u RETURNING x + y, x * y; +} {15|50 +35|300} + +# INSERT ... SELECT with RETURNING and function calls +do_execsql_test_on_specific_db {:memory:} insert-select-returning-functions { + CREATE TABLE u (name TEXT, value INTEGER); + CREATE TABLE t (name TEXT, value INTEGER); + INSERT INTO u VALUES ('hello', 100), ('world', 200); + INSERT INTO t SELECT name, value FROM u RETURNING upper(name), value / 10; +} {HELLO|10 +WORLD|20} + +# INSERT ... SELECT with RETURNING and aggregates in source +do_execsql_test_on_specific_db {:memory:} insert-select-returning-aggregate-source { + CREATE TABLE u (category TEXT, amount INTEGER); + CREATE TABLE t (category TEXT, total INTEGER); + INSERT INTO u VALUES ('A', 10), ('A', 20), ('B', 30), ('B', 40); + INSERT INTO t SELECT category, SUM(amount) FROM u GROUP BY category RETURNING *; +} {A|30 +B|70} + +# INSERT ... SELECT with RETURNING literal values +do_execsql_test_on_specific_db {:memory:} insert-select-returning-literals { + CREATE TABLE u (id INTEGER); + CREATE TABLE t (id INTEGER, constant INTEGER); + INSERT INTO u VALUES (1), (2), (3); + INSERT INTO t SELECT id, 42 FROM u RETURNING constant; +} {42 +42 +42} + +# INSERT ... SELECT with RETURNING and ORDER BY in source +do_execsql_test_on_specific_db {:memory:} insert-select-returning-ordered { + CREATE TABLE u (value INTEGER); + CREATE TABLE t (value INTEGER); + INSERT INTO u VALUES (30), (10), (20); + INSERT INTO t SELECT value FROM u ORDER BY value RETURNING value; +} {10 +20 +30} + +# ============================================================================ +# UPDATE RETURNING tests +# ============================================================================ + +# Basic column references +do_execsql_test_on_specific_db {:memory:} update-returning-single-column { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5); + UPDATE t SET value = 20.5 WHERE id = 1 RETURNING id; +} {1} + +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5); + UPDATE t SET value = 20.5 WHERE id = 1 RETURNING id, name, value; +} {1|test|20.5} + +do_execsql_test_on_specific_db {:memory:} update-returning-all-columns { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5); + UPDATE t SET value = 20.5 WHERE id = 1 RETURNING *; +} {1|test|20.5} + +# Table-qualified column references +do_execsql_test_on_specific_db {:memory:} update-returning-table-qualified { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'old'); + UPDATE t SET name = 'new' WHERE id = 1 RETURNING t.id, t.name; +} {1|new} + +# Arbitrary expressions not referencing columns +do_execsql_test_on_specific_db {:memory:} update-returning-literal { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10); + UPDATE t SET value = 20 WHERE id = 1 RETURNING 42; +} {42} + +do_execsql_test_on_specific_db {:memory:} update-returning-constant-expression { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1); + UPDATE t SET id = 2 RETURNING 2 + 3 * 4; +} {14} + +# Expressions referencing updated columns +do_execsql_test_on_specific_db {:memory:} update-returning-column-arithmetic { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10); + UPDATE t SET value = 20 WHERE id = 1 RETURNING 2 * value; +} {40} + +do_execsql_test_on_specific_db {:memory:} update-returning-complex-expression { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 3); + UPDATE t SET x = 8 WHERE id = 1 RETURNING x + y * 2; +} {14} + +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-column-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4); + UPDATE t SET a = 5, b = 6 WHERE id = 1 RETURNING a * b + c; +} {34} + +# Function calls +do_execsql_test_on_specific_db {:memory:} update-returning-function-call { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello'); + UPDATE t SET name = 'world' WHERE id = 1 RETURNING upper(name); +} {WORLD} + +do_execsql_test_on_specific_db {:memory:} update-returning-function-multiple-columns { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'john', 'doe'); + UPDATE t SET first = 'jane', last = 'smith' WHERE id = 1 RETURNING upper(first) || ' ' || upper(last); +} {"JANE SMITH"} + +# Mixed expressions +do_execsql_test_on_specific_db {:memory:} update-returning-mixed-expressions { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 10); + UPDATE t SET name = 'updated', value = 30 WHERE id = 1 RETURNING id, upper(name), value * 2, 42; +} {1|UPDATED|60|42} + +# Multiple rows +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-rows { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'first'), (2, 'second'), (3, 'third'); + UPDATE t SET name = 'updated' RETURNING id, name; +} {1|updated +2|updated +3|updated} + +do_execsql_test_on_specific_db {:memory:} update-returning-multiple-rows-expressions { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10), (2, 20), (3, 30); + UPDATE t SET value = value * 2 RETURNING id, value; +} {1|20 +2|40 +3|60} + +# WHERE clause filtering +do_execsql_test_on_specific_db {:memory:} update-returning-with-where { + CREATE TABLE t (id INTEGER, name TEXT, active INTEGER); + INSERT INTO t VALUES (1, 'first', 1), (2, 'second', 0), (3, 'third', 1); + UPDATE t SET name = 'updated' WHERE active = 1 RETURNING id, name; +} {1|updated +3|updated} + +# Old vs new values +do_execsql_test_on_specific_db {:memory:} update-returning-old-vs-new-values { + CREATE TABLE t (id INTEGER, counter INTEGER); + INSERT INTO t VALUES (1, 5); + UPDATE t SET counter = counter + 10 WHERE id = 1 RETURNING id, counter; +} {1|15} + +# NULL handling +do_execsql_test_on_specific_db {:memory:} update-returning-null-values { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 10); + UPDATE t SET name = NULL, value = NULL WHERE id = 1 RETURNING id, name, value; +} {1||} + +do_execsql_test_on_specific_db {:memory:} update-returning-null-expression { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET name = NULL WHERE id = 1 RETURNING coalesce(name, 'default'); +} {default} + +# Rowid +do_execsql_test_on_specific_db {:memory:} update-returning-rowid { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test'); + UPDATE t SET name = 'updated' RETURNING rowid, name; +} {1|updated} + +do_execsql_test_on_specific_db {:memory:} update-returning-rowid-expression { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test'); + UPDATE t SET name = 'updated' RETURNING rowid * 2; +} {2} + +# Complex nested expressions +do_execsql_test_on_specific_db {:memory:} update-returning-nested-expressions { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER, z INTEGER); + INSERT INTO t VALUES (1, 2, 3, 4); + UPDATE t SET x = 5, y = 6 WHERE id = 1 RETURNING (x + y) * (z - 1); +} {33} + +do_execsql_test_on_specific_db {:memory:} update-returning-case-expression { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5); + UPDATE t SET value = 15 WHERE id = 1 RETURNING CASE WHEN value > 10 THEN 'high' WHEN value > 0 THEN 'low' ELSE 'zero' END; +} {high} + +# String operations +do_execsql_test_on_specific_db {:memory:} update-returning-string-concat { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, 'John', 'Doe'); + UPDATE t SET first = 'Jane', last = 'Smith' WHERE id = 1 RETURNING first || ' ' || last; +} {"Jane Smith"} + +do_execsql_test_on_specific_db {:memory:} update-returning-substring { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world'); + UPDATE t SET name = 'goodbye world' WHERE id = 1 RETURNING substr(name, 1, 7); +} {goodbye} + +# Row value updates +do_execsql_test_on_specific_db {:memory:} update-returning-row-values { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET (id, name) = (2, 'mordor') RETURNING id, name; +} {2|mordor} + +# ============================================================================ +# Edge cases and complex scenarios +# ============================================================================ + +# RETURNING expressions that don't reference any columns +do_execsql_test_on_specific_db {:memory:} insert-returning-no-column-reference { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING 1 + 1, 'constant', NULL; +} {2|constant|} + +do_execsql_test_on_specific_db {:memory:} update-returning-no-column-reference { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1); + UPDATE t SET id = 2 RETURNING 1 + 1, 'constant', NULL; +} {2|constant|} + +# RETURNING with aggregate-like expressions (should work per-row) +do_execsql_test_on_specific_db {:memory:} insert-returning-sum-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 2, 3) RETURNING a + b; +} {5} + +do_execsql_test_on_specific_db {:memory:} update-returning-sum-expression { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 2, 3); + UPDATE t SET a = 5, b = 7 WHERE id = 1 RETURNING a + b; +} {12} + +# ============================================================================ +# NASTY EDGE CASES - Things that should work but might break +# ============================================================================ + +# Column name same as table name - ambiguity resolution +do_execsql_test_on_specific_db {:memory:} insert-returning-column-same-as-table-name { + CREATE TABLE t (t INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t.t, t.name; +} {1|test} + +do_execsql_test_on_specific_db {:memory:} insert-returning-column-same-as-table-name-unqualified { + CREATE TABLE t (t INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t, name; +} {1|test} + +# RETURNING with rowid when INTEGER PRIMARY KEY exists - can reference both +do_execsql_test_on_specific_db {:memory:} insert-returning-rowid-and-pk-alias { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING rowid, id, name; +} {1|1|test} + +do_execsql_test_on_specific_db {:memory:} update-returning-rowid-and-pk-alias { + CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT); + INSERT INTO t VALUES (1, 'old'); + UPDATE t SET name = 'new' WHERE id = 1 RETURNING rowid, id, name; +} {1|1|new} + +# RETURNING with expressions referencing updated columns in UPDATE +do_execsql_test_on_specific_db {:memory:} update-returning-reference-updated-column { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 10); + UPDATE t SET x = 20 WHERE id = 1 RETURNING x, x * 2, x + y; +} {20|40|30} + +do_execsql_test_on_specific_db {:memory:} update-returning-reference-multiple-updated-columns { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, 1, 2, 3); + UPDATE t SET a = 10, b = 20 WHERE id = 1 RETURNING a, b, a + b, a * b + c; +} {10|20|30|203} + +# RETURNING with NULLIF edge cases +do_execsql_test_on_specific_db {:memory:} insert-returning-nullif { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING NULLIF(value, 5); +} {} + +do_execsql_test_on_specific_db {:memory:} insert-returning-nullif-no-match { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING NULLIF(value, 10); +} {5} + +do_execsql_test_on_specific_db {:memory:} update-returning-nullif { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5); + UPDATE t SET value = 10 WHERE id = 1 RETURNING NULLIF(value, 5); +} {10} + +# RETURNING with COALESCE multiple arguments +do_execsql_test_on_specific_db {:memory:} insert-returning-coalesce-multiple { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER, c INTEGER); + INSERT INTO t VALUES (1, NULL, NULL, 42) RETURNING COALESCE(a, b, c); +} {42} + +do_execsql_test_on_specific_db {:memory:} insert-returning-coalesce-all-null { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, NULL, NULL) RETURNING COALESCE(a, b, 'default'); +} {default} + +# RETURNING with arithmetic on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-null-arithmetic { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, NULL, 5) RETURNING x + y, x * y, x - y; +} {||} + +do_execsql_test_on_specific_db {:memory:} update-returning-null-arithmetic { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 10, 5); + UPDATE t SET x = NULL WHERE id = 1 RETURNING x + y, x * y; +} {|} + +# RETURNING with string functions on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-string-func-null { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, NULL) RETURNING upper(name), length(name), substr(name, 1, 5); +} {||} + +do_execsql_test_on_specific_db {:memory:} update-returning-string-func-null { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET name = NULL WHERE id = 1 RETURNING upper(name), length(name); +} {|} + +# RETURNING with CASE expressions having NULL branches +do_execsql_test_on_specific_db {:memory:} insert-returning-case-with-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING CASE WHEN value > 10 THEN 'high' WHEN value > 0 THEN NULL ELSE 'zero' END; +} {} + +do_execsql_test_on_specific_db {:memory:} insert-returning-case-nested { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 5, 10) RETURNING CASE WHEN a > b THEN a WHEN a < b THEN b ELSE NULL END; +} {10} + +# RETURNING with type conversions +do_execsql_test_on_specific_db {:memory:} insert-returning-type-conversion { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42) RETURNING CAST(value AS TEXT), CAST(value AS REAL); +} {42|42.0} + +do_execsql_test_on_specific_db {:memory:} update-returning-type-conversion { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42); + UPDATE t SET value = 100 WHERE id = 1 RETURNING CAST(value AS TEXT); +} {100} + +# RETURNING when updating rowid directly (if supported) +do_execsql_test_on_specific_db {:memory:} update-rowid-returning { + CREATE TABLE t (name TEXT); + INSERT INTO t VALUES ('test'); + UPDATE t SET rowid = 999 WHERE rowid = 1 RETURNING rowid, name; +} {999|test} + +# RETURNING with expressions referencing columns not in result set +do_execsql_test_on_specific_db {:memory:} insert-returning-reference-hidden-column { + CREATE TABLE t (id INTEGER, secret INTEGER, public INTEGER); + INSERT INTO t VALUES (1, 100, 50) RETURNING public, secret * 2; +} {50|200} + +do_execsql_test_on_specific_db {:memory:} update-returning-reference-hidden-column { + CREATE TABLE t (id INTEGER, secret INTEGER, public INTEGER); + INSERT INTO t VALUES (1, 100, 50); + UPDATE t SET public = 75 WHERE id = 1 RETURNING public, secret + public; +} {75|175} + +# RETURNING with duplicate column names in result (should work) +do_execsql_test_on_specific_db {:memory:} insert-returning-duplicate-column-names { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING id, id, name, name; +} {1|1|test|test} + +do_execsql_test_on_specific_db {:memory:} update-returning-duplicate-column-names { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10); + UPDATE t SET value = 20 WHERE id = 1 RETURNING value, value * 2, value; +} {20|40|20} + +# RETURNING with expressions using column names as strings (should not work, but test anyway) +do_execsql_test_on_specific_db {:memory:} insert-returning-column-name-as-string { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING 'id', 'name'; +} {id|name} + +# RETURNING with excluded in regular INSERT (should error) +do_execsql_test_in_memory_any_error insert-returning-excluded-not-upsert { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING excluded.name; +} + +# RETURNING with expressions referencing columns that were updated to NULL +do_execsql_test_on_specific_db {:memory:} update-returning-updated-to-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42); + UPDATE t SET value = NULL WHERE id = 1 RETURNING value, COALESCE(value, 0); +} {|0} + +# RETURNING with expressions referencing columns that were updated from NULL +do_execsql_test_on_specific_db {:memory:} update-returning-updated-from-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, NULL); + UPDATE t SET value = 42 WHERE id = 1 RETURNING value, COALESCE(value, 0); +} {42|42} + +# RETURNING with expressions that use backticks +do_execsql_test_on_specific_db {:memory:} insert-returning-backtick-columns { + CREATE TABLE t (`id` INTEGER, `name` TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING `id`, `name`; +} {1|test} + +# RETURNING with expressions that use double quotes (if DQS enabled) +do_execsql_test_skip_lines_on_specific_db 1 {:memory:} insert-returning-double-quote-columns { + .dbconfig dqs_dml on + CREATE TABLE t ("id" INTEGER, "name" TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING "id", "name"; +} {1|test} + +# RETURNING with expressions referencing columns in different cases (case sensitivity) +do_execsql_test_on_specific_db {:memory:} insert-returning-case-insensitive { + CREATE TABLE t (ID INTEGER, Name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING id, name, ID, Name; +} {1|test|1|test} + +# RETURNING with expressions that reference columns that don't exist (should error) +do_execsql_test_in_memory_any_error insert-returning-nonexistent-column { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING nonexistent; +} + +do_execsql_test_in_memory_any_error update-returning-nonexistent-column { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1); + UPDATE t SET id = 2 RETURNING nonexistent; +} + +# RETURNING with expressions that reference wrong table (should error) +do_execsql_test_in_memory_any_error insert-returning-wrong-table { + CREATE TABLE t1 (id INTEGER); + CREATE TABLE t2 (id INTEGER); + INSERT INTO t1 VALUES (1) RETURNING t2.id; +} + +# RETURNING with expressions that mix table-qualified and unqualified +do_execsql_test_on_specific_db {:memory:} insert-returning-mixed-qualification { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test') RETURNING t.id, name, t.name, id; +} {1|test|test|1} + +# RETURNING with expressions that reference columns updated in SET clause +do_execsql_test_on_specific_db {:memory:} update-returning-set-clause-reference { + CREATE TABLE t (id INTEGER, x INTEGER, y INTEGER); + INSERT INTO t VALUES (1, 5, 10); + UPDATE t SET x = 20, y = x + 10 WHERE id = 1 RETURNING x, y; +} {20|15} + +# RETURNING with expressions that reference columns in order of update +do_execsql_test_on_specific_db {:memory:} update-returning-order-dependent { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 1, 2); + UPDATE t SET a = b, b = a WHERE id = 1 RETURNING a, b; +} {2|1} + +# RETURNING with empty result set (no rows affected) +do_execsql_test_on_specific_db {:memory:} update-returning-no-rows { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'test'); + UPDATE t SET name = 'updated' WHERE id = 999 RETURNING id, name; +} {} + +# RETURNING with expressions using || operator on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-concat-null { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, NULL, 'Doe') RETURNING first || ' ' || last; +} {} + +do_execsql_test_on_specific_db {:memory:} insert-returning-concat-both-null { + CREATE TABLE t (id INTEGER, first TEXT, last TEXT); + INSERT INTO t VALUES (1, NULL, NULL) RETURNING first || ' ' || last; +} {} + +# RETURNING with expressions using IN operator +do_execsql_test_on_specific_db {:memory:} insert-returning-in-operator { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING value IN (1, 2, 3, 4, 5); +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-in-operator-false { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 10) RETURNING value IN (1, 2, 3, 4, 5); +} {0} + +# RETURNING with expressions using LIKE operator +do_execsql_test_on_specific_db {:memory:} insert-returning-like-operator { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING name LIKE 'hello%'; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-like-operator-false { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING name LIKE 'goodbye%'; +} {0} + +# RETURNING with expressions using IS NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-is-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, NULL) RETURNING value IS NULL; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-is-not-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42) RETURNING value IS NOT NULL; +} {1} + +# RETURNING with expressions using BETWEEN +do_execsql_test_on_specific_db {:memory:} insert-returning-between { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING value BETWEEN 1 AND 10; +} {1} + +do_execsql_test_on_specific_db {:memory:} insert-returning-between-false { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 15) RETURNING value BETWEEN 1 AND 10; +} {0} + +# RETURNING with expressions using GLOB (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-glob { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello.txt') RETURNING name GLOB '*.txt'; +} {1} + +# RETURNING with expressions that overflow (large numbers) +do_execsql_test_on_specific_db {:memory:} insert-returning-large-number { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 9223372036854775807) RETURNING value; +} {9223372036854775807} + +# RETURNING with expressions using ABS on negative +do_execsql_test_on_specific_db {:memory:} insert-returning-abs-negative { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, -42) RETURNING ABS(value); +} {42} + +# RETURNING with expressions using MAX/MIN (should work per-row, not aggregate) +do_execsql_test_on_specific_db {:memory:} insert-returning-max-min-per-row { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 5, 10) RETURNING MAX(a, b), MIN(a, b); +} {10|5} + +# RETURNING with expressions using ROUND +do_execsql_test_on_specific_db {:memory:} insert-returning-round { + CREATE TABLE t (id INTEGER, value REAL); + INSERT INTO t VALUES (1, 3.14159) RETURNING ROUND(value, 2); +} {3.14} + +# RETURNING with expressions using LENGTH on empty string +do_execsql_test_on_specific_db {:memory:} insert-returning-length-empty { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, '') RETURNING LENGTH(name); +} {0} + +# RETURNING with expressions using TRIM +do_execsql_test_on_specific_db {:memory:} insert-returning-trim { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, ' hello ') RETURNING TRIM(name); +} {hello} + +# RETURNING with expressions using REPLACE +do_execsql_test_on_specific_db {:memory:} insert-returning-replace { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello world') RETURNING REPLACE(name, 'world', 'universe'); +} {"hello universe"} + +# RETURNING with expressions using LOWER/UPPER on mixed case +do_execsql_test_on_specific_db {:memory:} insert-returning-lower-upper { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'HeLLo WoRLd') RETURNING LOWER(name), UPPER(name); +} {"hello world|HELLO WORLD"} + +# RETURNING with expressions using SUBSTR with negative start +do_execsql_test_on_specific_db {:memory:} insert-returning-substr-negative { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'hello') RETURNING SUBSTR(name, -3); +} {llo} + +# RETURNING with expressions using CHAR (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-char { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 65) RETURNING CHAR(value); +} {A} + +# RETURNING with expressions using UNICODE (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-unicode { + CREATE TABLE t (id INTEGER, name TEXT); + INSERT INTO t VALUES (1, 'A') RETURNING UNICODE(name); +} {65} + +# RETURNING with expressions using DATE functions (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-date { + CREATE TABLE t (id INTEGER, date_text TEXT); + INSERT INTO t VALUES (1, '2023-01-01') RETURNING DATE(date_text); +} {2023-01-01} + +# RETURNING with expressions using JULIANDAY (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-julianday { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING JULIANDAY('2023-01-01'); +} {2459945.5} + +# RETURNING with expressions using STRFTIME (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-strftime { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING STRFTIME('%Y-%m-%d', '2023-01-01'); +} {2023-01-01} + +# RETURNING with expressions using JSON functions (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-json { + CREATE TABLE t (id INTEGER, data TEXT); + INSERT INTO t VALUES (1, '{"key": "value"}') RETURNING JSON_EXTRACT(data, '$.key'); +} {"value"} + +# RETURNING with expressions using typeof +do_execsql_test_on_specific_db {:memory:} insert-returning-typeof { + CREATE TABLE t (id INTEGER, name TEXT, value REAL); + INSERT INTO t VALUES (1, 'test', 10.5) RETURNING typeof(id), typeof(name), typeof(value); +} {integer|text|real} + +# RETURNING with expressions using typeof on NULL +do_execsql_test_on_specific_db {:memory:} insert-returning-typeof-null { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, NULL) RETURNING typeof(value); +} {null} + +# RETURNING with expressions using printf (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-printf { + CREATE TABLE t (id INTEGER, name TEXT, value INTEGER); + INSERT INTO t VALUES (1, 'test', 42) RETURNING printf('id=%d name=%s value=%d', id, name, value); +} {"id=1 name=test value=42"} + +# RETURNING with expressions using randomblob/zeroblob (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-zeroblob { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING LENGTH(zeroblob(100)); +} {100} + +# RETURNING with expressions using changes (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-changes { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING changes(); +} {0} + +# RETURNING with expressions using last_insert_rowid (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-last-insert-rowid { + CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT); + INSERT INTO t (name) VALUES ('test') RETURNING last_insert_rowid(); +} {1} + +# RETURNING with expressions using total_changes (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-total-changes { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING total_changes(); +} {0} + +# RETURNING with expressions using likely/unlikely (optimization hints) +do_execsql_test_on_specific_db {:memory:} insert-returning-likely { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING likely(value > 0); +} {1} + +# RETURNING with expressions using iif (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-iif { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 5) RETURNING IIF(value > 0, 'positive', 'negative'); +} {positive} + +# RETURNING with expressions using json_array/json_object (if supported) +do_execsql_test_on_specific_db {:memory:} insert-returning-json-array { + CREATE TABLE t (id INTEGER, a INTEGER, b INTEGER); + INSERT INTO t VALUES (1, 2, 3) RETURNING JSON_ARRAY(a, b); +} {[2,3]} + +# RETURNING with expressions using aggregate functions (should error) +do_execsql_test_in_memory_any_error insert-returning-aggregate { + CREATE TABLE t (id INTEGER, value INTEGER); + INSERT INTO t VALUES (1, 42) RETURNING SUM(value); +} + +do_execsql_test_in_memory_any_error insert-returning-aggregate-count { + CREATE TABLE t (id INTEGER); + INSERT INTO t VALUES (1) RETURNING COUNT(*); +} \ No newline at end of file