Merge 'Fix: ensure PRAGMA cache_size changes persist only for current session' from meteorgan

According to [the
document](https://www.sqlite.org/pragma.html#pragma_cache_size):
```
When you change the cache size using the cache_size pragma, the change only endures for the current session. The cache size reverts to the default value when the database is closed and reopened.
```
so, we shouldn't persist cache_size to database header.
this PR also addresses two minor issues:
1. Sets the default cache_size to -2000 to align with SQLite's default
2. Uses the actual page size to calculate the cache size.

Reviewed-by: Pere Diaz Bou <pere-altea@homail.com>

Closes #1672
This commit is contained in:
Pere Diaz Bou
2025-06-06 16:21:02 +02:00
5 changed files with 73 additions and 31 deletions

View File

@@ -240,6 +240,7 @@ impl Database {
syms: RefCell::new(SymbolTable::new()),
total_changes: Cell::new(0),
_shared_cache: false,
cache_size: Cell::new(self.header.lock().default_page_cache_size),
});
if let Err(e) = conn.register_builtins() {
return Err(LimboError::ExtensionError(e));
@@ -339,6 +340,7 @@ pub struct Connection {
total_changes: Cell<i64>,
syms: RefCell<SymbolTable>,
_shared_cache: bool,
cache_size: Cell<i32>,
}
impl Connection {
@@ -595,6 +597,13 @@ impl Connection {
self.total_changes.get()
}
pub fn get_cache_size(&self) -> i32 {
self.cache_size.get()
}
pub fn set_cache_size(&self, size: i32) {
self.cache_size.set(size);
}
#[cfg(feature = "fs")]
pub fn open_new(&self, path: &str, vfs: &str) -> Result<(Arc<dyn IO>, Arc<Database>)> {
Database::open_with_vfs(&self._db, path, vfs)

View File

@@ -251,7 +251,7 @@ impl Default for DatabaseHeader {
freelist_pages: 0,
schema_cookie: 0,
schema_format: 4, // latest format, new sqlite3 databases use this format
default_page_cache_size: 500, // pages
default_page_cache_size: DEFAULT_CACHE_SIZE,
vacuum_mode_largest_root_page: 0,
text_encoding: 1, // utf-8
user_version: 0,

View File

@@ -88,6 +88,7 @@ pub fn translate(
body.map(|b| *b),
database_header.clone(),
pager,
connection.clone(),
program,
)?,
stmt => translate_inner(schema, stmt, syms, query_mode, program)?,

View File

@@ -3,7 +3,7 @@
use limbo_sqlite3_parser::ast;
use limbo_sqlite3_parser::ast::PragmaName;
use std::rc::Rc;
use std::rc::{Rc, Weak};
use std::sync::Arc;
use crate::fast_lock::SpinLock;
@@ -33,6 +33,7 @@ pub fn translate_pragma(
body: Option<ast::PragmaBody>,
database_header: Arc<SpinLock<DatabaseHeader>>,
pager: Rc<Pager>,
connection: Weak<crate::Connection>,
mut program: ProgramBuilder,
) -> crate::Result<ProgramBuilder> {
let opts = ProgramBuilderOpts {
@@ -56,7 +57,14 @@ pub fn translate_pragma(
match body {
None => {
query_pragma(pragma, schema, None, database_header.clone(), &mut program)?;
query_pragma(
pragma,
schema,
None,
database_header.clone(),
connection,
&mut program,
)?;
}
Some(ast::PragmaBody::Equals(value)) => match pragma {
PragmaName::TableInfo => {
@@ -65,6 +73,7 @@ pub fn translate_pragma(
schema,
Some(value),
database_header.clone(),
connection,
&mut program,
)?;
}
@@ -76,6 +85,7 @@ pub fn translate_pragma(
value,
database_header.clone(),
pager,
connection,
&mut program,
)?;
}
@@ -87,6 +97,7 @@ pub fn translate_pragma(
schema,
Some(value),
database_header.clone(),
connection,
&mut program,
)?;
}
@@ -109,36 +120,51 @@ fn update_pragma(
value: ast::Expr,
header: Arc<SpinLock<DatabaseHeader>>,
pager: Rc<Pager>,
connection: Weak<crate::Connection>,
program: &mut ProgramBuilder,
) -> crate::Result<()> {
match pragma {
PragmaName::CacheSize => {
let cache_size = match value {
ast::Expr::Literal(ast::Literal::Numeric(numeric_value)) => {
numeric_value.parse::<i64>()?
}
ast::Expr::Unary(ast::UnaryOperator::Negative, expr) => match *expr {
ast::Expr::Literal(ast::Literal::Numeric(numeric_value)) => {
-numeric_value.parse::<i64>()?
}
_ => bail_parse_error!("Not a valid value"),
},
_ => bail_parse_error!("Not a valid value"),
let cache_size = match parse_signed_number(&value)? {
Value::Integer(size) => size,
Value::Float(size) => size as i64,
_ => bail_parse_error!("Invalid value for cache size pragma"),
};
update_cache_size(cache_size, header, pager)?;
update_cache_size(cache_size, header, pager, connection)?;
Ok(())
}
PragmaName::JournalMode => {
query_pragma(PragmaName::JournalMode, schema, None, header, program)?;
query_pragma(
PragmaName::JournalMode,
schema,
None,
header,
connection,
program,
)?;
Ok(())
}
PragmaName::LegacyFileFormat => Ok(()),
PragmaName::WalCheckpoint => {
query_pragma(PragmaName::WalCheckpoint, schema, None, header, program)?;
query_pragma(
PragmaName::WalCheckpoint,
schema,
None,
header,
connection,
program,
)?;
Ok(())
}
PragmaName::PageCount => {
query_pragma(PragmaName::PageCount, schema, None, header, program)?;
query_pragma(
PragmaName::PageCount,
schema,
None,
header,
connection,
program,
)?;
Ok(())
}
PragmaName::UserVersion => {
@@ -178,13 +204,14 @@ fn query_pragma(
schema: &Schema,
value: Option<ast::Expr>,
database_header: Arc<SpinLock<DatabaseHeader>>,
connection: Weak<crate::Connection>,
program: &mut ProgramBuilder,
) -> crate::Result<()> {
let register = program.alloc_register();
match pragma {
PragmaName::CacheSize => {
program.emit_int(
database_header.lock().default_page_cache_size.into(),
connection.upgrade().unwrap().get_cache_size() as i64,
register,
);
program.emit_result_row(register, 1);
@@ -287,30 +314,25 @@ fn update_cache_size(
value: i64,
header: Arc<SpinLock<DatabaseHeader>>,
pager: Rc<Pager>,
connection: Weak<crate::Connection>,
) -> crate::Result<()> {
let mut cache_size_unformatted: i64 = value;
let mut cache_size = if cache_size_unformatted < 0 {
let kb = cache_size_unformatted.abs() * 1024;
kb / 512 // assume 512 page size for now
let page_size = header.lock().get_page_size();
kb / page_size as i64
} else {
value
} as usize;
if cache_size < MIN_PAGE_CACHE_SIZE {
// update both in memory and stored disk value
cache_size = MIN_PAGE_CACHE_SIZE;
cache_size_unformatted = MIN_PAGE_CACHE_SIZE as i64;
}
let mut header_guard = header.lock();
// update in-memory header
header_guard.default_page_cache_size = cache_size_unformatted
.try_into()
.unwrap_or_else(|_| panic!("invalid value, too big for a i32 {}", value));
// update in disk
pager.write_database_header(&header_guard)?;
connection
.upgrade()
.unwrap()
.set_cache_size(cache_size_unformatted as i32);
// update cache size
pager

View File

@@ -3,6 +3,16 @@
set testdir [file dirname $argv0]
source $testdir/tester.tcl
do_execsql_test_on_specific_db ":memory:" pragma-cache-size-default {
PRAGMA cache_size
} {-2000}
do_execsql_test pragma-set-cache-size {
PRAGMA cache_size = 100;
PRAGMA cache_size
} {100}
# Even though the cache size was set to 100 in previous test, a new connection defaults back to -2000.
do_execsql_test pragma-cache-size {
PRAGMA cache_size
} {-2000}