diff --git a/packages/session-persistence/session-persistence-sqlite/src/schema.ts b/packages/session-persistence/session-persistence-sqlite/src/schema.ts index 395caefb13..a078a43c8b 100644 --- a/packages/session-persistence/session-persistence-sqlite/src/schema.ts +++ b/packages/session-persistence/session-persistence-sqlite/src/schema.ts @@ -91,7 +91,7 @@ function configureDatabase(db: DatabaseSync, path: string, journalMode: JournalM const { user_version: onDisk } = db.prepare('PRAGMA user_version').get() as { user_version: number } const { application_id: applicationId } = db.prepare('PRAGMA application_id').get() as { application_id: number } const { count: userObjectCount } = db.prepare( - "SELECT COUNT(*) AS count FROM sqlite_schema WHERE name NOT LIKE 'sqlite_%'", + "SELECT COUNT(*) AS count FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*'", ).get() as { count: number } if (onDisk === 0 && (applicationId !== 0 || userObjectCount > 0)) { throw new Error(`session database at "${path}" has an unversioned schema or application identity`) diff --git a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts index 886a9ff91f..a7f3ae2f05 100644 --- a/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts +++ b/packages/session-persistence/session-persistence-sqlite/tests/sqlite.spec.ts @@ -345,6 +345,23 @@ describe('SessionPersistenceSqlite: durability and crash semantics', () => { unchanged.close() }) + it('counts a sqliteX table as user-owned instead of mistaking it for SQLite metadata', async () => { + const path = await freshDbPath() + const unrelated = new DatabaseSync(path) + unrelated.exec('CREATE TABLE sqliteX (value TEXT)') + unrelated.exec("INSERT INTO sqliteX VALUES ('safe')") + unrelated.close() + + expect(() => openDatabase(path, 'wal')).toThrow(/unversioned schema or application identity/) + + const unchanged = new DatabaseSync(path) + expect(unchanged.prepare('SELECT value FROM sqliteX').get()).toEqual({ value: 'safe' }) + expect(unchanged.prepare('PRAGMA application_id').get()).toEqual({ application_id: 0 }) + expect(unchanged.prepare('PRAGMA user_version').get()).toEqual({ user_version: 0 }) + expect(unchanged.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'delete' }) + unchanged.close() + }) + it('rejects view-only and foreign-application unversioned databases without mutation', async () => { const viewPath = await freshDbPath() const viewOnly = new DatabaseSync(viewPath) diff --git a/packages/session-query/session-query-sqlite/src/schema.ts b/packages/session-query/session-query-sqlite/src/schema.ts index 9d92247708..47f6374ba6 100644 --- a/packages/session-query/session-query-sqlite/src/schema.ts +++ b/packages/session-query/session-query-sqlite/src/schema.ts @@ -78,7 +78,7 @@ export async function openSearchDatabase(path: string, journalMode: JournalMode) 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", + "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) } diff --git a/packages/session-query/session-query-sqlite/tests/sqlite.spec.ts b/packages/session-query/session-query-sqlite/tests/sqlite.spec.ts index 1923c6f3eb..1812a85428 100644 --- a/packages/session-query/session-query-sqlite/tests/sqlite.spec.ts +++ b/packages/session-query/session-query-sqlite/tests/sqlite.spec.ts @@ -1148,6 +1148,26 @@ describe('SQLite schema, cancellation, and real persistence integration', () => expect(stillForeign.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'wal' }) stillForeign.close() + const wildcardPath = await temporaryPath('sqlite-wildcard.db') + const wildcard = new DatabaseSync(wildcardPath) + wildcard.exec('PRAGMA journal_mode = WAL') + wildcard.exec('CREATE TABLE sqliteX(value TEXT)') + wildcard.exec("INSERT INTO sqliteX VALUES ('safe')") + wildcard.close() + const wildcardCtx = new Context() + await wildcardCtx.plugin(SessionStore) + await expect(wildcardCtx.plugin(SessionQuerySqlite, { + path: wildcardPath, + journalMode: 'delete', + })).rejects.toThrow(expectCode('SESSION_QUERY_INDEX_FAILED')) + expect(wildcardCtx.sessionQuery).toBeUndefined() + const stillWildcard = new DatabaseSync(wildcardPath) + expect(stillWildcard.prepare('SELECT value FROM sqliteX').get()).toEqual({ value: 'safe' }) + expect(stillWildcard.prepare('PRAGMA application_id').get()).toEqual({ application_id: 0 }) + expect(stillWildcard.prepare('PRAGMA user_version').get()).toEqual({ user_version: 0 }) + expect(stillWildcard.prepare('PRAGMA journal_mode').get()).toEqual({ journal_mode: 'wal' }) + stillWildcard.close() + const otherAppPath = await temporaryPath('other-app.db') const otherApp = new DatabaseSync(otherAppPath) otherApp.exec('PRAGMA application_id = 123')