12 KiB
Migration Guide: Dexie to absurd-sql
Overview
This document outlines the migration process from Dexie.js to absurd-sql for the TimeSafari app's storage implementation. The migration aims to provide a consistent SQLite-based storage solution across all platforms while maintaining data integrity and ensuring a smooth transition for users.
Current Status: The migration is in Phase 2 with a well-defined migration fence in place. Core settings and account data have been migrated, with contact migration in progress.
Migration Goals
-
Data Integrity
- Preserve all existing data
- Maintain data relationships
- Ensure data consistency
-
Performance
- Improve query performance
- Reduce storage overhead
- Optimize for platform-specific features
-
Security
- Maintain or improve encryption
- Preserve access controls
- Enhance data protection
-
User Experience
- Zero data loss
- Minimal downtime
- Automatic migration where possible
Migration Fence
The migration is controlled by a migration fence that separates legacy Dexie code from the new SQLite implementation. See Migration Fence Definition for complete details.
Key Fence Components
- Configuration Control:
USE_DEXIE_DB = false
(default) - Service Layer: All database operations go through
PlatformService
- Migration Tools: Exclusive access to both databases during migration
- Code Boundaries: Clear separation between legacy and new code
Prerequisites
-
Backup Requirements
interface MigrationBackup { timestamp: number; accounts: Account[]; settings: Setting[]; contacts: Contact[]; metadata: { version: string; platform: string; dexieVersion: string; }; }
-
Dependencies
{ "@jlongster/sql.js": "^1.8.0", "absurd-sql": "^1.8.0" }
-
Storage Requirements
- Sufficient IndexedDB quota
- Available disk space for SQLite
- Backup storage space
-
Platform Support
- Web: Modern browser with IndexedDB support
- iOS: iOS 13+ with SQLite support
- Android: Android 5+ with SQLite support
- Electron: Latest version with SQLite support
Current Migration Status
✅ Completed
- SQLite Database Service: Fully implemented with absurd-sql
- Platform Service Layer: Unified database interface
- Migration Tools: Data comparison and transfer utilities
- Settings Migration: Core user settings transferred
- Account Migration: Identity and key management
- Schema Migration: Complete table structure migration
🔄 In Progress
- Contact Migration: User contact data (via import interface)
- Data Verification: Comprehensive integrity checks
- Performance Optimization: Query optimization and indexing
📋 Planned
- Code Cleanup: Remove unused Dexie imports
- Documentation Updates: Complete migration guides
- Testing: Comprehensive migration testing
Migration Process
1. Preparation
// src/services/storage/migration/MigrationService.ts
import initSqlJs from '@jlongster/sql.js';
import { SQLiteFS } from 'absurd-sql';
import IndexedDBBackend from 'absurd-sql/dist/indexeddb-backend';
class MigrationService {
private async checkPrerequisites(): Promise<void> {
// Check IndexedDB availability
if (!window.indexedDB) {
throw new StorageError(
'IndexedDB not available',
StorageErrorCodes.INITIALIZATION_FAILED
);
}
// Check storage quota
const quota = await navigator.storage.estimate();
if (quota.quota && quota.usage && quota.usage > quota.quota * 0.9) {
throw new StorageError(
'Insufficient storage space',
StorageErrorCodes.STORAGE_FULL
);
}
// Check platform support
const capabilities = await PlatformDetection.getCapabilities();
if (!capabilities.hasFileSystem) {
throw new StorageError(
'Platform does not support required features',
StorageErrorCodes.INITIALIZATION_FAILED
);
}
}
private async createBackup(): Promise<MigrationBackup> {
const dexieDB = new Dexie('TimeSafariDB');
return {
timestamp: Date.now(),
accounts: await dexieDB.accounts.toArray(),
settings: await dexieDB.settings.toArray(),
contacts: await dexieDB.contacts.toArray(),
metadata: {
version: '1.0.0',
platform: await PlatformDetection.getPlatform(),
dexieVersion: Dexie.version
}
};
}
}
2. Data Migration
// src/services/storage/migration/DataMigration.ts
class DataMigration {
async migrateAccounts(): Promise<MigrationResult> {
const result: MigrationResult = {
success: true,
accountsMigrated: 0,
errors: [],
warnings: []
};
try {
const dexieAccounts = await this.getDexieAccounts();
for (const account of dexieAccounts) {
try {
await this.migrateAccount(account);
result.accountsMigrated++;
} catch (error) {
result.errors.push(`Failed to migrate account ${account.did}: ${error}`);
result.success = false;
}
}
} catch (error) {
result.errors.push(`Account migration failed: ${error}`);
result.success = false;
}
return result;
}
async migrateSettings(): Promise<MigrationResult> {
const result: MigrationResult = {
success: true,
settingsMigrated: 0,
errors: [],
warnings: []
};
try {
const dexieSettings = await this.getDexieSettings();
for (const setting of dexieSettings) {
try {
await this.migrateSetting(setting);
result.settingsMigrated++;
} catch (error) {
result.errors.push(`Failed to migrate setting ${setting.id}: ${error}`);
result.success = false;
}
}
} catch (error) {
result.errors.push(`Settings migration failed: ${error}`);
result.success = false;
}
return result;
}
async migrateContacts(): Promise<MigrationResult> {
// Contact migration is handled through the contact import interface
// This provides better user control and validation
const result: MigrationResult = {
success: true,
contactsMigrated: 0,
errors: [],
warnings: []
};
try {
const dexieContacts = await this.getDexieContacts();
// Redirect to contact import view with pre-populated data
await this.redirectToContactImport(dexieContacts);
result.contactsMigrated = dexieContacts.length;
} catch (error) {
result.errors.push(`Contact migration failed: ${error}`);
result.success = false;
}
return result;
}
}
3. Verification
class MigrationVerification {
async verifyMigration(dexieData: MigrationData): Promise<boolean> {
// Verify account count
const accountResult = await this.sqliteDB.exec('SELECT COUNT(*) as count FROM accounts');
const accountCount = accountResult[0].values[0][0];
if (accountCount !== dexieData.accounts.length) {
return false;
}
// Verify settings count
const settingsResult = await this.sqliteDB.exec('SELECT COUNT(*) as count FROM settings');
const settingsCount = settingsResult[0].values[0][0];
if (settingsCount !== dexieData.settings.length) {
return false;
}
// Verify data integrity
for (const account of dexieData.accounts) {
const result = await this.sqliteDB.exec(
'SELECT * FROM accounts WHERE did = ?',
[account.did]
);
const migratedAccount = result[0]?.values[0];
if (!migratedAccount ||
migratedAccount[1] !== account.publicKeyHex) {
return false;
}
}
return true;
}
}
Using the Migration Interface
Accessing Migration Tools
- Navigate to the Account page in the TimeSafari app
- Scroll down to find the Database Migration link
- Click the link to open the migration interface
Migration Steps
-
Compare Databases
- Click "Compare Databases" to see differences
- Review the comparison results
- Identify data that needs migration
-
Migrate Settings
- Click "Migrate Settings" to transfer user settings
- Verify settings are correctly transferred
- Check application functionality
-
Migrate Contacts
- Click "Migrate Contacts" to open contact import
- Review and confirm contact data
- Complete the import process
-
Verify Migration
- Run comparison again to verify completion
- Test application functionality
- Export backup data if needed
Error Handling
Common Issues
-
Dexie Database Not Enabled
- Error: "Dexie database is not enabled"
- Solution: Set
USE_DEXIE_DB = true
inconstants/app.ts
temporarily
-
Database Connection Issues
- Error: "Failed to retrieve data"
- Solution: Check database initialization and permissions
-
Migration Failures
- Error: "Migration failed: [specific error]"
- Solution: Review error details and check data integrity
Error Recovery
- Review error messages carefully
- Check browser console for additional details
- Verify database connectivity and permissions
- Retry the operation if appropriate
- Export comparison data for manual review if needed
Best Practices
Before Migration
- Backup your data if possible
- Test the migration on a small dataset first
- Verify that both databases are accessible
- Review the comparison results before migrating
During Migration
- Don't interrupt the migration process
- Monitor the progress and error messages
- Note any warnings or skipped records
- Export comparison data for reference
After Migration
- Verify that data was migrated correctly
- Test the application functionality
- Disable Dexie database (
USE_DEXIE_DB = false
) - Clean up any temporary files or exports
Performance Considerations
1. Migration Performance
- Use transactions for bulk data transfer
- Implement progress indicators
- Process data in background when possible
2. Application Performance
- Optimize SQLite queries
- Maintain proper database indexes
- Use efficient memory management
Security Considerations
1. Data Protection
- Maintain encryption standards across migration
- Preserve user privacy during migration
- Log all migration operations
2. Error Handling
- Handle migration failures gracefully
- Provide clear user messaging
- Maintain rollback capabilities
Testing Strategy
1. Migration Testing
describe('Database Migration', () => {
it('should migrate data without loss', async () => {
// 1. Enable Dexie
// 2. Create test data
// 3. Run migration
// 4. Verify data integrity
// 5. Disable Dexie
});
});
2. Application Testing
describe('Feature with Database', () => {
it('should work with SQLite only', async () => {
// Test with USE_DEXIE_DB = false
// Verify all operations use PlatformService
});
});
Conclusion
The migration from Dexie to absurd-sql provides:
- Better Performance: Improved query performance and storage efficiency
- Cross-Platform Consistency: Unified database interface across platforms
- Enhanced Security: Better encryption and access controls
- Future-Proof Architecture: Modern SQLite-based storage system
The migration fence ensures a controlled and safe transition while maintaining data integrity and application stability.