diff --git a/doc/activeDid-migration-plan.md b/doc/activeDid-migration-plan.md index 12407172..9329a6f5 100644 --- a/doc/activeDid-migration-plan.md +++ b/doc/activeDid-migration-plan.md @@ -1,7 +1,7 @@ # ActiveDid Migration Plan - Separate Table Architecture **Author**: Matthew Raymer -**Date**: 2025-08-29T07:24Z +**Date**: 2025-08-29T08:03Z **Status**: 🎯 **PLANNING** - Active migration planning phase ## Objective @@ -85,29 +85,35 @@ flowchart TD ## Repro: End-to-End Procedure -### Phase 1: Enhanced Schema Creation +### Phase 1: Enhanced Schema Creation via migration.ts -```sql --- Create new active_identity table with proper constraints -CREATE TABLE active_identity ( - id INTEGER PRIMARY KEY CHECK (id = 1), - activeDid TEXT NOT NULL, - lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), - FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE CASCADE -); - --- Add performance indexes -CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid); -CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); - --- Insert default record (will be updated during migration) -INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, '', datetime('now')); +```typescript +// Add to MIGRATIONS array in src/db-sql/migration.ts +{ + name: "003_active_did_separate_table", + sql: ` + -- Create new active_identity table with proper constraints + CREATE TABLE IF NOT EXISTS active_identity ( + id INTEGER PRIMARY KEY CHECK (id = 1), + activeDid TEXT NOT NULL, + lastUpdated TEXT NOT NULL DEFAULT (datetime('now')), + FOREIGN KEY (activeDid) REFERENCES accounts(did) ON DELETE CASCADE + ); + + -- Add performance indexes + CREATE INDEX IF NOT EXISTS idx_active_identity_activeDid ON active_identity(activeDid); + CREATE UNIQUE INDEX IF NOT EXISTS idx_active_identity_single_record ON active_identity(id); + + -- Insert default record (will be updated during migration) + INSERT OR IGNORE INTO active_identity (id, activeDid, lastUpdated) VALUES (1, '', datetime('now')); + `, +}, ``` ### Phase 2: Enhanced Data Migration with Validation ```typescript -// Enhanced migration script with comprehensive validation +// Enhanced migration function with comprehensive validation async function migrateActiveDidToSeparateTable(): Promise { const result: MigrationResult = { success: false, @@ -117,7 +123,7 @@ async function migrateActiveDidToSeparateTable(): Promise { }; try { - // 1. Get current activeDid from settings + // 1. Get current activeDid from settings (legacy approach) const currentSettings = await retrieveSettingsForDefaultAccount(); const activeDid = currentSettings.activeDid; @@ -137,27 +143,20 @@ async function migrateActiveDidToSeparateTable(): Promise { return result; } - // 3. Check if active_identity table already has data - const existingActiveIdentity = await dbQuery( - "SELECT activeDid FROM active_identity WHERE id = 1" + // 3. Update active_identity table (new system) + await dbExec( + "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", + [activeDid] ); - if (existingActiveIdentity?.values?.length) { - // Update existing record - await dbExec( - "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", - [activeDid] - ); - } else { - // Insert new record - await dbExec( - "INSERT INTO active_identity (id, activeDid, lastUpdated) VALUES (1, ?, datetime('now'))", - [activeDid] - ); - } + // 4. Ensure legacy settings.activeDid stays in sync (backward compatibility) + // This maintains compatibility with IndexedDB migration service + await dbExec( + "UPDATE settings SET activeDid = ? WHERE id = ?", + [activeDid, MASTER_SETTINGS_KEY] + ); - result.success = true; - result.dataMigrated = 1; + dataMigrated = 1; result.warnings.push(`Successfully migrated activeDid: ${activeDid}`); } catch (error) { @@ -167,17 +166,9 @@ async function migrateActiveDidToSeparateTable(): Promise { return result; } - -// Migration result interface -interface MigrationResult { - success: boolean; - errors: string[]; - warnings: string[]; - dataMigrated: number; -} ``` -### Phase 3: Focused API Updates +### Phase 3: Focused API Updates with Dual-Write Pattern ```typescript // Updated PlatformServiceMixin method - maintains backward compatibility @@ -201,93 +192,20 @@ async $accountSettings(did?: string, defaults: Settings = {}): Promise } } -// New method for active identity management -async $getActiveIdentity(): Promise<{ activeDid: string | null }> { - try { - const result = await this.$dbQuery( - "SELECT activeDid FROM active_identity WHERE id = 1" - ); - - if (!result?.values?.length) { - return { activeDid: null }; - } - - return { activeDid: result.values[0][0] as string }; - } catch (error) { - logger.error("[Settings Trace] ❌ Failed to get active identity:", error); - return { activeDid: null }; - } -} - -// Enhanced method to get settings without activeDid -async _getSettingsWithoutActiveDid(): Promise { - const result = await this.$dbQuery( - "SELECT id, accountDid, apiServer, filterFeedByNearby, filterFeedByVisible, " + - "finishedOnboarding, firstName, hideRegisterPromptOnNewContact, isRegistered, " + - "lastName, lastAckedOfferToUserJwtId, lastAckedOfferToUserProjectsJwtId, " + - "lastNotifiedClaimId, lastViewedClaimId, notifyingNewActivityTime, " + - "notifyingReminderMessage, notifyingReminderTime, partnerApiServer, " + - "passkeyExpirationMinutes, profileImageUrl, searchBoxes, showContactGivesInline, " + - "showGeneralAdvanced, showShortcutBvc, vapid, warnIfProdServer, warnIfTestServer, " + - "webPushServer FROM settings WHERE id = ?", - [MASTER_SETTINGS_KEY] - ); - - if (!result?.values?.length) { - return DEFAULT_SETTINGS; - } - - return this._mapColumnsToValues(result.columns, result.values)[0] as Settings; -} - -// Enhanced save settings method -async $saveSettings(changes: Partial): Promise { - try { - // Remove fields that shouldn't be updated - const { accountDid, id, activeDid, ...safeChanges } = changes; - - if (Object.keys(safeChanges).length > 0) { - // Convert settings for database storage - const convertedChanges = this._convertSettingsForStorage(safeChanges); - const setParts: string[] = []; - const params: unknown[] = []; - - Object.entries(convertedChanges).forEach(([key, value]) => { - if (value !== undefined) { - setParts.push(`${key} = ?`); - params.push(value); - } - }); - - if (setParts.length > 0) { - params.push(MASTER_SETTINGS_KEY); - await this.$dbExec( - `UPDATE settings SET ${setParts.join(", ")} WHERE id = ?`, - params, - ); - } - } - - // Handle activeDid separately in new table - if (changes.activeDid !== undefined) { - await this.$updateActiveDid(changes.activeDid); - } - - return true; - } catch (error) { - logger.error("[PlatformServiceMixin] Error saving settings:", error); - return false; - } -} - -// Enhanced update activeDid method +// Enhanced update activeDid method with dual-write pattern async $updateActiveDid(newDid: string | null): Promise { try { if (newDid === null) { - // Clear active identity + // Clear active identity in both tables await this.$dbExec( "UPDATE active_identity SET activeDid = '', lastUpdated = datetime('now') WHERE id = 1" ); + + // Keep legacy field in sync (backward compatibility) + await this.$dbExec( + "UPDATE settings SET activeDid = '' WHERE id = ?", + [MASTER_SETTINGS_KEY] + ); } else { // Validate DID exists before setting const accountExists = await this.$dbQuery( @@ -300,11 +218,17 @@ async $updateActiveDid(newDid: string | null): Promise { return false; } - // Update active identity + // Update active identity in new table await this.$dbExec( "UPDATE active_identity SET activeDid = ?, lastUpdated = datetime('now') WHERE id = 1", [newDid] ); + + // Keep legacy field in sync (backward compatibility) + await this.$dbExec( + "UPDATE settings SET activeDid = ? WHERE id = ?", + [newDid, MASTER_SETTINGS_KEY] + ); } // Update internal tracking @@ -431,30 +355,30 @@ async $getMergedSettings(defaultKey: string, accountDid?: string, defaultFallbac ## What Works (Evidence) - ✅ **Current activeDid storage** in settings table - - **Time**: 2025-08-29T07:24Z - - **Evidence**: `src/db/tables/settings.ts:25` - activeDid field exists + - **Time**: 2025-08-29T08:03Z + - **Evidence**: `src/db-sql/migration.ts:67` - activeDid field exists in initial migration - **Verify at**: Current database schema and Settings type definition - ✅ **PlatformServiceMixin integration** with activeDid - - **Time**: 2025-08-29T07:24Z + - **Time**: 2025-08-29T08:03Z - **Evidence**: `src/utils/PlatformServiceMixin.ts:108` - activeDid tracking - **Verify at**: Component usage across all platforms - ✅ **Database migration infrastructure** exists - - **Time**: 2025-08-29T07:24Z + - **Time**: 2025-08-29T08:03Z - **Evidence**: `src/db-sql/migration.ts:31` - migration system in place - **Verify at**: Existing migration scripts and database versioning ## What Doesn't (Evidence & Hypotheses) - ❌ **No separate active_identity table** exists - - **Time**: 2025-08-29T07:24Z + - **Time**: 2025-08-29T08:03Z - **Evidence**: Database schema only shows settings table - **Hypothesis**: Table needs to be created as part of migration - - **Next probe**: Create migration script for new table + - **Next probe**: Add migration to existing MIGRATIONS array - ❌ **Data corruption issues** with orphaned activeDid references - - **Time**: 2025-08-29T07:24Z + - **Time**: 2025-08-29T08:03Z - **Evidence**: `IdentitySwitcherView.vue:175` - `hasCorruptedIdentity` detection - **Hypothesis**: Current schema allows activeDid to point to non-existent accounts - **Next probe**: Implement foreign key constraints in new table @@ -522,7 +446,7 @@ async function rollbackActiveDidMigration(): Promise { | Owner | Task | Exit Criteria | Target Date (UTC) | |-------|------|---------------|-------------------| -| Development Team | Create enhanced migration script | Migration script with validation and rollback | 2025-08-30 | +| Development Team | Add migration to existing MIGRATIONS array | Migration script integrated with existing system | 2025-08-30 | | Development Team | Update type definitions | Settings type updated, ActiveIdentity type created | 2025-08-30 | | Development Team | Update PlatformServiceMixin | Core methods updated and tested | 2025-08-31 | | Development Team | Implement foreign key constraints | Schema validation prevents corruption | 2025-08-31 | @@ -552,7 +476,7 @@ async function rollbackActiveDidMigration(): Promise { ## Collaboration Hooks - **Sign-off checklist**: - - [ ] Migration script tested on development database + - [ ] Migration script integrated with existing MIGRATIONS array - [ ] Foreign key constraints implemented and tested - [ ] PlatformServiceMixin updated and tested - [ ] Rollback procedures validated @@ -571,31 +495,33 @@ async function rollbackActiveDidMigration(): Promise { ## What Needs to Change -### **1. Database Schema** +### **1. Database Schema via migration.ts** +- Add migration to existing MIGRATIONS array in `src/db-sql/migration.ts` - Create `active_identity` table with foreign key constraints - Add performance indexes -- Remove `activeDid` field from `settings` table +- **Keep `activeDid` field in `settings` table temporarily** for backward compatibility +- **Preserve `MASTER_SETTINGS_KEY = "1"`** for legacy migration support ### **2. PlatformServiceMixin Methods** -- `$accountSettings()` - integrate with new table -- `$saveSettings()` - handle activeDid in new table -- `$updateActiveDid()` - validate and update new table +- `$accountSettings()` - integrate with new table while maintaining backward compatibility +- `$saveSettings()` - handle activeDid in new table, sync with legacy field +- `$updateActiveDid()` - validate and update new table, sync with legacy field - `$getActiveIdentity()` - new method for identity management ### **3. Master Settings Functions** -- `retrieveSettingsForDefaultAccount()` - integrate with new table -- `$getMergedSettings()` - integrate with new table - -### **4. Migration Scripts** -- Create migration script with validation -- Implement rollback procedures -- Add data corruption detection +- `retrieveSettingsForDefaultAccount()` - integrate with new table while preserving legacy support +- `$getMergedSettings()` - integrate with new table while preserving legacy support -### **5. Type Definitions** -- Update Settings type to remove activeDid +### **4. Type Definitions** +- **Keep `activeDid` in Settings type temporarily** for backward compatibility - Create ActiveIdentity type for new table - Update related interfaces +### **5. Legacy Compatibility** +- **Preserve `MASTER_SETTINGS_KEY = "1"`** for IndexedDB migration service +- **Maintain dual-write pattern** during transition period +- **Ensure legacy clients can still migrate** from Dexie to SQLite + ## What Doesn't Need to Change - **All Vue components** - API layer handles migration transparently @@ -603,3 +529,48 @@ async function rollbackActiveDidMigration(): Promise { - **User interface** - No changes to identity selection UI - **Authentication flow** - Existing system unchanged - **Component logic** - All activeDid handling through API methods +- **Migration system** - Use existing migration.ts approach, not separate files +- **IndexedDB migration service** - Must continue working for legacy clients + +## Enhanced Architecture: Dual-Write Pattern + +### **Phase 1: Add New Table (Current)** +```typescript +// Create active_identity table +// Keep existing settings.activeDid for backward compatibility +// Use dual-write pattern during transition +``` + +### **Phase 2: Dual-Write Pattern** +```typescript +// When updating activeDid: +// 1. Update active_identity table (new system) +// 2. Update settings.activeDid (legacy compatibility) +// 3. Ensure both stay in sync +``` + +### **Phase 3: Future Cleanup (Not in Current Scope)** +```typescript +// Eventually: +// 1. Remove activeDid from settings table +// 2. Deprecate MASTER_SETTINGS_KEY +// 3. Use pure accountDid IS NULL pattern +// 4. Update IndexedDB migration service +``` + +## Backward Compatibility Requirements + +### **Critical: IndexedDB Migration Service** +- **Must continue working** for users migrating from Dexie +- **Must recognize `id = "1"`** as master settings +- **Must preserve existing migration paths** + +### **Important: Legacy Database Operations** +- **Must continue working** for existing SQLite databases +- **Must handle both old and new patterns** +- **Must not break existing queries** + +### **Desired: Cleaner Architecture** +- **New operations** use `accountDid IS NULL` pattern +- **Legacy operations** continue using `MASTER_SETTINGS_KEY` +- **Gradual migration** toward cleaner patterns \ No newline at end of file