From 169b574c9ece874440c36058eda6fc1bac25bd37 Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 30 Sep 2026 12:40:15 -0700 Subject: [PATCH 1/2] fix: migrate SQLite before expanding the connection pool --- crates/adapters/gfa-store/src/lib.rs | 29 ++++++++++++++++++-------- crates/adapters/gfa-store/src/tests.rs | 20 ++++++++++++++++++ 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/crates/adapters/gfa-store/src/lib.rs b/crates/adapters/gfa-store/src/lib.rs index acbeed3..3ccfe6e 100644 --- a/crates/adapters/gfa-store/src/lib.rs +++ b/crates/adapters/gfa-store/src/lib.rs @@ -4,7 +4,7 @@ use gfa_service::{ }; use sqlx::{ sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions, SqliteSynchronous}, - Sqlite, SqlitePool, Transaction, + Connection, Sqlite, SqliteConnection, SqlitePool, Transaction, }; use std::{path::Path, time::Duration}; @@ -33,22 +33,33 @@ impl SqliteMatchStore { } async fn connect(options: SqliteConnectOptions, connections: u32) -> Result { + let options = options.foreign_keys(true).busy_timeout(Duration::from_secs(5)); + if connections > 1 { + // Configure WAL and migrate before the pool can open other connections. + // A checkout during migrations otherwise lets pool maintenance race + // PRAGMA journal_mode against SQLite's schema write lock. + let mut connection = SqliteConnection::connect_with(&options) + .await + .map_err(unavailable)?; + let migrated = MIGRATOR.run(&mut connection).await; + connection.close().await.map_err(unavailable)?; + migrated.map_err(unavailable)?; + } let pool = SqlitePoolOptions::new() .max_connections(connections) .min_connections(1) .idle_timeout(None) .max_lifetime(None) .test_before_acquire(false) - .connect_with( - options - .foreign_keys(true) - .busy_timeout(Duration::from_secs(5)), - ) + .connect_with(options) .await .map_err(unavailable)?; - if let Err(error) = MIGRATOR.run(&pool).await { - pool.close().await; - return Err(unavailable(error)); + // In-memory databases must keep their sole connection alive. + if connections == 1 { + if let Err(error) = MIGRATOR.run(&pool).await { + pool.close().await; + return Err(unavailable(error)); + } } Ok(Self { pool }) } diff --git a/crates/adapters/gfa-store/src/tests.rs b/crates/adapters/gfa-store/src/tests.rs index 2d5df9b..196848e 100644 --- a/crates/adapters/gfa-store/src/tests.rs +++ b/crates/adapters/gfa-store/src/tests.rs @@ -285,3 +285,23 @@ async fn corrupt_event_sequences_fail_instead_of_returning_partial_history() -> store.close().await; Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn fresh_database_initialization_precedes_pool_expansion() -> TestResult { + let directory = tempfile::tempdir()?; + for iteration in 0..32 { + let path = directory.path().join(format!("fresh-{iteration}.sqlite")); + let store = SqliteMatchStore::open(&path).await?; + store.create(new_record("startup")).await?; + assert_eq!(store.load("startup").await?, Some(new_record("startup"))); + store.close().await; + let restored = SqliteMatchStore::open(&path).await?; + assert_eq!(restored.load("startup").await?, Some(new_record("startup"))); + restored.close().await; + } + let memory = SqliteMatchStore::in_memory().await?; + memory.create(new_record("memory")).await?; + assert_eq!(memory.load("memory").await?, Some(new_record("memory"))); + memory.close().await; + Ok(()) +} From e242df1678a5c6a8a2d49aa8dc4af0dc85c5e136 Mon Sep 17 00:00:00 2001 From: Mason Date: Wed, 30 Sep 2026 12:42:34 -0700 Subject: [PATCH 2/2] style: format SQLite bootstrap initialization --- crates/adapters/gfa-store/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/adapters/gfa-store/src/lib.rs b/crates/adapters/gfa-store/src/lib.rs index 3ccfe6e..37195d3 100644 --- a/crates/adapters/gfa-store/src/lib.rs +++ b/crates/adapters/gfa-store/src/lib.rs @@ -33,7 +33,9 @@ impl SqliteMatchStore { } async fn connect(options: SqliteConnectOptions, connections: u32) -> Result { - let options = options.foreign_keys(true).busy_timeout(Duration::from_secs(5)); + let options = options + .foreign_keys(true) + .busy_timeout(Duration::from_secs(5)); if connections > 1 { // Configure WAL and migrate before the pool can open other connections. // A checkout during migrations otherwise lets pool maintenance race