Skip to content

Commit f1dfc76

Browse files
fix(storage): bootstrap incremental vacuum before WAL
1 parent 45a8b4b commit f1dfc76

2 files changed

Lines changed: 40 additions & 5 deletions

File tree

crates/tracedecay-rusqlite-runtime/src/connection/mod.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,8 @@ impl std::error::Error for ConnectionPolicyError {
332332
}
333333

334334
pub(crate) fn open(path: &Path, mode: ConnectionMode) -> Result<Connection, ConnectionPolicyError> {
335+
let fresh_writer = mode == ConnectionMode::Writer
336+
&& std::fs::metadata(path).is_ok_and(|metadata| metadata.len() == 0);
335337
let flags = match mode {
336338
ConnectionMode::Reader => OpenFlags::SQLITE_OPEN_READ_ONLY,
337339
ConnectionMode::Writer | ConnectionMode::Maintenance => OpenFlags::SQLITE_OPEN_READ_WRITE,
@@ -340,7 +342,7 @@ pub(crate) fn open(path: &Path, mode: ConnectionMode) -> Result<Connection, Conn
340342
let connection =
341343
Connection::open_with_flags(path, flags).map_err(|source| policy("open", source))?;
342344

343-
apply_pragmas(&connection, mode)?;
345+
apply_pragmas(&connection, mode, fresh_writer)?;
344346
assert_compile_options(&connection)?;
345347
apply_limits(&connection, mode)?;
346348
install_authorizer(&connection, mode)?;
@@ -362,7 +364,7 @@ pub fn open_immutable_reader(path: &Path) -> Result<Connection, ConnectionPolicy
362364
| OpenFlags::SQLITE_OPEN_PRIVATE_CACHE;
363365
let connection =
364366
Connection::open_with_flags(uri, flags).map_err(|source| policy("open", source))?;
365-
apply_pragmas(&connection, ConnectionMode::Reader)?;
367+
apply_pragmas(&connection, ConnectionMode::Reader, false)?;
366368
assert_compile_options(&connection)?;
367369
apply_limits(&connection, ConnectionMode::Reader)?;
368370
install_authorizer(&connection, ConnectionMode::Reader)?;
@@ -396,6 +398,7 @@ fn immutable_health_uri(path: &Path) -> Result<String, ConnectionPolicyError> {
396398
fn apply_pragmas(
397399
connection: &Connection,
398400
mode: ConnectionMode,
401+
fresh_writer: bool,
399402
) -> Result<(), ConnectionPolicyError> {
400403
// SQLite must never wait past the runtime's own queue/deadline authority.
401404
connection
@@ -409,6 +412,12 @@ fn apply_pragmas(
409412
.map_err(|source| policy("trusted schema", source))?;
410413

411414
if mode == ConnectionMode::Writer {
415+
if fresh_writer {
416+
connection
417+
.pragma_update(None, "auto_vacuum", "INCREMENTAL")
418+
.map_err(|source| policy("fresh auto-vacuum", source))?;
419+
verify_pragma_i64(connection, "auto_vacuum", 2)?;
420+
}
412421
connection
413422
.pragma_update(None, "journal_mode", "WAL")
414423
.map_err(|source| policy("WAL journal", source))?;
@@ -578,6 +587,13 @@ fn is_read_only_introspection_pragma(pragma_name: &str) -> bool {
578587
.any(|candidate| pragma_name.eq_ignore_ascii_case(candidate))
579588
}
580589

590+
fn is_safe_writer_pragma(pragma_name: &str, pragma_value: &str) -> bool {
591+
pragma_name.eq_ignore_ascii_case("wal_autocheckpoint")
592+
|| pragma_name.eq_ignore_ascii_case("wal_checkpoint")
593+
|| (pragma_name.eq_ignore_ascii_case("auto_vacuum")
594+
&& (pragma_value.eq_ignore_ascii_case("incremental") || pragma_value == "2"))
595+
}
596+
581597
fn authorize(mode: ConnectionMode, context: AuthContext<'_>) -> Authorization {
582598
if mode == ConnectionMode::Maintenance {
583599
return Authorization::Allow;
@@ -613,12 +629,11 @@ fn authorize(mode: ConnectionMode, context: AuthContext<'_>) -> Authorization {
613629
context.action,
614630
AuthAction::Pragma {
615631
pragma_name,
616-
pragma_value: Some(_),
632+
pragma_value: Some(pragma_value),
617633
}
618634
if !is_read_only_introspection_pragma(pragma_name)
619635
&& (mode != ConnectionMode::Writer
620-
|| (!pragma_name.eq_ignore_ascii_case("wal_autocheckpoint")
621-
&& !pragma_name.eq_ignore_ascii_case("wal_checkpoint")))
636+
|| !is_safe_writer_pragma(pragma_name, pragma_value))
622637
)
623638
|| (mode == ConnectionMode::Reader
624639
&& matches!(

crates/tracedecay-rusqlite-runtime/src/connection/tests.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ fn limits_and_authorizer_reject_oversized_or_unsafe_sql() {
144144
assert!(connection.prepare(&oversized).is_err());
145145
}
146146

147+
#[test]
148+
fn writer_bootstraps_fresh_incremental_auto_vacuum_before_wal() {
149+
let directory = tempfile::tempdir().expect("temporary directory");
150+
let path = directory.path().join("fresh.sqlite3");
151+
std::fs::File::create(&path).expect("create empty database file");
152+
let connection = open(&path, ConnectionMode::Writer).expect("writer policy");
153+
154+
assert_eq!(
155+
connection
156+
.query_row("PRAGMA auto_vacuum", [], |row| row.get::<_, i64>(0))
157+
.unwrap(),
158+
2
159+
);
160+
assert!(
161+
connection
162+
.execute_batch("PRAGMA auto_vacuum = NONE")
163+
.is_err()
164+
);
165+
}
166+
147167
#[test]
148168
fn progress_cancellation_interrupts_and_is_removed_after_scope() {
149169
let file = database();

0 commit comments

Comments
 (0)