mirror of
https://github.com/aljazceru/turso.git
synced 2025-12-26 12:34:22 +01:00
Merge 'Refactor LIMIT/OFFSET handling to support expressions' from bit-aloo
Add expression support for `LIMIT` and `OFFSET` by storing them as `Expr` instead of fixed integers. Constant expressions are folded with `try_fold_to_i64`, while dynamic ones emit runtime checks, including the new `IfNeg` opcode to clamp negative or `NULL` values to zero. The current `build_limit_offset_expr` implementation is still naive and will be refined in future work. Fixes #2913 Closes #2720
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<isize>,
|
||||
offset: Option<isize>,
|
||||
limit: &Option<Box<Expr>>,
|
||||
offset: &Option<Box<Expr>>,
|
||||
) {
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -154,8 +154,8 @@ pub enum Plan {
|
||||
CompoundSelect {
|
||||
left: Vec<(SelectPlan, ast::CompoundOperator)>,
|
||||
right_most: SelectPlan,
|
||||
limit: Option<isize>,
|
||||
offset: Option<isize>,
|
||||
limit: Option<Box<Expr>>,
|
||||
offset: Option<Box<Expr>>,
|
||||
order_by: Option<Vec<(ast::Expr, SortOrder)>>,
|
||||
},
|
||||
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<Aggregate>,
|
||||
/// limit clause
|
||||
pub limit: Option<isize>,
|
||||
pub limit: Option<Box<Expr>>,
|
||||
/// offset clause
|
||||
pub offset: Option<isize>,
|
||||
pub offset: Option<Box<Expr>>,
|
||||
/// 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<ast::Expr>, SortOrder)>,
|
||||
/// limit clause
|
||||
pub limit: Option<isize>,
|
||||
pub limit: Option<Box<Expr>>,
|
||||
/// offset clause
|
||||
pub offset: Option<isize>,
|
||||
pub offset: Option<Box<Expr>>,
|
||||
/// 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<ast::Expr>)>,
|
||||
pub where_clause: Vec<WhereTerm>,
|
||||
pub order_by: Vec<(Box<ast::Expr>, SortOrder)>,
|
||||
pub limit: Option<isize>,
|
||||
pub offset: Option<isize>,
|
||||
pub limit: Option<Box<Expr>>,
|
||||
pub offset: Option<Box<Expr>>,
|
||||
// TODO: optional RETURNING clause
|
||||
pub returning: Option<Vec<ResultSetColumn>>,
|
||||
// whether the WHERE clause is always false
|
||||
|
||||
@@ -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<isize>, Option<isize>)> {
|
||||
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::<isize>().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::<isize>().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<Expr>) {
|
||||
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<crate::Connection>,
|
||||
) -> Result<(Option<Box<Expr>>, Option<Box<Expr>>)> {
|
||||
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()))
|
||||
}
|
||||
|
||||
@@ -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<LimitCtx>,
|
||||
) -> 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<usize>,
|
||||
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<Expr>) -> Option<i64> {
|
||||
match expr.as_ref() {
|
||||
Expr::Literal(Literal::Numeric(n)) => n.parse::<i64>().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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -34,7 +34,7 @@ fn emit_values_when_single_row(
|
||||
t_ctx: &TranslateCtx,
|
||||
) -> Result<usize> {
|
||||
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 {
|
||||
|
||||
@@ -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<Pager>,
|
||||
mv_store: Option<&Arc<MvStore>>,
|
||||
) -> Result<InsnFunctionStepResult> {
|
||||
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<Self> {
|
||||
match self {
|
||||
|
||||
@@ -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} {}",
|
||||
|
||||
@@ -1076,6 +1076,10 @@ pub enum Insn {
|
||||
dest: usize, // P2: output register for result
|
||||
new_mode: Option<String>, // 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user