implement the MaxPgCount opcode

It is used by the pragma max_page_count, which is also implemented.
This commit is contained in:
Glauber Costa
2025-08-05 15:32:02 -05:00
parent cc98f9f88b
commit f36974f086
9 changed files with 153 additions and 2 deletions

View File

@@ -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 | |

View File

@@ -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"],

View File

@@ -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<Option<u32>>,
reserved_space: OnceCell<u8>,
free_page_state: RefCell<FreePageState>,
/// Maximum number of pages allowed in the database. Default is 1073741823 (SQLite default).
max_page_count: Cell<u32>,
#[cfg(not(feature = "omit_autovacuum"))]
/// State machine for [Pager::ptrmap_get]
ptrmap_get_state: RefCell<PtrMapGetState>,
@@ -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<IOResult<u32>> {
// 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<RefCell<dyn Wal>>) {
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);
{

View File

@@ -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)) => {

View File

@@ -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<Pager>,
mv_store: Option<&Arc<MvStore>>,
) -> Result<InsnFunctionStepResult> {
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::*;

View File

@@ -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,

View File

@@ -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,
}
}
}

View File

@@ -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)
}

View File

@@ -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.