From f36974f086cf95e56646725738fd67896bb8e6bb Mon Sep 17 00:00:00 2001 From: Glauber Costa Date: Tue, 5 Aug 2025 15:32:02 -0500 Subject: [PATCH] implement the MaxPgCount opcode It is used by the pragma max_page_count, which is also implemented. --- COMPAT.md | 4 +-- core/pragma.rs | 7 ++++ core/storage/pager.rs | 35 +++++++++++++++++++ core/translate/pragma.rs | 28 +++++++++++++++ core/vdbe/execute.rs | 31 ++++++++++++++++ core/vdbe/explain.rs | 9 +++++ core/vdbe/insn.rs | 10 ++++++ testing/pragma.test | 29 +++++++++++++++ vendored/sqlite3-parser/src/parser/ast/mod.rs | 2 ++ 9 files changed, 153 insertions(+), 2 deletions(-) diff --git a/COMPAT.md b/COMPAT.md index 33e166c11..c940cc95d 100644 --- a/COMPAT.md +++ b/COMPAT.md @@ -142,7 +142,7 @@ Turso aims to be fully compatible with SQLite, with opt-in features not supporte | PRAGMA legacy_alter_table | No | | | PRAGMA legacy_file_format | Yes | | | PRAGMA locking_mode | No | | -| PRAGMA max_page_count | No | | +| PRAGMA max_page_count | Yes | | | PRAGMA mmap_size | No | | | PRAGMA module_list | No | | | PRAGMA optimize | No | | @@ -486,7 +486,7 @@ Modifiers: | LoadAnalysis | No | | | Lt | Yes | | | MakeRecord | Yes | | -| MaxPgcnt | No | | +| MaxPgcnt | Yes | | | MemMax | No | | | Move | Yes | | | Multiply | Yes | | diff --git a/core/pragma.rs b/core/pragma.rs index b8d42a49f..bb740972d 100644 --- a/core/pragma.rs +++ b/core/pragma.rs @@ -66,6 +66,13 @@ pub fn pragma_for(pragma: &PragmaName) -> Pragma { PragmaFlags::Result0 | PragmaFlags::SchemaReq | PragmaFlags::NoColumns1, &["page_size"], ), + MaxPageCount => Pragma::new( + PragmaFlags::NeedSchema + | PragmaFlags::Result0 + | PragmaFlags::SchemaReq + | PragmaFlags::NoColumns1, + &["max_page_count"], + ), SchemaVersion => Pragma::new( PragmaFlags::NoColumns1 | PragmaFlags::Result0, &["schema_version"], diff --git a/core/storage/pager.rs b/core/storage/pager.rs index 210af9076..03e3618e0 100644 --- a/core/storage/pager.rs +++ b/core/storage/pager.rs @@ -24,6 +24,9 @@ use super::page_cache::{CacheError, CacheResizeResult, DumbLruPageCache, PageCac use super::sqlite3_ondisk::begin_write_btree_page; use super::wal::CheckpointMode; +/// SQLite's default maximum page count +const DEFAULT_MAX_PAGE_COUNT: u32 = 0xfffffffe; + #[cfg(not(feature = "omit_autovacuum"))] use ptrmap::*; @@ -407,6 +410,8 @@ pub struct Pager { pub(crate) page_size: Cell>, reserved_space: OnceCell, free_page_state: RefCell, + /// Maximum number of pages allowed in the database. Default is 1073741823 (SQLite default). + max_page_count: Cell, #[cfg(not(feature = "omit_autovacuum"))] /// State machine for [Pager::ptrmap_get] ptrmap_get_state: RefCell, @@ -513,11 +518,32 @@ impl Pager { }), free_page_state: RefCell::new(FreePageState::Start), allocate_page_state: RefCell::new(AllocatePageState::Start), + max_page_count: Cell::new(DEFAULT_MAX_PAGE_COUNT), #[cfg(not(feature = "omit_autovacuum"))] ptrmap_get_state: RefCell::new(PtrMapGetState::Start), }) } + /// Get the maximum page count for this database + pub fn get_max_page_count(&self) -> u32 { + self.max_page_count.get() + } + + /// Set the maximum page count for this database + /// Returns the new maximum page count (may be clamped to current database size) + pub fn set_max_page_count(&self, new_max: u32) -> crate::Result> { + // Get current database size + let current_page_count = match self.with_header(|header| header.database_size.get())? { + IOResult::Done(size) => size, + IOResult::IO => return Ok(IOResult::IO), + }; + + // Clamp new_max to be at least the current database size + let clamped_max = std::cmp::max(new_max, current_page_count); + self.max_page_count.set(clamped_max); + Ok(IOResult::Done(clamped_max)) + } + pub fn set_wal(&mut self, wal: Rc>) { self.wal = Some(wal); } @@ -1919,6 +1945,15 @@ impl Pager { } AllocatePageState::AllocateNewPage { current_db_size } => { let new_db_size = *current_db_size + 1; + + // Check if allocating a new page would exceed the maximum page count + let max_page_count = self.get_max_page_count(); + if new_db_size > max_page_count { + return Err(LimboError::DatabaseFull( + "database or disk is full".to_string(), + )); + } + // FIXME: should reserve page cache entry before modifying the database let page = allocate_new_page(new_db_size as usize, &self.buffer_pool, 0); { diff --git a/core/translate/pragma.rs b/core/translate/pragma.rs index 9d60dbdef..5d09ea366 100644 --- a/core/translate/pragma.rs +++ b/core/translate/pragma.rs @@ -145,6 +145,24 @@ fn update_pragma( connection, program, ), + PragmaName::MaxPageCount => { + let data = parse_signed_number(&value)?; + let max_page_count_value = match data { + Value::Integer(i) => i as usize, + Value::Float(f) => f as usize, + _ => unreachable!(), + }; + + let result_reg = program.alloc_register(); + program.emit_insn(Insn::MaxPgcnt { + db: 0, + dest: result_reg, + new_max: max_page_count_value, + }); + program.emit_result_row(result_reg, 1); + program.add_pragma_result_column("max_page_count".into()); + Ok((program, TransactionMode::Write)) + } PragmaName::UserVersion => { let data = parse_signed_number(&value)?; let version_value = match data { @@ -368,6 +386,16 @@ fn query_pragma( program.add_pragma_result_column(pragma.to_string()); Ok((program, TransactionMode::Read)) } + PragmaName::MaxPageCount => { + program.emit_insn(Insn::MaxPgcnt { + db: 0, + dest: register, + new_max: 0, // 0 means just return current max + }); + program.emit_result_row(register, 1); + program.add_pragma_result_column(pragma.to_string()); + Ok((program, TransactionMode::Read)) + } PragmaName::TableInfo => { let table = match value { Some(ast::Expr::Name(name)) => { diff --git a/core/vdbe/execute.rs b/core/vdbe/execute.rs index efed42ed6..4fbc37f37 100644 --- a/core/vdbe/execute.rs +++ b/core/vdbe/execute.rs @@ -8315,6 +8315,37 @@ fn stringify_register(reg: &mut Register) -> bool { } } +pub fn op_max_pgcnt( + program: &Program, + state: &mut ProgramState, + insn: &Insn, + pager: &Rc, + mv_store: Option<&Arc>, +) -> Result { + let Insn::MaxPgcnt { db, dest, new_max } = insn else { + unreachable!("unexpected Insn {:?}", insn) + }; + + if *db > 0 { + todo!("temp/attached databases not implemented yet"); + } + + let result_value = if *new_max == 0 { + // If new_max is 0, just return current maximum without changing it + pager.get_max_page_count() + } else { + // Set new maximum page count (will be clamped to current database size) + match pager.set_max_page_count(*new_max as u32)? { + IOResult::Done(new_max_count) => new_max_count, + IOResult::IO => return Ok(InsnFunctionStepResult::IO), + } + }; + + state.registers[*dest] = Register::Value(Value::Integer(result_value.into())); + state.pc += 1; + Ok(InsnFunctionStepResult::Step) +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/vdbe/explain.rs b/core/vdbe/explain.rs index 06ea94257..959516c5e 100644 --- a/core/vdbe/explain.rs +++ b/core/vdbe/explain.rs @@ -1645,6 +1645,15 @@ pub fn insn_to_str( 0, format!("drop_column({table}, {column_index})"), ), + Insn::MaxPgcnt { db, dest, new_max } => ( + "MaxPgcnt", + *db as i32, + *dest as i32, + *new_max as i32, + Value::build_text(""), + 0, + format!("r[{dest}]=max_page_count(db[{db}],{new_max})"), + ), Insn::CollSeq { reg, collation } => ( "CollSeq", reg.unwrap_or(0) as i32, diff --git a/core/vdbe/insn.rs b/core/vdbe/insn.rs index 0301c342e..8f8e69cfd 100644 --- a/core/vdbe/insn.rs +++ b/core/vdbe/insn.rs @@ -1034,6 +1034,15 @@ pub enum Insn { table: String, column_index: usize, }, + /// Try to set the maximum page count for database P1 to the value in P3. + /// Do not let the maximum page count fall below the current page count and + /// do not change the maximum page count value if P3==0. + /// Store the maximum page count after the change in register P2. + MaxPgcnt { + db: usize, // P1: database index + dest: usize, // P2: output register + new_max: usize, // P3: new maximum page count (0 = just return current) + }, } impl Insn { @@ -1165,6 +1174,7 @@ impl Insn { Insn::IntegrityCk { .. } => execute::op_integrity_check, Insn::RenameTable { .. } => execute::op_rename_table, Insn::DropColumn { .. } => execute::op_drop_column, + Insn::MaxPgcnt { .. } => execute::op_max_pgcnt, } } } diff --git a/testing/pragma.test b/testing/pragma.test index c2cefe784..fb1aa7a31 100755 --- a/testing/pragma.test +++ b/testing/pragma.test @@ -296,3 +296,32 @@ products|id products|name products|price } + +do_execsql_test_on_specific_db ":memory:" pragma-max-page-count-default { + PRAGMA max_page_count +} {4294967294} + +do_execsql_test_on_specific_db ":memory:" pragma-max-page-count-set-large { + PRAGMA max_page_count = 1000; + PRAGMA max_page_count +} {1000 +1000} + +do_execsql_test_on_specific_db ":memory:" pragma-max-page-count-set-zero-ignored { + PRAGMA max_page_count = 0; + PRAGMA max_page_count +} {4294967294 +4294967294 +} + +do_execsql_test_on_specific_db ":memory:" pragma-max-page-count-clamping-with-data { + CREATE TABLE test (id INTEGER); + PRAGMA page_count; + PRAGMA max_page_count = 1; +} {2 +2} + +do_execsql_test_in_memory_any_error pragma-max-page-count-enforcement-error { + PRAGMA max_page_count = 1; + CREATE TABLE test (id INTEGER) +} diff --git a/vendored/sqlite3-parser/src/parser/ast/mod.rs b/vendored/sqlite3-parser/src/parser/ast/mod.rs index 605840f31..e22faabde 100644 --- a/vendored/sqlite3-parser/src/parser/ast/mod.rs +++ b/vendored/sqlite3-parser/src/parser/ast/mod.rs @@ -1810,6 +1810,8 @@ pub enum PragmaName { JournalMode, /// Noop as per SQLite docs LegacyFileFormat, + /// Set or get the maximum number of pages in the database file. + MaxPageCount, /// Return the total number of pages in the database file. PageCount, /// Return the page size of the database in bytes.