Compare commits

...

1 Commits

Author SHA1 Message Date
Psychotoxical 697c3e094e fix(library): let in-memory reads survive the shared cache
`open_in_memory` cannot use WAL, so it shares one cache across its
connections — and shared-cache mode locks at table granularity. A read on a
table the write connection holds fails with SQLITE_LOCKED ("database table
is locked"), which is not a busy condition, so `busy_timeout` never retries
it and the read surfaces as a hard error. A sync-capability test that reads
the phase from one connection while another writes it has failed on Windows
since it landed; CI runs the rust jobs on ubuntu only, so it stayed hidden.

Reading uncommitted rows drops the reader's table lock and gives the
in-memory store the reader/writer concurrency the file-backed WAL path
already has. Test-only — file-backed connections are untouched.
2026-07-27 15:27:11 +02:00
@@ -251,17 +251,22 @@ impl LibraryStore {
.expect("cluster attach write");
let read_conn = Connection::open(&uri).expect("in-memory read connection");
configure_read_connection(&read_conn).expect("read pragmas");
configure_in_memory_read_connection(&read_conn).expect("in-memory read pragmas");
// Shared-cache identity DB: write connection created schema first.
crate::identity::attach_cluster_read_memory(&read_conn, &cluster_uri)
.expect("cluster attach read");
let mainstage_read_conn =
Connection::open(&uri).expect("in-memory mainstage read connection");
configure_read_connection(&mainstage_read_conn).expect("mainstage read pragmas");
configure_in_memory_read_connection(&mainstage_read_conn)
.expect("in-memory mainstage read pragmas");
crate::identity::attach_cluster_read_memory(&mainstage_read_conn, &cluster_uri)
.expect("cluster attach mainstage read");
let scope_detail_read_conn =
Connection::open(&uri).expect("in-memory scope detail read connection");
configure_read_connection(&scope_detail_read_conn).expect("scope detail read pragmas");
configure_in_memory_read_connection(&scope_detail_read_conn)
.expect("in-memory scope detail read pragmas");
crate::identity::attach_cluster_read_memory(&scope_detail_read_conn, &cluster_uri)
.expect("cluster attach scope detail read");
Self {
@@ -950,6 +955,21 @@ fn configure_write_connection(conn: &Connection) -> rusqlite::Result<()> {
Ok(())
}
/// Extra read pragma for the in-memory store only (tests).
///
/// A file-backed database runs in WAL, where a reader and the single writer
/// never block each other. The in-memory store cannot use WAL, so it shares one
/// cache across its connections (`cache=shared`) — and shared-cache mode locks
/// at *table* granularity: a read on a table the write connection is holding
/// fails with `SQLITE_LOCKED` ("database table is locked"). That is not a busy
/// condition, so `busy_timeout` never retries it and the read surfaces as a hard
/// error. Reading uncommitted rows drops the reader's table lock and restores
/// the concurrency the production WAL path has. Test-only: it never touches a
/// file-backed connection.
fn configure_in_memory_read_connection(conn: &Connection) -> rusqlite::Result<()> {
conn.pragma_update(None, "read_uncommitted", true)
}
fn configure_read_connection(conn: &Connection) -> rusqlite::Result<()> {
register_sql_functions(conn)?;
conn.busy_timeout(Duration::from_secs(5))?;