diff --git a/core/translate/compound_select.rs b/core/translate/compound_select.rs index 15fb4d841..8f816cd1f 100644 --- a/core/translate/compound_select.rs +++ b/core/translate/compound_select.rs @@ -1,6 +1,8 @@ use crate::schema::{Index, IndexColumn, Schema}; use crate::translate::emitter::{emit_query, LimitCtx, TranslateCtx}; +use crate::translate::expr::translate_expr; use crate::translate::plan::{Plan, QueryDestination, SelectPlan}; +use crate::translate::result_row::try_fold_expr_to_i64; use crate::vdbe::builder::{CursorType, ProgramBuilder}; use crate::vdbe::insn::Insn; use crate::vdbe::BranchOffset; @@ -31,36 +33,55 @@ pub fn emit_program_for_compound_select( let right_plan = right_most.clone(); // Trivial exit on LIMIT 0 - if let Some(limit) = limit { - if *limit == 0 { - program.result_columns = right_plan.result_columns; - program.table_references.extend(right_plan.table_references); - return Ok(()); - } + if matches!(limit.as_ref().and_then(try_fold_expr_to_i64), Some(v) if v == 0) { + program.result_columns = right_plan.result_columns; + program.table_references.extend(right_plan.table_references); + return Ok(()); } + let right_most_ctx = TranslateCtx::new( + program, + schema, + syms, + right_most.table_references.joined_tables().len(), + ); + // Each subselect shares the same limit_ctx and offset, because the LIMIT, OFFSET applies to // the entire compound select, not just a single subselect. - let limit_ctx = limit.map(|limit| { + let limit_ctx = limit.as_ref().map(|limit| { let reg = program.alloc_register(); - program.emit_insn(Insn::Integer { - value: limit as i64, - dest: reg, - }); + if let Some(val) = try_fold_expr_to_i64(limit) { + program.emit_insn(Insn::Integer { + value: val, + dest: reg, + }); + } else { + program.add_comment(program.offset(), "OFFSET expr"); + _ = translate_expr(program, None, limit, reg, &right_most_ctx.resolver); + program.emit_insn(Insn::MustBeInt { reg }); + } LimitCtx::new_shared(reg) }); - let offset_reg = offset.map(|offset| { + let offset_reg = offset.as_ref().map(|offset_expr| { let reg = program.alloc_register(); - program.emit_insn(Insn::Integer { - value: offset as i64, - dest: reg, - }); + + if let Some(val) = try_fold_expr_to_i64(offset_expr) { + // Compile-time constant offset + program.emit_insn(Insn::Integer { + value: val, + dest: reg, + }); + } else { + program.add_comment(program.offset(), "OFFSET expr"); + _ = translate_expr(program, None, offset_expr, reg, &right_most_ctx.resolver); + program.emit_insn(Insn::MustBeInt { reg }); + } let combined_reg = program.alloc_register(); program.emit_insn(Insn::OffsetLimit { offset_reg: reg, combined_reg, - limit_reg: limit_ctx.unwrap().reg_limit, + limit_reg: limit_ctx.as_ref().unwrap().reg_limit, }); reg @@ -137,8 +158,8 @@ fn emit_compound_select( let compound_select = Plan::CompoundSelect { left, right_most: plan, - limit, - offset, + limit: limit.clone(), + offset: offset.clone(), order_by, }; emit_compound_select( diff --git a/core/translate/delete.rs b/core/translate/delete.rs index 5dd26ee8c..8f9cf48ca 100644 --- a/core/translate/delete.rs +++ b/core/translate/delete.rs @@ -107,7 +107,8 @@ pub fn prepare_delete_plan( )?; // Parse the LIMIT/OFFSET clause - let (resolved_limit, resolved_offset) = limit.map_or(Ok((None, None)), |l| parse_limit(&l))?; + let (resolved_limit, resolved_offset) = + limit.map_or(Ok((None, None)), |mut l| parse_limit(&mut l, connection))?; let plan = DeletePlan { table_references, diff --git a/core/translate/display.rs b/core/translate/display.rs index f183bc3e8..384cf7f54 100644 --- a/core/translate/display.rs +++ b/core/translate/display.rs @@ -217,7 +217,7 @@ impl fmt::Display for UpdatePlan { )?; } } - if let Some(limit) = self.limit { + if let Some(limit) = self.limit.as_ref() { writeln!(f, "LIMIT: {limit}")?; } if let Some(ret) = &self.returning { diff --git a/core/translate/emitter.rs b/core/translate/emitter.rs index 29cbd210a..02d613beb 100644 --- a/core/translate/emitter.rs +++ b/core/translate/emitter.rs @@ -26,6 +26,7 @@ use crate::schema::{BTreeTable, Column, Schema, Table}; use crate::translate::compound_select::emit_program_for_compound_select; use crate::translate::expr::{emit_returning_results, ReturningValueRegisters}; use crate::translate::plan::{DeletePlan, Plan, QueryDestination, Search}; +use crate::translate::result_row::try_fold_expr_to_i64; use crate::translate::values::emit_values; use crate::util::exprs_are_equivalent; use crate::vdbe::builder::{CursorKey, CursorType, ProgramBuilder}; @@ -227,7 +228,7 @@ fn emit_program_for_select( ); // Trivial exit on LIMIT 0 - if let Some(limit) = plan.limit { + if let Some(limit) = plan.limit.as_ref().and_then(try_fold_expr_to_i64) { if limit == 0 { program.result_columns = plan.result_columns; program.table_references.extend(plan.table_references); @@ -256,7 +257,7 @@ pub fn emit_query<'a>( // Emit subqueries first so the results can be read in the main query loop. emit_subqueries(program, t_ctx, &mut plan.table_references)?; - init_limit(program, t_ctx, plan.limit, plan.offset); + init_limit(program, t_ctx, &plan.limit, &plan.offset); // No rows will be read from source table loops if there is a constant false condition eg. WHERE 0 // however an aggregation might still happen, @@ -404,13 +405,15 @@ fn emit_program_for_delete( ); // exit early if LIMIT 0 - if let Some(0) = plan.limit { - program.result_columns = plan.result_columns; - program.table_references.extend(plan.table_references); - return Ok(()); + if let Some(limit) = plan.limit.as_ref().and_then(try_fold_expr_to_i64) { + if limit == 0 { + program.result_columns = plan.result_columns; + program.table_references.extend(plan.table_references); + return Ok(()); + } } - init_limit(program, &mut t_ctx, plan.limit, None); + init_limit(program, &mut t_ctx, &plan.limit, &None); // No rows will be read from source table loops if there is a constant false condition eg. WHERE 0 let after_main_loop_label = program.allocate_label(); @@ -660,13 +663,15 @@ fn emit_program_for_update( ); // Exit on LIMIT 0 - if let Some(0) = plan.limit { - program.result_columns = plan.returning.unwrap_or_default(); - program.table_references.extend(plan.table_references); - return Ok(()); + if let Some(limit) = plan.limit.as_ref().and_then(try_fold_expr_to_i64) { + if limit == 0 { + program.result_columns = plan.returning.unwrap_or_default(); + program.table_references.extend(plan.table_references); + return Ok(()); + } } - init_limit(program, &mut t_ctx, plan.limit, plan.offset); + init_limit(program, &mut t_ctx, &plan.limit, &plan.offset); let after_main_loop_label = program.allocate_label(); t_ctx.label_main_loop_end = Some(after_main_loop_label); if plan.contains_constant_false_condition { @@ -1541,41 +1546,69 @@ pub fn emit_cdc_insns( }); Ok(()) } - /// Initialize the limit/offset counters and registers. /// In case of compound SELECTs, the limit counter is initialized only once, /// hence [LimitCtx::initialize_counter] being false in those cases. fn init_limit( program: &mut ProgramBuilder, t_ctx: &mut TranslateCtx, - limit: Option, - offset: Option, + limit: &Option>, + offset: &Option>, ) { - if t_ctx.limit_ctx.is_none() { - t_ctx.limit_ctx = limit.map(|_| LimitCtx::new(program)); + if t_ctx.limit_ctx.is_none() && limit.is_some() { + t_ctx.limit_ctx = Some(LimitCtx::new(program)); } - let Some(limit_ctx) = t_ctx.limit_ctx else { + let Some(limit_ctx) = &t_ctx.limit_ctx else { return; }; if limit_ctx.initialize_counter { - program.emit_insn(Insn::Integer { - value: limit.expect("limit must be Some if limit_ctx is Some") as i64, - dest: limit_ctx.reg_limit, - }); + if let Some(expr) = limit { + if let Some(value) = try_fold_expr_to_i64(expr) { + program.emit_insn(Insn::Integer { + value, + dest: limit_ctx.reg_limit, + }); + } else { + let r = limit_ctx.reg_limit; + program.add_comment(program.offset(), "OFFSET expr"); + _ = translate_expr(program, None, expr, r, &t_ctx.resolver); + program.emit_insn(Insn::MustBeInt { reg: r }); + } + } } - if t_ctx.reg_offset.is_none() && offset.is_some_and(|n| n.ne(&0)) { - let reg = program.alloc_register(); - t_ctx.reg_offset = Some(reg); - program.emit_insn(Insn::Integer { - value: offset.unwrap() as i64, - dest: reg, - }); - let combined_reg = program.alloc_register(); - t_ctx.reg_limit_offset_sum = Some(combined_reg); - program.emit_insn(Insn::OffsetLimit { - limit_reg: t_ctx.limit_ctx.unwrap().reg_limit, - offset_reg: reg, - combined_reg, - }); + + if t_ctx.reg_offset.is_none() { + if let Some(expr) = offset { + if let Some(value) = try_fold_expr_to_i64(expr) { + if value != 0 { + let reg = program.alloc_register(); + t_ctx.reg_offset = Some(reg); + program.emit_insn(Insn::Integer { value, dest: reg }); + let combined_reg = program.alloc_register(); + t_ctx.reg_limit_offset_sum = Some(combined_reg); + program.emit_insn(Insn::OffsetLimit { + limit_reg: limit_ctx.reg_limit, + offset_reg: reg, + combined_reg, + }); + } + } else { + let reg = program.alloc_register(); + t_ctx.reg_offset = Some(reg); + let r = reg; + + program.add_comment(program.offset(), "OFFSET expr"); + _ = translate_expr(program, None, expr, r, &t_ctx.resolver); + program.emit_insn(Insn::MustBeInt { reg: r }); + + let combined_reg = program.alloc_register(); + t_ctx.reg_limit_offset_sum = Some(combined_reg); + program.emit_insn(Insn::OffsetLimit { + limit_reg: limit_ctx.reg_limit, + offset_reg: reg, + combined_reg, + }); + } + } } } diff --git a/core/translate/order_by.rs b/core/translate/order_by.rs index 129bc8d5b..da980bd9a 100644 --- a/core/translate/order_by.rs +++ b/core/translate/order_by.rs @@ -117,7 +117,13 @@ pub fn emit_order_by( }); program.preassign_label_to_next_insn(sort_loop_start_label); - emit_offset(program, plan, sort_loop_next_label, t_ctx.reg_offset); + emit_offset( + program, + plan, + sort_loop_next_label, + t_ctx.reg_offset, + &t_ctx.resolver, + ); program.emit_insn(Insn::SorterData { cursor_id: sort_cursor, diff --git a/core/translate/plan.rs b/core/translate/plan.rs index e43cdbd76..082e39f96 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -154,8 +154,8 @@ pub enum Plan { CompoundSelect { left: Vec<(SelectPlan, ast::CompoundOperator)>, right_most: SelectPlan, - limit: Option, - offset: Option, + limit: Option>, + offset: Option>, order_by: Option>, }, Delete(DeletePlan), @@ -292,9 +292,9 @@ pub struct SelectPlan { /// all the aggregates collected from the result columns, order by, and (TODO) having clauses pub aggregates: Vec, /// limit clause - pub limit: Option, + pub limit: Option>, /// offset clause - pub offset: Option, + pub offset: Option>, /// query contains a constant condition that is always false pub contains_constant_false_condition: bool, /// the destination of the resulting rows from this plan. @@ -378,9 +378,9 @@ pub struct DeletePlan { /// order by clause pub order_by: Vec<(Box, SortOrder)>, /// limit clause - pub limit: Option, + pub limit: Option>, /// offset clause - pub offset: Option, + pub offset: Option>, /// query contains a constant condition that is always false pub contains_constant_false_condition: bool, /// Indexes that must be updated by the delete operation. @@ -394,8 +394,8 @@ pub struct UpdatePlan { pub set_clauses: Vec<(usize, Box)>, pub where_clause: Vec, pub order_by: Vec<(Box, SortOrder)>, - pub limit: Option, - pub offset: Option, + pub limit: Option>, + pub offset: Option>, // TODO: optional RETURNING clause pub returning: Option>, // whether the WHERE clause is always false diff --git a/core/translate/planner.rs b/core/translate/planner.rs index b0b696bb4..cf5a4314d 100644 --- a/core/translate/planner.rs +++ b/core/translate/planner.rs @@ -13,6 +13,7 @@ use super::{ use crate::function::{AggFunc, ExtFunc}; use crate::translate::expr::WalkControl; use crate::{ + ast::Limit, function::Func, schema::{Schema, Table}, translate::expr::walk_expr_mut, @@ -23,8 +24,8 @@ use crate::{ use turso_macros::match_ignore_ascii_case; use turso_parser::ast::Literal::Null; use turso_parser::ast::{ - self, As, Expr, FromClause, JoinType, Limit, Literal, Materialized, QualifiedName, - TableInternalId, UnaryOperator, With, + self, As, Expr, FromClause, JoinType, Literal, Materialized, QualifiedName, TableInternalId, + With, }; pub const ROWID: &str = "rowid"; @@ -1145,44 +1146,6 @@ fn parse_join( Ok(()) } -pub fn parse_limit(limit: &Limit) -> Result<(Option, Option)> { - let offset_val = match &limit.offset { - Some(offset_expr) => match offset_expr.as_ref() { - Expr::Literal(ast::Literal::Numeric(n)) => n.parse().ok(), - // If OFFSET is negative, the result is as if OFFSET is zero - Expr::Unary(UnaryOperator::Negative, expr) => { - if let Expr::Literal(ast::Literal::Numeric(ref n)) = &**expr { - n.parse::().ok().map(|num| -num) - } else { - crate::bail_parse_error!("Invalid OFFSET clause"); - } - } - _ => crate::bail_parse_error!("Invalid OFFSET clause"), - }, - None => Some(0), - }; - - if let Expr::Literal(ast::Literal::Numeric(n)) = limit.expr.as_ref() { - Ok((n.parse().ok(), offset_val)) - } else if let Expr::Unary(UnaryOperator::Negative, expr) = limit.expr.as_ref() { - if let Expr::Literal(ast::Literal::Numeric(n)) = expr.as_ref() { - let limit_val = n.parse::().ok().map(|num| -num); - Ok((limit_val, offset_val)) - } else { - crate::bail_parse_error!("Invalid LIMIT clause"); - } - } else if let Expr::Id(id) = limit.expr.as_ref() { - let id_bytes = id.as_str().as_bytes(); - match_ignore_ascii_case!(match id_bytes { - b"true" => Ok((Some(1), offset_val)), - b"false" => Ok((Some(0), offset_val)), - _ => crate::bail_parse_error!("Invalid LIMIT clause"), - }) - } else { - crate::bail_parse_error!("Invalid LIMIT clause"); - } -} - pub fn break_predicate_at_and_boundaries(predicate: &Expr, out_predicates: &mut Vec) { match predicate { Expr::Binary(left, ast::Operator::And, right) => { @@ -1215,3 +1178,16 @@ where } Ok(None) } + +#[allow(clippy::type_complexity)] +pub fn parse_limit( + limit: &mut Limit, + connection: &std::sync::Arc, +) -> Result<(Option>, Option>)> { + let mut empty_refs = TableReferences::new(Vec::new(), Vec::new()); + bind_column_references(&mut limit.expr, &mut empty_refs, None, connection)?; + if let Some(ref mut off_expr) = limit.offset { + bind_column_references(off_expr, &mut empty_refs, None, connection)?; + } + Ok((Some(limit.expr.clone()), limit.offset.clone())) +} diff --git a/core/translate/result_row.rs b/core/translate/result_row.rs index f2b722988..2e636caca 100644 --- a/core/translate/result_row.rs +++ b/core/translate/result_row.rs @@ -1,3 +1,5 @@ +use turso_parser::ast::{Expr, Literal, Name, Operator, UnaryOperator}; + use crate::{ vdbe::{ builder::ProgramBuilder, @@ -30,7 +32,7 @@ pub fn emit_select_result( limit_ctx: Option, ) -> Result<()> { if let (Some(jump_to), Some(_)) = (offset_jump_to, label_on_limit_reached) { - emit_offset(program, plan, jump_to, reg_offset); + emit_offset(program, plan, jump_to, reg_offset, resolver); } let start_reg = reg_result_cols_start; @@ -163,16 +165,68 @@ pub fn emit_offset( plan: &SelectPlan, jump_to: BranchOffset, reg_offset: Option, + resolver: &Resolver, ) { - match plan.offset { - Some(offset) if offset > 0 => { - program.add_comment(program.offset(), "OFFSET"); + let Some(offset_expr) = &plan.offset else { + return; + }; + + if let Some(val) = try_fold_expr_to_i64(offset_expr) { + if val > 0 { + program.add_comment(program.offset(), "OFFSET const"); program.emit_insn(Insn::IfPos { reg: reg_offset.expect("reg_offset must be Some"), target_pc: jump_to, decrement_by: 1, }); } - _ => {} + return; + } + + let r = reg_offset.expect("reg_offset must be Some"); + + program.add_comment(program.offset(), "OFFSET expr"); + + _ = translate_expr(program, None, offset_expr, r, resolver); + + program.emit_insn(Insn::MustBeInt { reg: r }); + + program.emit_insn(Insn::IfPos { + reg: r, + target_pc: jump_to, + decrement_by: 1, + }); +} + +#[allow(clippy::borrowed_box)] +pub fn try_fold_expr_to_i64(expr: &Box) -> Option { + match expr.as_ref() { + Expr::Literal(Literal::Numeric(n)) => n.parse::().ok(), + Expr::Literal(Literal::Null) => Some(0), + Expr::Id(Name::Ident(s)) => { + let lowered = s.to_ascii_lowercase(); + if lowered == "true" { + Some(1) + } else if lowered == "false" { + Some(0) + } else { + None + } + } + Expr::Unary(UnaryOperator::Negative, inner) => try_fold_expr_to_i64(inner).map(|v| -v), + Expr::Unary(UnaryOperator::Positive, inner) => try_fold_expr_to_i64(inner), + Expr::Binary(left, op, right) => { + let l = try_fold_expr_to_i64(left)?; + let r = try_fold_expr_to_i64(right)?; + match op { + Operator::Add => Some(l.saturating_add(r)), + Operator::Subtract => Some(l.saturating_sub(r)), + Operator::Multiply => Some(l.saturating_mul(r)), + Operator::Divide if r != 0 => Some(l.saturating_div(r)), + _ => None, + } + } + + _ => None, } } diff --git a/core/translate/select.rs b/core/translate/select.rs index cee03b87a..37de54b4d 100644 --- a/core/translate/select.rs +++ b/core/translate/select.rs @@ -150,8 +150,7 @@ pub fn prepare_select_plan( } let (limit, offset) = select .limit - .as_ref() - .map_or(Ok((None, None)), parse_limit)?; + .map_or(Ok((None, None)), |mut l| parse_limit(&mut l, connection))?; // FIXME: handle ORDER BY for compound selects if !select.order_by.is_empty() { @@ -431,8 +430,8 @@ fn prepare_one_select_plan( plan.order_by = key; // Parse the LIMIT/OFFSET clause - (plan.limit, plan.offset) = limit.as_ref().map_or(Ok((None, None)), parse_limit)?; - + (plan.limit, plan.offset) = + limit.map_or(Ok((None, None)), |mut l| parse_limit(&mut l, connection))?; // Return the unoptimized query plan Ok(plan) } diff --git a/core/translate/update.rs b/core/translate/update.rs index f94ebe118..676fd37ca 100644 --- a/core/translate/update.rs +++ b/core/translate/update.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use crate::schema::{BTreeTable, Column, Type}; use crate::translate::optimizer::optimize_select_plan; use crate::translate::plan::{Operation, QueryDestination, Scan, Search, SelectPlan}; +use crate::translate::planner::parse_limit; use crate::vdbe::builder::CursorType; use crate::{ bail_parse_error, @@ -21,8 +22,7 @@ use super::plan::{ ColumnUsedMask, IterationDirection, JoinedTable, Plan, ResultSetColumn, TableReferences, UpdatePlan, }; -use super::planner::bind_column_references; -use super::planner::{parse_limit, parse_where}; +use super::planner::{bind_column_references, parse_where}; /* * Update is simple. By default we scan the table, and for each row, we check the WHERE * clause. If it evaluates to true, we build the new record with the updated value and insert. @@ -331,7 +331,10 @@ pub fn prepare_update_plan( }; // Parse the LIMIT/OFFSET clause - let (limit, offset) = body.limit.as_ref().map_or(Ok((None, None)), parse_limit)?; + let (limit, offset) = body + .limit + .as_mut() + .map_or(Ok((None, None)), |l| parse_limit(l, connection))?; // Check what indexes will need to be updated by checking set_clauses and see // if a column is contained in an index. diff --git a/core/translate/values.rs b/core/translate/values.rs index 8315290e6..63f90055e 100644 --- a/core/translate/values.rs +++ b/core/translate/values.rs @@ -34,7 +34,7 @@ fn emit_values_when_single_row( t_ctx: &TranslateCtx, ) -> Result { let end_label = program.allocate_label(); - emit_offset(program, plan, end_label, t_ctx.reg_offset); + emit_offset(program, plan, end_label, t_ctx.reg_offset, &t_ctx.resolver); let first_row = &plan.values[0]; let row_len = first_row.len(); let start_reg = program.alloc_registers(row_len); @@ -87,7 +87,7 @@ fn emit_toplevel_values( }); let goto_label = program.allocate_label(); - emit_offset(program, plan, goto_label, t_ctx.reg_offset); + emit_offset(program, plan, goto_label, t_ctx.reg_offset, &t_ctx.resolver); let row_len = plan.values[0].len(); let copy_start_reg = program.alloc_registers(row_len); for i in 0..row_len { diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index ec7ab3408..42e034320 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -7377,6 +7377,33 @@ pub fn op_alter_column( Ok(InsnFunctionStepResult::Step) } +pub fn op_if_neg( + program: &Program, + state: &mut ProgramState, + insn: &Insn, + pager: &Rc, + mv_store: Option<&Arc>, +) -> Result { + load_insn!(IfNeg { reg, target_pc }, insn); + + match &state.registers[*reg] { + Register::Value(Value::Integer(i)) if *i < 0 => { + state.pc = target_pc.as_offset_int(); + } + Register::Value(Value::Float(f)) if *f < 0.0 => { + state.pc = target_pc.as_offset_int(); + } + Register::Value(Value::Null) => { + state.pc += 1; + } + _ => { + state.pc += 1; + } + } + + Ok(InsnFunctionStepResult::Step) +} + impl Value { pub fn exec_lower(&self) -> Option { match self { diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 054e7308b..fe3b23073 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -1709,6 +1709,15 @@ pub fn insn_to_str( 0, format!("collation={collation}"), ), + Insn::IfNeg { reg, target_pc } => ( + "IfNeg", + *reg as i32, + target_pc.as_debug_int(), + 0, + Value::build_text(""), + 0, + format!("if (r[{}] < 0) goto {}", reg, target_pc.as_debug_int()), + ), }; format!( "{:<4} {:<17} {:<4} {:<4} {:<4} {:<13} {:<2} {}", diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 45210fd42..799eb86b9 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -1076,6 +1076,10 @@ pub enum Insn { dest: usize, // P2: output register for result new_mode: Option, // P3: new journal mode (if setting) }, + IfNeg { + reg: usize, + target_pc: BranchOffset, + }, } impl Insn { @@ -1213,6 +1217,7 @@ impl Insn { Insn::AlterColumn { .. } => execute::op_alter_column, Insn::MaxPgcnt { .. } => execute::op_max_pgcnt, Insn::JournalMode { .. } => execute::op_journal_mode, + Insn::IfNeg { .. } => execute::op_if_neg, } } } diff --git a/testing/select.test b/testing/select.test index 9ed482e4e..6efc3e061 100755 --- a/testing/select.test +++ b/testing/select.test @@ -59,6 +59,12 @@ do_execsql_test_error select-doubly-qualified-wrong-column { SELECT main.users.wrong FROM users LIMIT 0; } {.*} +do_execsql_test select-limit-expression { + select price from products limit 2 + 1 - 1; +} {79.0 +82.0} + + # ORDER BY id here because sqlite uses age_idx here and we (yet) don't so force it to evaluate in ID order do_execsql_test select-limit-true { SELECT id FROM users ORDER BY id LIMIT true; @@ -743,3 +749,21 @@ do_execsql_test_on_specific_db {:memory:} select-in-complex { SELECT * FROM test_table WHERE category IN ('A', 'B') AND value IN (10, 30, 40); } {1|A|10 3|A|30} + +foreach {testname limit ans} { + limit-const-1 1 {1} + limit-text-2 '2' {1 2} + limit-bool-true true {1} + limit-expr-add 1+2+3 {1 2 3 4 5 6} + limit-expr-sub 5-2-3 {} + limit-expr-paren (1+1)*2 {1 2 3 4} + limit-bool-add true+2 {1 2 3} + limit-text-math '2'*2+1 {1 2 3 4 5} + limit-bool-false-add false+4 {1 2 3 4} + limit-mixed-math (1+'1')*(1+1)-(5/5) {1 2 3} + limit-text-bool ('false'+2) {1 2} + limit-coalesce COALESCE(NULL,0+1) {1} +} { + do_execsql_test limit-complex-exprs-$testname \ + "SELECT id FROM users ORDER BY id LIMIT $limit" $ans +} \ No newline at end of file