| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166 |
- /** SQLite schema for the disposable session full-text read model. */
- import { DatabaseSync } from 'node:sqlite'
- import { mkdir, open } from 'node:fs/promises'
- import { dirname, resolve } from 'node:path'
- /** Current derived-index schema version. Incompatible versions reset in place. */
- export const SESSION_QUERY_SQLITE_SCHEMA_VERSION = 3
- /** SQLite application id protecting unrelated databases from derived resets. */
- export const SESSION_QUERY_SQLITE_APPLICATION_ID = 0x44534851
- /** Supported SQLite journal modes. */
- export type JournalMode = 'wal' | 'delete' | 'truncate' | 'persist'
- const DERIVED_USER_TABLES = new Set([
- 'search_state',
- 'persisted_sessions',
- 'persisted_docs',
- 'persisted_docs_data',
- 'persisted_docs_idx',
- 'persisted_docs_content',
- 'persisted_docs_docsize',
- 'persisted_docs_config',
- ])
- /**
- * Exclusively create a missing database file with owner-only permissions.
- * Existing files retain their modes, and errors other than `EEXIST` propagate.
- */
- async function createDatabaseFile(path: string): Promise<void> {
- try {
- const handle = await open(path, 'wx', 0o600)
- await handle.close()
- } catch (error) {
- if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error
- }
- }
- /**
- * Open, validate, and initialize persistent and connection-local schemas.
- * @param path - dedicated derived-index path or `:memory:`; missing filesystem paths are created owner-only.
- * @param journalMode - validated SQLite journal mode.
- * @returns initialized database handle owned by the search service.
- */
- export async function openSearchDatabase(path: string, journalMode: JournalMode): Promise<DatabaseSync> {
- const actual = path === ':memory:' ? path : resolve(path)
- if (actual !== ':memory:') {
- await mkdir(dirname(actual), { recursive: true, mode: 0o700 })
- await createDatabaseFile(actual)
- }
- const db = new DatabaseSync(actual)
- try {
- const { application_id: applicationId } = db.prepare('PRAGMA application_id').get() as { application_id: number }
- const { user_version: version } = db.prepare('PRAGMA user_version').get() as { user_version: number }
- const userTables = listUserTables(db)
- if (applicationId !== 0 && applicationId !== SESSION_QUERY_SQLITE_APPLICATION_ID) {
- throw new Error(`session-search database at "${actual}" belongs to another application`)
- }
- if (applicationId === 0 && userTables.length > 0) {
- throw new Error(`session-search database at "${actual}" is not an empty or recognized derived index`)
- }
- if (applicationId === SESSION_QUERY_SQLITE_APPLICATION_ID && version !== SESSION_QUERY_SQLITE_SCHEMA_VERSION) {
- resetDerivedSchema(db, actual, userTables)
- }
- // Apply mutating pragmas only after refusing foreign or canonical files.
- // journalMode is a validated closed union, not caller-controlled SQL.
- db.exec(`PRAGMA journal_mode = ${journalMode.toUpperCase()}`)
- ensurePersistentSchema(db)
- ensureTemporarySchema(db)
- return db
- } catch (error: unknown) {
- db.close()
- throw error
- }
- }
- function listUserTables(db: DatabaseSync): string[] {
- const rows = db.prepare(
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name",
- ).all() as Array<{ name: string }>
- return rows.map(row => row.name)
- }
- function resetDerivedSchema(db: DatabaseSync, path: string, userTables: readonly string[]): void {
- const unknownTables = userTables.filter(name => !DERIVED_USER_TABLES.has(name))
- if (unknownTables.length > 0) {
- throw new Error(
- `session-search database at "${path}" has unrecognized user tables: ${unknownTables.join(', ')}`,
- )
- }
- for (const name of userTables) {
- db.exec(`DROP TABLE IF EXISTS ${quoteIdentifier(name)}`)
- }
- db.exec('PRAGMA user_version = 0')
- }
- function ensurePersistentSchema(db: DatabaseSync): void {
- db.exec(`PRAGMA application_id = ${SESSION_QUERY_SQLITE_APPLICATION_ID}`)
- db.exec(`
- CREATE TABLE IF NOT EXISTS search_state (
- singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
- global_generation INTEGER NOT NULL
- ) STRICT
- `)
- db.exec('INSERT OR IGNORE INTO search_state (singleton, global_generation) VALUES (1, 0)')
- db.exec(`
- CREATE TABLE IF NOT EXISTS persisted_sessions (
- id TEXT PRIMARY KEY,
- version INTEGER NOT NULL,
- created_at INTEGER NOT NULL,
- cwd TEXT,
- parent_session TEXT,
- seed_length INTEGER,
- delegation_depth INTEGER,
- revision TEXT NOT NULL,
- generation INTEGER NOT NULL
- ) STRICT
- `)
- db.exec(`
- CREATE VIRTUAL TABLE IF NOT EXISTS persisted_docs USING fts5(
- text,
- session_id UNINDEXED,
- seq UNINDEXED,
- type UNINDEXED,
- time UNINDEXED,
- surface UNINDEXED,
- codepoint_length UNINDEXED,
- tokenize = 'unicode61'
- )
- `)
- db.exec(`PRAGMA user_version = ${SESSION_QUERY_SQLITE_SCHEMA_VERSION}`)
- }
- function ensureTemporarySchema(db: DatabaseSync): void {
- db.exec(`
- CREATE TEMP TABLE IF NOT EXISTS live_sessions (
- id TEXT PRIMARY KEY,
- version INTEGER NOT NULL,
- created_at INTEGER NOT NULL,
- cwd TEXT,
- parent_session TEXT,
- seed_length INTEGER,
- delegation_depth INTEGER,
- fingerprint TEXT NOT NULL,
- persisted INTEGER NOT NULL CHECK (persisted IN (0, 1)),
- generation INTEGER NOT NULL
- ) STRICT
- `)
- db.exec(`
- CREATE VIRTUAL TABLE IF NOT EXISTS temp.live_docs USING fts5(
- text,
- session_id UNINDEXED,
- seq UNINDEXED,
- type UNINDEXED,
- time UNINDEXED,
- surface UNINDEXED,
- codepoint_length UNINDEXED,
- tokenize = 'unicode61'
- )
- `)
- }
- function quoteIdentifier(value: string): string {
- return `"${value.replaceAll('"', '""')}"`
- }
|