From b7b6be5831bd0b21f109b7a5484fb83a5b55c306 Mon Sep 17 00:00:00 2001 From: Matthew Raymer Date: Mon, 2 Jun 2025 02:48:08 +0000 Subject: [PATCH] fix(sqlite): resolve duplicate table creation in migrations Split initial schema into two sequential migrations to prevent duplicate table creation and improve migration clarity. Changes: - Separate initial schema into two distinct migrations: * 001_initial_accounts (v1): Create accounts table & index * 002_secret_and_settings (v2): Create remaining tables (secret, settings, contacts, logs, temp) - Add version conflict detection to prevent duplicate migration versions - Ensure migrations are sequential (no gaps) - Update rollback scripts to only drop relevant tables Technical Details: - Add validateMigrationVersions() to check for: * Duplicate version numbers * Sequential version ordering * Gaps in version numbers - Validate migrations both at definition time and runtime - Update schema_version tracking to reflect new versioning Testing: - Verified no duplicate table creation - Confirmed migrations run in correct order - Validated rollback procedures - Checked version conflict detection --- electron/src/rt/sqlite-migrations.ts | 52 ++++++++++++++++++---------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/electron/src/rt/sqlite-migrations.ts b/electron/src/rt/sqlite-migrations.ts index 5cb29c6b..75962933 100644 --- a/electron/src/rt/sqlite-migrations.ts +++ b/electron/src/rt/sqlite-migrations.ts @@ -323,6 +323,31 @@ const parseSQL = (sql: string): ParsedSQL => { return result; }; +// Add version conflict detection +const validateMigrationVersions = (migrations: Migration[]): void => { + const versions = new Set(); + const duplicates = new Set(); + + migrations.forEach(migration => { + if (versions.has(migration.version)) { + duplicates.add(migration.version); + } + versions.add(migration.version); + }); + + if (duplicates.size > 0) { + throw new Error(`Duplicate migration versions found: ${Array.from(duplicates).join(', ')}`); + } + + // Verify versions are sequential + const sortedVersions = Array.from(versions).sort((a, b) => a - b); + for (let i = 0; i < sortedVersions.length; i++) { + if (sortedVersions[i] !== i + 1) { + throw new Error(`Migration versions must be sequential. Found gap at version ${i + 1}`); + } + } +}; + // Initial migration for accounts table const INITIAL_MIGRATION: Migration = { version: 1, @@ -357,24 +382,10 @@ const INITIAL_MIGRATION: Migration = { const MIGRATIONS: Migration[] = [ INITIAL_MIGRATION, { - version: 1, - name: 'initial_schema', - description: 'Initial database schema with accounts, secret, settings, contacts, logs, and temp tables', + version: 2, + name: '002_secret_and_settings', + description: 'Add secret, settings, contacts, logs, and temp tables', sql: ` - -- Accounts table for user identities - CREATE TABLE IF NOT EXISTS accounts ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - dateCreated TEXT NOT NULL, - derivationPath TEXT, - did TEXT NOT NULL, - identityEncrBase64 TEXT, -- encrypted & base64-encoded - mnemonicEncrBase64 TEXT, -- encrypted & base64-encoded - passkeyCredIdHex TEXT, - publicKeyHex TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did); - -- Secret table for storing encryption keys -- Note: This is a temporary solution until better secure storage is implemented CREATE TABLE IF NOT EXISTS secret ( @@ -447,7 +458,6 @@ const MIGRATIONS: Migration[] = [ ); `, rollback: ` - DROP TABLE IF EXISTS accounts; DROP TABLE IF EXISTS secret; DROP TABLE IF EXISTS settings; DROP TABLE IF EXISTS contacts; @@ -457,6 +467,9 @@ const MIGRATIONS: Migration[] = [ } ]; +// Validate migrations before export +validateMigrationVersions(MIGRATIONS); + // Helper functions const verifyPluginState = async (plugin: any): Promise => { try { @@ -1004,6 +1017,9 @@ export async function runMigrations( ): Promise { logger.info('Starting migration process'); + // Validate migrations before running + validateMigrationVersions(MIGRATIONS); + // Verify plugin is available if (!await verifyPluginState(plugin)) { throw new Error('SQLite plugin not available');