mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(hub): reconcile divergent sqlite schema migrations
This commit is contained in:
+11
-1
@@ -29,7 +29,7 @@ export { ScratchlistStore } from './scratchlistStore'
|
||||
export { SessionStore } from './sessionStore'
|
||||
export { UserStore } from './userStore'
|
||||
|
||||
const SCHEMA_VERSION: number = 13
|
||||
const SCHEMA_VERSION: number = 14
|
||||
const REQUIRED_TABLES = [
|
||||
'sessions',
|
||||
'machines',
|
||||
@@ -140,6 +140,7 @@ export class Store {
|
||||
10: () => this.migrateFromV10ToV11(),
|
||||
11: () => this.migrateFromV11ToV12(),
|
||||
12: () => this.migrateFromV12ToV13(),
|
||||
13: () => this.migrateFromV13ToV14(),
|
||||
})
|
||||
|
||||
if (currentVersion === 0) {
|
||||
@@ -528,6 +529,9 @@ export class Store {
|
||||
}
|
||||
|
||||
private migrateFromV12ToV13(): void {
|
||||
// Two development branches previously used schema v12 for different
|
||||
// tables. Reconcile both shapes before advancing the version.
|
||||
this.migrateFromV11ToV12()
|
||||
this.db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS message_epochs (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
@@ -537,6 +541,12 @@ export class Store {
|
||||
`)
|
||||
}
|
||||
|
||||
private migrateFromV13ToV14(): void {
|
||||
// Repair v13 databases produced before the divergent v12 migrations
|
||||
// were reconciled. Both underlying migrations are idempotent.
|
||||
this.migrateFromV12ToV13()
|
||||
}
|
||||
|
||||
private getSessionColumnNames(): Set<string> {
|
||||
const rows = this.db.prepare('PRAGMA table_info(sessions)').all() as Array<{ name: string }>
|
||||
return new Set(rows.map((row) => row.name))
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('Store V11→V12 migration: session_scratchlist table', () => {
|
||||
expect(rows).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('V11 DB migrates through V13 via Store: session_scratchlist created', () => {
|
||||
it('V11 DB migrates through V14 via Store: session_scratchlist created', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'hapi-migration-v12-test-'))
|
||||
const dbPath = join(dir, 'test.db')
|
||||
let store: Store | undefined
|
||||
@@ -61,7 +61,7 @@ describe('Store V11→V12 migration: session_scratchlist table', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('V9 DB migrates through V13 (multi-hop service_tier + fcm_devices + scratchlist)', () => {
|
||||
it('V9 DB migrates through V14 (multi-hop service_tier + fcm_devices + scratchlist)', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'hapi-migration-v9-to-v12-'))
|
||||
const dbPath = join(dir, 'test.db')
|
||||
let store: Store | undefined
|
||||
|
||||
@@ -5,15 +5,17 @@ import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { Store } from './index'
|
||||
|
||||
describe('Store V12→V13 migration: message_epochs', () => {
|
||||
it('fresh DB has message_epochs table', () => {
|
||||
describe('Store V12/V13→V14 schema reconciliation', () => {
|
||||
it('fresh DB has both reconciled tables', () => {
|
||||
const store = new Store(':memory:')
|
||||
expect(tableExists(store, 'message_epochs')).toBe(true)
|
||||
expect(tableExists(store, 'session_scratchlist')).toBe(true)
|
||||
expect(getUserVersion(store)).toBe(14)
|
||||
store.close()
|
||||
})
|
||||
|
||||
it('V12 DB migrates to V13 and preserves existing messages', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'hapi-migration-v13-test-'))
|
||||
it('scratchlist V12 DB migrates to V14 and preserves existing messages', () => {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'hapi-migration-v14-test-'))
|
||||
const dbPath = join(dir, 'test.db')
|
||||
let store: Store | undefined
|
||||
try {
|
||||
@@ -31,6 +33,8 @@ describe('Store V12→V13 migration: message_epochs', () => {
|
||||
|
||||
store = new Store(dbPath)
|
||||
expect(tableExists(store, 'message_epochs')).toBe(true)
|
||||
expect(tableExists(store, 'session_scratchlist')).toBe(true)
|
||||
expect(getUserVersion(store)).toBe(14)
|
||||
expect(store.messages.getMessageEpoch('session-1')).toBe(0)
|
||||
expect(store.messages.getMessages('session-1')).toHaveLength(1)
|
||||
} finally {
|
||||
@@ -38,6 +42,43 @@ describe('Store V12→V13 migration: message_epochs', () => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it.each([
|
||||
['V12', 12],
|
||||
['V13', 13]
|
||||
] as const)('repairs divergent %s DB missing session_scratchlist', (_label, version) => {
|
||||
const dir = mkdtempSync(join(tmpdir(), `hapi-migration-v${version}-repair-test-`))
|
||||
const dbPath = join(dir, 'test.db')
|
||||
let store: Store | undefined
|
||||
try {
|
||||
const db = new Database(dbPath, { create: true, readwrite: true, strict: true })
|
||||
db.exec('PRAGMA journal_mode = WAL')
|
||||
db.exec('PRAGMA foreign_keys = ON')
|
||||
createV12Schema(db)
|
||||
db.exec(`
|
||||
DROP TABLE session_scratchlist;
|
||||
CREATE TABLE message_epochs (
|
||||
session_id TEXT PRIMARY KEY,
|
||||
epoch INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (session_id) REFERENCES sessions(id) ON DELETE CASCADE
|
||||
);
|
||||
INSERT INTO sessions (id, created_at, updated_at) VALUES ('session-1', 1, 1);
|
||||
INSERT INTO messages (id, session_id, content, created_at, seq, invoked_at)
|
||||
VALUES ('message-1', 'session-1', '{}', 1, 1, 1);
|
||||
PRAGMA user_version = ${version};
|
||||
`)
|
||||
db.close()
|
||||
|
||||
store = new Store(dbPath)
|
||||
expect(tableExists(store, 'message_epochs')).toBe(true)
|
||||
expect(tableExists(store, 'session_scratchlist')).toBe(true)
|
||||
expect(getUserVersion(store)).toBe(14)
|
||||
expect(store.messages.getMessages('session-1')).toHaveLength(1)
|
||||
} finally {
|
||||
store?.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function tableExists(store: Store, name: string): boolean {
|
||||
@@ -48,6 +89,12 @@ function tableExists(store: Store, name: string): boolean {
|
||||
return row !== null
|
||||
}
|
||||
|
||||
function getUserVersion(store: Store): number {
|
||||
const db: Database = (store as unknown as { db: Database }).db
|
||||
const row = db.prepare('PRAGMA user_version').get() as { user_version: number }
|
||||
return row.user_version
|
||||
}
|
||||
|
||||
function createV12Schema(db: Database): void {
|
||||
db.exec(`
|
||||
CREATE TABLE sessions (
|
||||
|
||||
Reference in New Issue
Block a user