Merge 'Redesign parameter binding in query translator' from Preston Thorpe

closes #1467
## Example:
Previously as explained in #1449, our parameter binding wasn't working
properly because we would essentially
assign the first index of whatever was translated first
```console
limbo> create table t (id integer primary key, name text, age integer);
limbo> explain select * from t where name = ? and id > ? and age between ? and ?;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     20    0                    0   Start at 20
1     OpenRead           0     2     0                    0   table=t, root=2
2     Variable           1     4     0                    0   r[4]=parameter(1) # always 1
3     IsNull             4     19    0                    0   if (r[4]==NULL) goto 19
4     SeekGT             0     19    4                    0   key=[4..4]
5       Column           0     1     5                    0   r[5]=t.name
6       Variable         2     6     0                    0   r[6]=parameter(2) # always 2
7       Ne               5     6     18                   0   if r[5]!=r[6] goto 18
8       Variable         3     7     0                    0   r[7]=parameter(3) # etc...
9       Column           0     2     8                    0   r[8]=t.age
10      Gt               7     8     18                   0   if r[7]>r[8] goto 18
11      Column           0     2     9                    0   r[9]=t.age
12      Variable         4     10    0                    0   r[10]=parameter(4)
13      Gt               9     10    18                   0   if r[9]>r[10] goto 18
14      RowId            0     1     0                    0   r[1]=t.rowid
15      Column           0     1     2                    0   r[2]=t.name
16      Column           0     2     3                    0   r[3]=t.age
17      ResultRow        1     3     0                    0   output=r[1..3]
18    Next               0     5     0                    0
19    Halt               0     0     0                    0
20    Transaction        0     0     0                    0   write=false
21    Goto               0     1     0                    0
```
## Solution:
`rewrite_expr` currently is used to transform `true|false` to `1|0`, so
it has been adapted to transform anonymous `Expr::Variable`s to named
variables, inserting the appropriate index of the parameter by passing
in a counter.
```rust
        ast::Expr::Variable(var) => {
            if var.is_empty() {
                // rewrite anonymous variables only, ensure that the `param_idx` starts at 1 and
                // all the expressions are rewritten in the order they come in the statement
                *expr = ast::Expr::Variable(format!("{}{param_idx}", PARAM_PREFIX));
                *param_idx += 1;
            }
            Ok(())
        }
```
# Corrected output: (notice the seek)
```console
limbo> explain select * from t where name = ? and id > ? and age between ? and ?;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     20    0                    0   Start at 20
1     OpenRead           0     2     0                    0   table=t, root=2
2     Variable           2     4     0                    0   r[4]=parameter(2)
3     IsNull             4     19    0                    0   if (r[4]==NULL) goto 19
4     SeekGT             0     19    4                    0   key=[4..4]
5       Column           0     1     5                    0   r[5]=t.name
6       Variable         1     6     0                    0   r[6]=parameter(1)
7       Ne               5     6     18                   0   if r[5]!=r[6] goto 18
8       Variable         3     7     0                    0   r[7]=parameter(3)
9       Column           0     2     8                    0   r[8]=t.age
10      Gt               7     8     18                   0   if r[7]>r[8] goto 18
11      Column           0     2     9                    0   r[9]=t.age
12      Variable         4     10    0                    0   r[10]=parameter(4)
13      Gt               9     10    18                   0   if r[9]>r[10] goto 18
14      RowId            0     1     0                    0   r[1]=t.rowid
15      Column           0     1     2                    0   r[2]=t.name
16      Column           0     2     3                    0   r[3]=t.age
17      ResultRow        1     3     0                    0   output=r[1..3]
18    Next               0     5     0                    0
19    Halt               0     0     0                    0
20    Transaction        0     0     0                    0   write=false
21    Goto               0     1     0                    0
```
## And a `Delete`:
```console
limbo> explain delete from t where name = ? and age > ? and id > ?;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     15    0                    0   Start at 15
1     OpenWrite          0     2     0                    0
2     Variable           3     1     0                    0   r[1]=parameter(3)
3     IsNull             1     14    0                    0   if (r[1]==NULL) goto 14
4     SeekGT             0     14    1                    0   key=[1..1]
5       Column           0     1     2                    0   r[2]=t.name
6       Variable         1     3     0                    0   r[3]=parameter(1)
7       Ne               2     3     13                   0   if r[2]!=r[3] goto 13
8       Column           0     2     4                    0   r[4]=t.age
9       Variable         2     5     0                    0   r[5]=parameter(2)
10      Le               4     5     13                   0   if r[4]<=r[5] goto 13
11      RowId            0     6     0                    0   r[6]=t.rowid
12      Delete           0     0     0                    0
13    Next               0     5     0                    0
14    Halt               0     0     0                    0
15    Transaction        0     1     0                    0   write=true
16    Goto               0     1     0                    0
```

Reviewed-by: Jussi Saurio <jussi.saurio@gmail.com>

Closes #1475
This commit is contained in:
Jussi Saurio
2025-05-14 09:26:06 +03:00
6 changed files with 374 additions and 120 deletions

View File

@@ -1,6 +1,7 @@
use super::ast;
use std::num::NonZero;
pub const PARAM_PREFIX: &str = "__param_";
#[derive(Clone, Debug)]
pub enum Parameter {
Anonymous(NonZero<usize>),
@@ -24,52 +25,10 @@ impl Parameter {
}
}
#[derive(Debug)]
struct InsertContext {
param_positions: Vec<usize>,
current_col_value_idx: usize,
}
impl InsertContext {
fn new(param_positions: Vec<usize>) -> Self {
Self {
param_positions,
current_col_value_idx: 0,
}
}
/// Find the relevant parameter index needed for the current value index of insert stmt
/// Example for table t (a,b,c):
/// `insert into t (c,a,b) values (?,?,?)`
///
/// col a -> value_index 1
/// col b -> value_index 2
/// col c -> value_index 0
///
/// however translation will always result in parameters 1, 2, 3
/// because columns are translated in the table order so `col a` gets
/// translated first, translate_expr calls parameters.push and always gets index 1.
///
/// Instead, we created an array representing all the value_index's that are type
/// Expr::Variable, in the case above would be [1, 2, 0], and stored it in insert_ctx.
/// That array can be used to look up the necessary parameter index by searching for the value
/// index in the array and returning the index of that value + 1.
/// value_index-> [1, 2, 0]
/// param index-> |0, 1, 2|
fn get_insert_param_index(&self) -> Option<NonZero<usize>> {
self.param_positions
.iter()
.position(|param| param.eq(&self.current_col_value_idx))
.map(|p| NonZero::new(p + 1).unwrap())
}
}
#[derive(Debug)]
pub struct Parameters {
index: NonZero<usize>,
pub list: Vec<Parameter>,
// Context for reordering parameters during insert statements
insert_ctx: Option<InsertContext>,
}
impl Default for Parameters {
@@ -83,7 +42,6 @@ impl Parameters {
Self {
index: 1.try_into().unwrap(),
list: vec![],
insert_ctx: None,
}
}
@@ -93,18 +51,6 @@ impl Parameters {
params.len()
}
/// Begin preparing for an Insert statement by providing the array of values from the Insert body.
pub fn init_insert_parameters(&mut self, values: &[Vec<ast::Expr>]) {
self.insert_ctx = Some(InsertContext::new(expected_param_indicies(values)));
}
/// Set the value index for the column currently being translated for an Insert stmt.
pub fn set_insert_value_index(&mut self, idx: usize) {
if let Some(ctx) = &mut self.insert_ctx {
ctx.current_col_value_idx = idx;
}
}
pub fn name(&self, index: NonZero<usize>) -> Option<String> {
self.list.iter().find_map(|p| match p {
Parameter::Anonymous(i) if *i == index => Some("?".to_string()),
@@ -132,15 +78,16 @@ impl Parameters {
pub fn push(&mut self, name: impl AsRef<str>) -> NonZero<usize> {
match name.as_ref() {
"" => {
param if param.is_empty() || param.starts_with(PARAM_PREFIX) => {
let index = self.next_index();
self.list.push(Parameter::Anonymous(index));
tracing::trace!("anonymous parameter at {index}");
if let Some(idx) = &self.insert_ctx {
idx.get_insert_param_index().unwrap_or(index)
let use_idx = if let Some(idx) = param.strip_prefix(PARAM_PREFIX) {
idx.parse().unwrap()
} else {
index
}
};
self.list.push(Parameter::Anonymous(use_idx));
tracing::trace!("anonymous parameter at {use_idx}");
use_idx
}
name if name.starts_with(['$', ':', '@', '#']) => {
match self
@@ -175,14 +122,3 @@ impl Parameters {
}
}
}
/// Gather all the expected indicies of all Expr::Variable
/// in the provided array of insert values.
pub fn expected_param_indicies(cols: &[Vec<ast::Expr>]) -> Vec<usize> {
cols.iter()
.flat_map(|col| col.iter())
.enumerate()
.filter(|(_, col)| matches!(col, ast::Expr::Variable(_)))
.map(|(i, _)| i)
.collect::<Vec<_>>()
}

View File

@@ -21,6 +21,7 @@ use crate::{Result, SymbolTable, VirtualTable};
use super::emitter::Resolver;
use super::expr::{translate_expr_no_constant_opt, NoConstantOptReason};
use super::optimizer::rewrite_expr;
#[allow(clippy::too_many_arguments)]
pub fn translate_insert(
@@ -30,7 +31,7 @@ pub fn translate_insert(
on_conflict: &Option<ResolveType>,
tbl_name: &QualifiedName,
columns: &Option<DistinctNames>,
body: &InsertBody,
body: &mut InsertBody,
_returning: &Option<Vec<ResultColumn>>,
syms: &SymbolTable,
) -> Result<ProgramBuilder> {
@@ -99,14 +100,16 @@ pub fn translate_insert(
.collect::<Vec<(&String, usize, usize)>>();
let root_page = btree_table.root_page;
let values = match body {
InsertBody::Select(select, _) => match &select.body.select.deref() {
OneSelect::Values(values) => values,
InsertBody::Select(ref mut select, _) => match select.body.select.as_mut() {
OneSelect::Values(ref mut values) => values,
_ => todo!(),
},
InsertBody::DefaultValues => &vec![vec![]],
InsertBody::DefaultValues => &mut vec![vec![]],
};
// prepare parameters by tracking the number of variables we will be binding to values later on
program.parameters.init_insert_parameters(values);
let mut param_idx = 1;
for expr in values.iter_mut().flat_map(|v| v.iter_mut()) {
rewrite_expr(expr, &mut param_idx)?;
}
let column_mappings = resolve_columns_for_insert(&table, columns, values)?;
let index_col_mappings = resolve_indicies_for_insert(schema, table.as_ref(), &column_mappings)?;
@@ -153,9 +156,8 @@ pub fn translate_insert(
program.preassign_label_to_next_insn(start_offset_label);
for (i, value) in values.iter().enumerate() {
for value in values.iter() {
populate_column_registers(
i,
&mut program,
value,
&column_mappings,
@@ -193,7 +195,6 @@ pub fn translate_insert(
});
populate_column_registers(
0,
&mut program,
&values[0],
&column_mappings,
@@ -585,7 +586,6 @@ fn resolve_indicies_for_insert(
/// Populates the column registers with values for a single row
#[allow(clippy::too_many_arguments)]
fn populate_column_registers(
row_idx: usize,
program: &mut ProgramBuilder,
value: &[Expr],
column_mappings: &[ColumnMapping],
@@ -609,14 +609,6 @@ fn populate_column_registers(
} else {
target_reg
};
// We need the 'parameters' to be aware of the value_index of the current row
// so it can map it to the correct parameter index in the Variable opcode
// but we need to make sure the value_index is not overwritten if this is a multi-row
// insert. For 'insert into t values: (?,?), (?,?);'
// value_index should be (1,2),(3,4) instead of (1,2),(1,2), so multiply by col length
program
.parameters
.set_insert_value_index(value_index + (column_mappings.len() * row_idx));
translate_expr_no_constant_opt(
program,
None,
@@ -680,8 +672,6 @@ fn translate_virtual_table_insert(
InsertBody::DefaultValues => &vec![],
_ => crate::bail_parse_error!("Unsupported INSERT body for virtual tables"),
};
// initiate parameters by tracking the number of variables we will be binding to values
program.parameters.init_insert_parameters(values);
let table = Table::Virtual(virtual_table.clone());
let column_mappings = resolve_columns_for_insert(&table, columns, values)?;
let registers_start = program.alloc_registers(2);
@@ -700,7 +690,6 @@ fn translate_virtual_table_insert(
let values_reg = program.alloc_registers(column_mappings.len());
populate_column_registers(
0,
program,
&values[0],
&column_mappings,

View File

@@ -198,7 +198,7 @@ pub fn translate(
or_conflict,
tbl_name,
columns,
body,
mut body,
returning,
} = *insert;
change_cnt_on = true;
@@ -209,7 +209,7 @@ pub fn translate(
&or_conflict,
&tbl_name,
&columns,
&body,
&mut body,
&returning,
syms,
)?

View File

@@ -3,6 +3,7 @@ use std::{cmp::Ordering, collections::HashMap, sync::Arc};
use limbo_sqlite3_parser::ast::{self, Expr, SortOrder};
use crate::{
parameters::PARAM_PREFIX,
schema::{Index, IndexColumn, Schema},
translate::plan::TerminationKey,
types::SeekOp,
@@ -416,23 +417,24 @@ fn eliminate_constant_conditions(
}
fn rewrite_exprs_select(plan: &mut SelectPlan) -> Result<()> {
let mut param_count = 1;
for rc in plan.result_columns.iter_mut() {
rewrite_expr(&mut rc.expr)?;
rewrite_expr(&mut rc.expr, &mut param_count)?;
}
for agg in plan.aggregates.iter_mut() {
rewrite_expr(&mut agg.original_expr)?;
rewrite_expr(&mut agg.original_expr, &mut param_count)?;
}
for cond in plan.where_clause.iter_mut() {
rewrite_expr(&mut cond.expr)?;
rewrite_expr(&mut cond.expr, &mut param_count)?;
}
if let Some(group_by) = &mut plan.group_by {
for expr in group_by.exprs.iter_mut() {
rewrite_expr(expr)?;
rewrite_expr(expr, &mut param_count)?;
}
}
if let Some(order_by) = &mut plan.order_by {
for (expr, _) in order_by.iter_mut() {
rewrite_expr(expr)?;
rewrite_expr(expr, &mut param_count)?;
}
}
@@ -440,27 +442,29 @@ fn rewrite_exprs_select(plan: &mut SelectPlan) -> Result<()> {
}
fn rewrite_exprs_delete(plan: &mut DeletePlan) -> Result<()> {
let mut param_idx = 1;
for cond in plan.where_clause.iter_mut() {
rewrite_expr(&mut cond.expr)?;
rewrite_expr(&mut cond.expr, &mut param_idx)?;
}
Ok(())
}
fn rewrite_exprs_update(plan: &mut UpdatePlan) -> Result<()> {
if let Some(rc) = plan.returning.as_mut() {
for rc in rc.iter_mut() {
rewrite_expr(&mut rc.expr)?;
}
}
let mut param_idx = 1;
for (_, expr) in plan.set_clauses.iter_mut() {
rewrite_expr(expr)?;
rewrite_expr(expr, &mut param_idx)?;
}
for cond in plan.where_clause.iter_mut() {
rewrite_expr(&mut cond.expr)?;
rewrite_expr(&mut cond.expr, &mut param_idx)?;
}
if let Some(order_by) = &mut plan.order_by {
for (expr, _) in order_by.iter_mut() {
rewrite_expr(expr)?;
rewrite_expr(expr, &mut param_idx)?;
}
}
if let Some(rc) = plan.returning.as_mut() {
for rc in rc.iter_mut() {
rewrite_expr(&mut rc.expr, &mut param_idx)?;
}
}
Ok(())
@@ -1856,7 +1860,7 @@ pub fn try_extract_rowid_search_expression(
}
}
fn rewrite_expr(expr: &mut ast::Expr) -> Result<()> {
pub fn rewrite_expr(expr: &mut ast::Expr, param_idx: &mut usize) -> Result<()> {
match expr {
ast::Expr::Id(id) => {
// Convert "true" and "false" to 1 and 0
@@ -1870,6 +1874,15 @@ fn rewrite_expr(expr: &mut ast::Expr) -> Result<()> {
}
Ok(())
}
ast::Expr::Variable(var) => {
if var.is_empty() {
// rewrite anonymous variables only, ensure that the `param_idx` starts at 1 and
// all the expressions are rewritten in the order they come in the statement
*expr = ast::Expr::Variable(format!("{}{param_idx}", PARAM_PREFIX));
*param_idx += 1;
}
Ok(())
}
ast::Expr::Between {
lhs,
not,
@@ -1884,9 +1897,9 @@ fn rewrite_expr(expr: &mut ast::Expr) -> Result<()> {
(ast::Operator::LessEquals, ast::Operator::LessEquals)
};
rewrite_expr(start)?;
rewrite_expr(lhs)?;
rewrite_expr(end)?;
rewrite_expr(start, param_idx)?;
rewrite_expr(lhs, param_idx)?;
rewrite_expr(end, param_idx)?;
let start = start.take_ownership();
let lhs = lhs.take_ownership();
@@ -1912,7 +1925,7 @@ fn rewrite_expr(expr: &mut ast::Expr) -> Result<()> {
}
ast::Expr::Parenthesized(ref mut exprs) => {
for subexpr in exprs.iter_mut() {
rewrite_expr(subexpr)?;
rewrite_expr(subexpr, param_idx)?;
}
let exprs = std::mem::take(exprs);
*expr = ast::Expr::Parenthesized(exprs);
@@ -1920,20 +1933,56 @@ fn rewrite_expr(expr: &mut ast::Expr) -> Result<()> {
}
// Process other expressions recursively
ast::Expr::Binary(lhs, _, rhs) => {
rewrite_expr(lhs)?;
rewrite_expr(rhs)?;
rewrite_expr(lhs, param_idx)?;
rewrite_expr(rhs, param_idx)?;
Ok(())
}
ast::Expr::Like {
lhs, rhs, escape, ..
} => {
rewrite_expr(lhs, param_idx)?;
rewrite_expr(rhs, param_idx)?;
if let Some(escape) = escape {
rewrite_expr(escape, param_idx)?;
}
Ok(())
}
ast::Expr::Case {
base,
when_then_pairs,
else_expr,
} => {
if let Some(base) = base {
rewrite_expr(base, param_idx)?;
}
for (lhs, rhs) in when_then_pairs.iter_mut() {
rewrite_expr(lhs, param_idx)?;
rewrite_expr(rhs, param_idx)?;
}
if let Some(else_expr) = else_expr {
rewrite_expr(else_expr, param_idx)?;
}
Ok(())
}
ast::Expr::InList { lhs, rhs, .. } => {
rewrite_expr(lhs, param_idx)?;
if let Some(rhs) = rhs {
for expr in rhs.iter_mut() {
rewrite_expr(expr, param_idx)?;
}
}
Ok(())
}
ast::Expr::FunctionCall { args, .. } => {
if let Some(args) = args {
for arg in args.iter_mut() {
rewrite_expr(arg)?;
rewrite_expr(arg, param_idx)?;
}
}
Ok(())
}
ast::Expr::Unary(_, arg) => {
rewrite_expr(arg)?;
rewrite_expr(arg, param_idx)?;
Ok(())
}
_ => Ok(()),

View File

@@ -379,7 +379,7 @@ impl SelectPlan {
name: limbo_sqlite3_parser::ast::Id("count".to_string()),
filter_over: None,
};
let result_col_expr = &self.result_columns.get(0).unwrap().expr;
let result_col_expr = &self.result_columns.first().unwrap().expr;
if *result_col_expr != count && *result_col_expr != count_star {
return false;
}

View File

@@ -479,3 +479,283 @@ fn test_insert_parameter_multiple_row() -> anyhow::Result<()> {
assert_eq!(ins.parameters().count(), 8);
Ok(())
}
#[test]
fn test_bind_parameters_update_query() -> anyhow::Result<()> {
let tmp_db = TempDatabase::new_with_rusqlite("create table test (a integer, b text);");
let conn = tmp_db.connect_limbo();
let mut ins = conn.prepare("insert into test (a, b) values (3, 'test1');")?;
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut ins = conn.prepare("update test set a = ? where b = ?;")?;
ins.bind_at(1.try_into()?, OwnedValue::Integer(222));
ins.bind_at(2.try_into()?, OwnedValue::build_text("test1"));
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut sel = conn.prepare("select a, b from test;")?;
loop {
match sel.step()? {
StepResult::Row => {
let row = sel.row().unwrap();
assert_eq!(
row.get::<&OwnedValue>(0).unwrap(),
&OwnedValue::Integer(222)
);
assert_eq!(
row.get::<&OwnedValue>(1).unwrap(),
&OwnedValue::build_text("test1"),
);
}
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
}
}
assert_eq!(ins.parameters().count(), 2);
Ok(())
}
#[test]
fn test_bind_parameters_update_query_multiple_where() -> anyhow::Result<()> {
let tmp_db = TempDatabase::new_with_rusqlite(
"create table test (a integer, b text, c integer, d integer);",
);
let conn = tmp_db.connect_limbo();
let mut ins = conn.prepare("insert into test (a, b, c, d) values (3, 'test1', 4, 5);")?;
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut ins = conn.prepare("update test set a = ? where b = ? and c = 4 and d = ?;")?;
ins.bind_at(1.try_into()?, OwnedValue::Integer(222));
ins.bind_at(2.try_into()?, OwnedValue::build_text("test1"));
ins.bind_at(3.try_into()?, OwnedValue::Integer(5));
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut sel = conn.prepare("select a, b, c, d from test;")?;
loop {
match sel.step()? {
StepResult::Row => {
let row = sel.row().unwrap();
assert_eq!(
row.get::<&OwnedValue>(0).unwrap(),
&OwnedValue::Integer(222)
);
assert_eq!(
row.get::<&OwnedValue>(1).unwrap(),
&OwnedValue::build_text("test1"),
);
assert_eq!(row.get::<&OwnedValue>(2).unwrap(), &OwnedValue::Integer(4));
assert_eq!(row.get::<&OwnedValue>(3).unwrap(), &OwnedValue::Integer(5));
}
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
}
}
assert_eq!(ins.parameters().count(), 3);
Ok(())
}
#[test]
fn test_bind_parameters_update_rowid_alias() -> anyhow::Result<()> {
let tmp_db =
TempDatabase::new_with_rusqlite("CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT);");
let conn = tmp_db.connect_limbo();
let mut ins = conn.prepare("insert into test (id, name) values (1, 'test');")?;
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut sel = conn.prepare("select id, name from test;")?;
loop {
match sel.step()? {
StepResult::Row => {
let row = sel.row().unwrap();
assert_eq!(row.get::<&OwnedValue>(0).unwrap(), &OwnedValue::Integer(1));
assert_eq!(
row.get::<&OwnedValue>(1).unwrap(),
&OwnedValue::build_text("test"),
);
}
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
}
}
let mut ins = conn.prepare("update test set name = ? where id = ?;")?;
ins.bind_at(1.try_into()?, OwnedValue::build_text("updated"));
ins.bind_at(2.try_into()?, OwnedValue::Integer(1));
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut sel = conn.prepare("select id, name from test;")?;
loop {
match sel.step()? {
StepResult::Row => {
let row = sel.row().unwrap();
assert_eq!(row.get::<&OwnedValue>(0).unwrap(), &OwnedValue::Integer(1));
assert_eq!(
row.get::<&OwnedValue>(1).unwrap(),
&OwnedValue::build_text("updated"),
);
}
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
}
}
assert_eq!(ins.parameters().count(), 2);
Ok(())
}
#[test]
fn test_bind_parameters_update_rowid_alias_seek_rowid() -> anyhow::Result<()> {
let tmp_db = TempDatabase::new_with_rusqlite(
"CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, age integer);",
);
let conn = tmp_db.connect_limbo();
conn.execute("insert into test (id, name, age) values (1, 'test', 4);")?;
conn.execute("insert into test (id, name, age) values (2, 'test', 11);")?;
let mut sel = conn.prepare("select id, name, age from test;")?;
let mut i = 0;
loop {
match sel.step()? {
StepResult::Row => {
let row = sel.row().unwrap();
assert_eq!(
row.get::<&OwnedValue>(0).unwrap(),
&OwnedValue::Integer(if i == 0 { 1 } else { 2 })
);
assert_eq!(
row.get::<&OwnedValue>(1).unwrap(),
&OwnedValue::build_text("test"),
);
assert_eq!(
row.get::<&OwnedValue>(2).unwrap(),
&OwnedValue::Integer(if i == 0 { 4 } else { 11 })
);
}
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
}
i += 1;
}
let mut ins = conn.prepare("update test set name = ? where id < ? AND age between ? and ?;")?;
ins.bind_at(1.try_into()?, OwnedValue::build_text("updated"));
ins.bind_at(2.try_into()?, OwnedValue::Integer(2));
ins.bind_at(3.try_into()?, OwnedValue::Integer(3));
ins.bind_at(4.try_into()?, OwnedValue::Integer(5));
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut sel = conn.prepare("select name from test;")?;
let mut i = 0;
loop {
match sel.step()? {
StepResult::Row => {
let row = sel.row().unwrap();
assert_eq!(
row.get::<&OwnedValue>(0).unwrap(),
&OwnedValue::build_text(if i == 0 { "updated" } else { "test" }),
);
}
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
}
i += 1;
}
assert_eq!(ins.parameters().count(), 4);
Ok(())
}
#[test]
fn test_bind_parameters_delete_rowid_alias_seek_out_of_order() -> anyhow::Result<()> {
let tmp_db = TempDatabase::new_with_rusqlite(
"CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT, age integer);",
);
let conn = tmp_db.connect_limbo();
conn.execute("insert into test (id, name, age) values (1, 'correct', 4);")?;
conn.execute("insert into test (id, name, age) values (5, 'test', 11);")?;
let mut ins =
conn.prepare("delete from test where age between ? and ? AND id > ? AND name = ?;")?;
ins.bind_at(1.try_into()?, OwnedValue::Integer(10));
ins.bind_at(2.try_into()?, OwnedValue::Integer(12));
ins.bind_at(3.try_into()?, OwnedValue::Integer(4));
ins.bind_at(4.try_into()?, OwnedValue::build_text("test"));
loop {
match ins.step()? {
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
_ => {}
}
}
let mut sel = conn.prepare("select name from test;")?;
let mut i = 0;
loop {
match sel.step()? {
StepResult::Row => {
let row = sel.row().unwrap();
assert_eq!(
row.get::<&OwnedValue>(0).unwrap(),
&OwnedValue::build_text("correct"),
);
}
StepResult::IO => tmp_db.io.run_once()?,
StepResult::Done | StepResult::Interrupt => break,
StepResult::Busy => panic!("database busy"),
}
i += 1;
}
assert_eq!(i, 1);
assert_eq!(ins.parameters().count(), 4);
Ok(())
}