File size: 6,098 Bytes
d04f74a | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 | /** SQLite schema for the disposable session full-text read model. */
import type { 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 = 8
/** 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 { DatabaseSync } = await import('node:sqlite')
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) {
assertDerivedUserTables(actual, userTables)
if (version !== SESSION_QUERY_SQLITE_SCHEMA_VERSION) resetDerivedSchema(db, 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 GLOB 'sqlite_*' ORDER BY name",
).all() as Array<{ name: string }>
return rows.map(row => row.name)
}
function assertDerivedUserTables(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(', ')}`,
)
}
}
function resetDerivedSchema(db: DatabaseSync, userTables: readonly string[]): void {
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,
agent_preset TEXT,
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,
agent_preset TEXT,
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('"', '""')}"`
}
|