diff --git a/packages/storage/src/__tests__/operational-state-store.test.ts b/packages/storage/src/__tests__/operational-state-store.test.ts index 4c46073f295..9162405f83d 100644 --- a/packages/storage/src/__tests__/operational-state-store.test.ts +++ b/packages/storage/src/__tests__/operational-state-store.test.ts @@ -89,6 +89,23 @@ test('new runtime databases use incremental vacuum before operational schema cre } }); +test('an operational opener configures the empty file left by another connection', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-operational-empty-open-')); + const first = new DatabaseSync(join(root, 'runtime.sqlite')); + try { + const lease = acquireOperationalStateDatabase(root); + try { + assert.equal(lease.database.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); + assert.equal(first.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); + } finally { + lease.close(); + } + } finally { + first.close(); + await rm(root, { recursive: true, force: true }); + } +}); + test('workflow 12 upgrade adds wait storage atomically and preserves every released row and schema scope', async () => { const root = await mkdtemp(join(tmpdir(), 'maka-operational-workflow-12-')); const databasePath = join(root, 'runtime.sqlite'); diff --git a/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts b/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts index 35de68bff55..3af4c65b69c 100644 --- a/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts +++ b/packages/storage/src/__tests__/sqlite-recovery-concurrency.test.ts @@ -236,6 +236,7 @@ describe('SQLite recovery authority multi-process races', () => { .user_version, SQLITE_RUNTIME_SCHEMA_VERSION, ); + assert.equal(database.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); } finally { database.close(); } diff --git a/packages/storage/src/__tests__/sqlite-runtime-store.test.ts b/packages/storage/src/__tests__/sqlite-runtime-store.test.ts index e4b8452973e..8bdfdef42ce 100644 --- a/packages/storage/src/__tests__/sqlite-runtime-store.test.ts +++ b/packages/storage/src/__tests__/sqlite-runtime-store.test.ts @@ -127,6 +127,35 @@ describe('SqliteRuntimeStore', () => { }); }); + it('configures incremental auto-vacuum when another opener created the empty file', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-sqlite-runtime-empty-open-')); + const dbPath = join(root, 'runtime.sqlite'); + const first = new DatabaseSync(dbPath); + try { + const store = createSqliteRuntimeStore(dbPath); + store.close(); + assert.equal(first.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 2); + } finally { + first.close(); + await rm(root, { recursive: true, force: true }); + } + }); + + it('preserves an initialized NONE database even when it has no tables', async () => { + const root = await mkdtemp(join(tmpdir(), 'maka-sqlite-runtime-empty-none-')); + const dbPath = join(root, 'runtime.sqlite'); + const first = new DatabaseSync(dbPath); + try { + first.exec('BEGIN IMMEDIATE; COMMIT'); + const store = createSqliteRuntimeStore(dbPath); + store.close(); + assert.equal(first.prepare('PRAGMA auto_vacuum').get()?.auto_vacuum, 0); + } finally { + first.close(); + await rm(root, { recursive: true, force: true }); + } + }); + it('reads a complete Session from one snapshot while another connection commits its terminal', async (t) => { await withStore(async (store, dbPath) => { const opening = invocationOpeningEvent(1); diff --git a/packages/storage/src/operational-state-store.ts b/packages/storage/src/operational-state-store.ts index a111215528d..ba657bd5b5b 100644 --- a/packages/storage/src/operational-state-store.ts +++ b/packages/storage/src/operational-state-store.ts @@ -226,7 +226,7 @@ class OperationalStateDatabaseOwner { configureSqliteRuntimeLockWait(this.database); this.database.exec('PRAGMA foreign_keys = ON'); if (options.schemaMigration !== 'require_current') { - configureSqliteRuntimeAutoVacuumForNewDatabase(this.database, databaseExisted); + configureSqliteRuntimeAutoVacuumForNewDatabase(this.database); } if (options.schemaMigration === 'require_current') { requireCurrentOperationalState(this.database); diff --git a/packages/storage/src/sqlite-runtime-schema.ts b/packages/storage/src/sqlite-runtime-schema.ts index 5591162eec6..e2ae018706e 100644 --- a/packages/storage/src/sqlite-runtime-schema.ts +++ b/packages/storage/src/sqlite-runtime-schema.ts @@ -954,16 +954,13 @@ export function configureSqliteRuntimeDatabase(db: DatabaseSync): void { db.exec('PRAGMA foreign_keys = ON'); } -/** Set the reclamation mode before schema creation on a newly created file only. */ -export function configureSqliteRuntimeAutoVacuumForNewDatabase( - db: DatabaseSync, - databaseExisted: boolean, -): void { - if (databaseExisted) return; - const existingSchema = db - .prepare("SELECT 1 AS present FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%' LIMIT 1") - .get() as { present?: unknown } | undefined; - if (existingSchema?.present === 1) return; +/** Set the reclamation mode before an uninitialized database gets its first page. */ +export function configureSqliteRuntimeAutoVacuumForNewDatabase(db: DatabaseSync): void { + // Another opener may have created the file without initializing its header. + // File existence cannot tell us whether it still needs this configuration. + // Preserve initialized databases, including those with no application tables. + const pages = db.prepare('PRAGMA page_count').get() as { page_count: number }; + if (pages.page_count !== 0) return; db.exec('PRAGMA auto_vacuum = INCREMENTAL'); const configured = db.prepare('PRAGMA auto_vacuum').get() as | { auto_vacuum?: unknown } diff --git a/packages/storage/src/sqlite-runtime-store.ts b/packages/storage/src/sqlite-runtime-store.ts index ceb3cf9b188..96df894ea83 100644 --- a/packages/storage/src/sqlite-runtime-store.ts +++ b/packages/storage/src/sqlite-runtime-store.ts @@ -36,7 +36,7 @@ export type { ImmutableRuntimePrefixProofReadBudget, } from './runtime-event-store-contract.js'; -import { existsSync, mkdirSync } from 'node:fs'; +import { mkdirSync } from 'node:fs'; import { createRequire } from 'node:module'; import { dirname } from 'node:path'; import type { DatabaseSync, SQLInputValue } from 'node:sqlite'; @@ -134,6 +134,7 @@ import { import { configureSqliteRuntimeDatabase, configureSqliteRuntimeAutoVacuumForNewDatabase, + configureSqliteRuntimeLockWait, migrateSqliteRuntimeDatabase, readUserVersion, RUNTIME_RECOVERY_AUTHORITY_CAPABILITY, @@ -346,7 +347,6 @@ export class SqliteRuntimeStore return; } const DatabaseSync = loadDatabaseSync(); - const databaseExisted = path !== ':memory:' && existsSync(path); this.db = options.readOnly ? new DatabaseSync(path, { readOnly: true }) : new DatabaseSync(path); @@ -360,7 +360,8 @@ export class SqliteRuntimeStore ); } } else { - configureSqliteRuntimeAutoVacuumForNewDatabase(this.db, databaseExisted); + configureSqliteRuntimeLockWait(this.db); + configureSqliteRuntimeAutoVacuumForNewDatabase(this.db); configureSqliteRuntimeDatabase(this.db); migrateSqliteRuntimeDatabase(this.db); }