diff --git a/COMPAT.md b/COMPAT.md index 9bb087639..1da0e2faa 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -103,7 +103,7 @@ This document describes the SQLite compatibility status of Limbo: | sqlite_compileoption_used(X) | No | | | sqlite_offset(X) | No | | | sqlite_source_id() | No | | -| sqlite_version() | No | | +| sqlite_version() | Yes | | | substr(X,Y,Z) | Yes | | | substr(X,Y) | Yes | | | substring(X,Y,Z) | Yes | | diff --git a/core/function.rs b/core/function.rs index 91b56e455..cad6e4fe9 100644 --- a/core/function.rs +++ b/core/function.rs @@ -73,6 +73,7 @@ pub enum ScalarFunc { Time, Unicode, Quote, + SqliteVersion, UnixEpoch, } @@ -105,6 +106,7 @@ impl Display for ScalarFunc { ScalarFunc::Time => "time".to_string(), ScalarFunc::Unicode => "unicode".to_string(), ScalarFunc::Quote => "quote".to_string(), + ScalarFunc::SqliteVersion => "sqlite_version".to_string(), ScalarFunc::UnixEpoch => "unixepoch".to_string(), }; write!(f, "{}", str) @@ -171,6 +173,7 @@ impl Func { "time" => Ok(Func::Scalar(ScalarFunc::Time)), "unicode" => Ok(Func::Scalar(ScalarFunc::Unicode)), "quote" => Ok(Func::Scalar(ScalarFunc::Quote)), + "sqlite_version" => Ok(Func::Scalar(ScalarFunc::SqliteVersion)), "json" => Ok(Func::Json(JsonFunc::Json)), "unixepoch" => Ok(Func::Scalar(ScalarFunc::UnixEpoch)), _ => Err(()), diff --git a/core/lib.rs b/core/lib.rs index d3c28eb35..2b35f07a6 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -19,7 +19,7 @@ use log::trace; use schema::Schema; use sqlite3_parser::ast; use sqlite3_parser::{ast::Cmd, lexer::sql::Parser}; -use std::sync::Arc; +use std::sync::{Arc, OnceLock}; use std::{cell::RefCell, rc::Rc}; #[cfg(feature = "fs")] use storage::database::FileStorage; @@ -42,6 +42,8 @@ pub use storage::pager::Page; pub use storage::wal::Wal; pub use types::Value; +pub static DATABASE_VERSION: OnceLock = OnceLock::new(); + pub struct Database { pager: Rc, schema: Rc, @@ -59,11 +61,15 @@ impl Database { } pub fn open( - io: Arc, + io: Arc, page_io: Rc, wal: Rc, ) -> Result { let db_header = Pager::begin_open(page_io.clone())?; + DATABASE_VERSION.get_or_init(|| { + let version = db_header.borrow().version_number; + version.to_string() + }); io.run_once()?; let pager = Rc::new(Pager::finish_open( db_header.clone(), diff --git a/core/storage/sqlite3_ondisk.rs b/core/storage/sqlite3_ondisk.rs index 58d204f46..d71cd615a 100644 --- a/core/storage/sqlite3_ondisk.rs +++ b/core/storage/sqlite3_ondisk.rs @@ -84,7 +84,7 @@ pub struct DatabaseHeader { application_id: u32, reserved: [u8; 20], version_valid_for: u32, - version_number: u32, + pub version_number: u32, } #[derive(Debug, Default)] diff --git a/core/translate/expr.rs b/core/translate/expr.rs index 85c9222c0..b06f972e8 100644 --- a/core/translate/expr.rs +++ b/core/translate/expr.rs @@ -1320,6 +1320,26 @@ pub fn translate_expr( Ok(target_register) } + ScalarFunc::SqliteVersion => { + if args.is_some() { + crate::bail_parse_error!("sqlite_version function with arguments"); + } + + let output_register = program.alloc_register(); + program.emit_insn(Insn::Function { + constant_mask: 0, + start_reg: output_register, + dest: output_register, + func: func_ctx, + }); + + program.emit_insn(Insn::Copy { + src_reg: output_register, + dst_reg: target_register, + amount: 1, + }); + Ok(target_register) + } } } } diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 43c49903a..56e4f87bc 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -631,12 +631,18 @@ pub fn insn_to_str( *dest as i32, OwnedValue::Text(Rc::new(func.func.to_string())), 0, - format!( - "r[{}]=func(r[{}..{}])", - dest, - start_reg, - start_reg + func.arg_count - 1 - ), + if func.arg_count == 0 { + format!("r[{}]=func()", dest) + } else if *start_reg == *start_reg + func.arg_count - 1 { + format!("r[{}]=func(r[{}])", dest, start_reg) + } else { + format!( + "r[{}]=func(r[{}..{}])", + dest, + start_reg, + start_reg + func.arg_count - 1 + ) + }, ), Insn::InitCoroutine { yield_reg, diff --git a/core/vdbe/mod.rs b/core/vdbe/mod.rs index d43805069..e39c52b92 100644 --- a/core/vdbe/mod.rs +++ b/core/vdbe/mod.rs @@ -31,7 +31,7 @@ use crate::schema::Table; use crate::storage::sqlite3_ondisk::DatabaseHeader; use crate::storage::{btree::BTreeCursor, pager::Pager}; use crate::types::{AggContext, Cursor, CursorResult, OwnedRecord, OwnedValue, Record}; -use crate::Result; +use crate::{Result, DATABASE_VERSION}; use datetime::{exec_date, exec_time, exec_unixepoch}; @@ -1608,6 +1608,12 @@ impl Program { } } } + ScalarFunc::SqliteVersion => { + let version_integer: i64 = + DATABASE_VERSION.get().unwrap().parse()?; + let version = execute_sqlite_version(version_integer); + state.registers[*dest] = OwnedValue::Text(Rc::new(version)); + } }, crate::function::Func::Agg(_) => { unreachable!("Aggregate functions should not be handled here") @@ -2212,13 +2218,21 @@ fn exec_if(reg: &OwnedValue, null_reg: &OwnedValue, not: bool) -> bool { } } +fn execute_sqlite_version(version_integer: i64) -> String { + let major = version_integer / 1_000_000; + let minor = (version_integer % 1_000_000) / 1_000; + let release = version_integer % 1_000; + + format!("{}.{}.{}", major, minor, release) +} + #[cfg(test)] mod tests { use super::{ exec_abs, exec_char, exec_if, exec_length, exec_like, exec_lower, exec_ltrim, exec_minmax, exec_nullif, exec_quote, exec_random, exec_round, exec_rtrim, exec_sign, exec_substring, - exec_trim, exec_unicode, exec_upper, get_new_rowid, Cursor, CursorResult, LimboError, - OwnedRecord, OwnedValue, Result, + exec_trim, exec_unicode, exec_upper, execute_sqlite_version, get_new_rowid, Cursor, + CursorResult, LimboError, OwnedRecord, OwnedValue, Result, }; use mockall::{mock, predicate}; use rand::{rngs::mock::StepRng, thread_rng}; @@ -2813,4 +2827,11 @@ mod tests { let expected = Some(OwnedValue::Null); assert_eq!(exec_sign(&input), expected); } + + #[test] + fn test_execute_sqlite_version() { + let version_integer = 3046001; + let expected = "3.46.1"; + assert_eq!(execute_sqlite_version(version_integer), expected); + } }