Merge 'Use the SetCookie opcode to implement user_version pragma' from meteorgan

1. implement the `SetCookie` opcode for `user_version`
2. reimplement `PRAGMA user_version=N` functionality using the
`SetCookie` opcode
**sqlite**
```
sqlite> explain pragma user_version=10;
addr  opcode         p1    p2    p3    p4             p5  comment
----  -------------  ----  ----  ----  -------------  --  -------------
0     Init           0     5     0                    0   Start at 5
1     Expire         1     1     0                    0
2     Transaction    0     1     0                    0
3     SetCookie      0     6     10                   1
4     Halt           0     0     0                    0
5     Goto           0     1     0                    0
```
**limbo**
```
limbo>  explain pragma user_version=10;
addr  opcode             p1    p2    p3    p4             p5  comment
----  -----------------  ----  ----  ----  -------------  --  -------
0     Init               0     3     0                    0   Start at 3
1     SetCookie          0     6     10                   1
2     Halt               0     0     0                    0
3     Transaction        0     1     0                    0   write=true
4     Goto               0     1     0                    0
```
To fully align the opcodes with SQLite, we still need to implement the
`Expire` opcode, which I plan to address in the next PR.

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

Closes #1590
This commit is contained in:
Jussi Saurio
2025-05-27 20:19:13 +03:00
6 changed files with 101 additions and 22 deletions

View File

@@ -10,10 +10,10 @@ use crate::fast_lock::SpinLock;
use crate::schema::Schema;
use crate::storage::sqlite3_ondisk::{DatabaseHeader, MIN_PAGE_CACHE_SIZE};
use crate::storage::wal::CheckpointMode;
use crate::util::normalize_ident;
use crate::util::{normalize_ident, parse_signed_number};
use crate::vdbe::builder::{ProgramBuilder, ProgramBuilderOpts, QueryMode};
use crate::vdbe::insn::{Cookie, Insn};
use crate::{bail_parse_error, Pager};
use crate::{bail_parse_error, Pager, Value};
use std::str::FromStr;
use strum::IntoEnumIterator;
@@ -142,27 +142,19 @@ fn update_pragma(
Ok(())
}
PragmaName::UserVersion => {
let version_value = match value {
ast::Expr::Literal(ast::Literal::Numeric(numeric_value)) => {
numeric_value.parse::<i32>()?
}
ast::Expr::Unary(ast::UnaryOperator::Negative, expr) => match *expr {
ast::Expr::Literal(ast::Literal::Numeric(numeric_value)) => {
-numeric_value.parse::<i32>()?
}
_ => bail_parse_error!("Not a valid value"),
},
_ => bail_parse_error!("Not a valid value"),
let data = parse_signed_number(&value)?;
let version_value = match data {
Value::Integer(i) => i as i32,
Value::Float(f) => f as i32,
_ => unreachable!(),
};
let mut header_guard = header.lock();
// update in-memory
header_guard.user_version = version_value;
// update in disk
pager.write_database_header(&header_guard);
program.emit_insn(Insn::SetCookie {
db: 0,
cookie: Cookie::UserVersion,
value: version_value,
p5: 1,
});
Ok(())
}
PragmaName::SchemaVersion => {

View File

@@ -4,7 +4,9 @@ use crate::{
types::{Value, ValueType},
LimboError, OpenFlags, Result, Statement, StepResult, SymbolTable, IO,
};
use limbo_sqlite3_parser::ast::{self, CreateTableBody, Expr, FunctionTail, Literal};
use limbo_sqlite3_parser::ast::{
self, CreateTableBody, Expr, FunctionTail, Literal, UnaryOperator,
};
use std::{rc::Rc, sync::Arc};
pub trait RoundToPrecision {
@@ -1011,6 +1013,27 @@ pub fn parse_numeric_literal(text: &str) -> Result<Value> {
Ok(Value::Float(float_value))
}
pub fn parse_signed_number(expr: &Expr) -> Result<Value> {
match expr {
Expr::Literal(Literal::Numeric(num)) => parse_numeric_literal(num),
Expr::Unary(op, expr) => match (op, expr.as_ref()) {
(UnaryOperator::Negative, Expr::Literal(Literal::Numeric(num))) => {
let data = "-".to_owned() + &num.to_string();
parse_numeric_literal(&data)
}
(UnaryOperator::Positive, Expr::Literal(Literal::Numeric(num))) => {
parse_numeric_literal(num)
}
_ => Err(LimboError::InvalidArgument(
"signed-number must follow the format: ([+|-] numeric-literal)".to_string(),
)),
},
_ => Err(LimboError::InvalidArgument(
"signed-number must follow the format: ([+|-] numeric-literal)".to_string(),
)),
}
}
// for TVF's we need these at planning time so we cannot emit translate_expr
pub fn vtable_args(args: &[ast::Expr]) -> Vec<limbo_ext::Value> {
let mut vtable_args = Vec::new();

View File

@@ -4520,6 +4520,37 @@ pub fn op_read_cookie(
Ok(InsnFunctionStepResult::Step)
}
pub fn op_set_cookie(
program: &Program,
state: &mut ProgramState,
insn: &Insn,
pager: &Rc<Pager>,
mv_store: Option<&Rc<MvStore>>,
) -> Result<InsnFunctionStepResult> {
let Insn::SetCookie {
db,
cookie,
value,
p5,
} = insn
else {
unreachable!("unexpected Insn {:?}", insn)
};
if *db > 0 {
todo!("temp databases not implemented yet");
}
match cookie {
Cookie::UserVersion => {
let mut header_guard = pager.db_header.lock();
header_guard.user_version = *value;
pager.write_database_header(&*header_guard);
}
cookie => todo!("{cookie:?} is not yet implement for SetCookie"),
}
state.pc += 1;
Ok(InsnFunctionStepResult::Step)
}
pub fn op_shift_right(
program: &Program,
state: &mut ProgramState,

View File

@@ -1373,6 +1373,20 @@ pub fn insn_to_str(
0,
"".to_string(),
),
Insn::SetCookie {
db,
cookie,
value,
p5,
} => (
"SetCookie",
*db as i32,
*cookie as i32,
*value,
Value::build_text(""),
*p5,
"".to_string(),
),
Insn::AutoCommit {
auto_commit,
rollback,

View File

@@ -844,6 +844,14 @@ pub enum Insn {
dest: usize,
cookie: Cookie,
},
/// Write the value in register P3 into cookie number P2 of database P1.
/// If P2 is the SCHEMA_VERSION cookie (cookie number 1) then the internal schema version is set to P3-P5
SetCookie {
db: usize,
cookie: Cookie,
value: i32,
p5: u16,
},
/// Open a new cursor P1 to a transient table.
OpenEphemeral {
cursor_id: usize,
@@ -1010,6 +1018,7 @@ impl Insn {
Insn::Noop => execute::op_noop,
Insn::PageCount { .. } => execute::op_page_count,
Insn::ReadCookie { .. } => execute::op_read_cookie,
Insn::SetCookie { .. } => execute::op_set_cookie,
Insn::OpenEphemeral { .. } | Insn::OpenAutoindex { .. } => execute::op_open_ephemeral,
Insn::Once { .. } => execute::op_once,
Insn::Found { .. } | Insn::NotFound { .. } => execute::op_found,

View File

@@ -55,3 +55,13 @@ do_execsql_test_on_specific_db ":memory:" pragma-user-version-update {
PRAGMA user_version = 42;
PRAGMA user_version;
} {42}
do_execsql_test_on_specific_db ":memory:" pragma-user-version-negative-value {
PRAGMA user_version = -10;
PRAGMA user_version;
} {-10}
do_execsql_test_on_specific_db ":memory:" pragma-user-version-float-value {
PRAGMA user_version = 10.9;
PRAGMA user_version;
} {10}