🔄 Migration Troubleshooting

Debug and resolve Datum migration issues.

📖 6 min read

Handle schema changes, data transformations, and version upgrades in Datum.

Schema Migration Failures#

Issue: Migration execution errors#

Symptoms: App crashes during startup with migration errors

Common Causes:

  • Invalid data transformation logic
  • Missing null checks in migration code
  • Schema version conflicts between local and remote

Debugging Steps:

// Enable detailed migration logging
final config = DatumConfig(
  logger: DatumLogger(
    enabled: true,
    level: LogLevel.debug,
  ),
);

// Check current schema version
final currentVersion = await localAdapter.getSchemaVersion();
print('Current schema version: $currentVersion');

// Verify migration path
final targetVersion = 3; // Your target version
if (currentVersion < targetVersion) {
  print('Migration needed from v$currentVersion to v$targetVersion');
}

Migration Implementation:

class Migration1To2 implements Migration {
  @override
  Future<Map<String, dynamic>> execute(Map<String, dynamic> data) async {
    // Always validate input data
    if (data == null) {
      throw MigrationException('Migration data cannot be null');
    }

    // Create a copy to avoid modifying original
    final migratedData = Map<String, dynamic>.from(data);

    // Safe field transformations
    if (migratedData.containsKey('oldFieldName')) {
      migratedData['newFieldName'] = migratedData['oldFieldName'];
      migratedData.remove('oldFieldName');
    }

    // Add default values for new required fields
    migratedData['newRequiredField'] ??= 'defaultValue';

    return migratedData;
  }

  @override
  Future<Map<String, dynamic>> rollback(Map<String, dynamic> data) async {
    // Implement rollback logic
    final rolledBackData = Map<String, dynamic>.from(data);

    if (rolledBackData.containsKey('newFieldName')) {
      rolledBackData['oldFieldName'] = rolledBackData['newFieldName'];
      rolledBackData.remove('newFieldName');
    }

    rolledBackData.remove('newRequiredField');
    return rolledBackData;
  }
}

Issue: Data loss during migration#

Symptoms: Data disappears after migration

Prevention Strategies:

// Always backup before migration
class MigrationManager {
  static Future<void> safeMigrate(
    LocalAdapter adapter,
    List<Migration> migrations,
  ) async {
    // Create backup
    final backup = await adapter.createBackup();
    print('Backup created: ${backup.path}');

    try {
      // Run migrations
      for (final migration in migrations) {
        await adapter.runMigration(migration);
        print('Migration ${migration.runtimeType} completed');
      }
    } catch (e) {
      // Restore from backup on failure
      await adapter.restoreFromBackup(backup);
      print('Migration failed, restored from backup');
      rethrow;
    }
  }
}

Version Compatibility Issues#

Issue: Local and remote schema mismatch#

Symptoms: Sync fails with schema incompatibility errors

Resolution Steps:

// Check schema versions
final localVersion = await localAdapter.getSchemaVersion();
final remoteVersion = await remoteAdapter.getSchemaVersion();

if (localVersion != remoteVersion) {
  print('Schema mismatch: local=$localVersion, remote=$remoteVersion');

  // Handle version differences
  if (localVersion < remoteVersion) {
    // Local is behind, update local schema
    await runMigrations(localAdapter, remoteVersion);
  } else {
    // Remote is behind, this might require server update
    throw Exception('Remote schema is outdated');
  }
}

Issue: Breaking changes in entity definitions#

Symptoms: Serialization/deserialization errors after entity changes

Entity Evolution Strategies:

class BackwardCompatibleTask extends DatumEntity {
  // Keep old field names for backward compatibility
  @deprecated
  String? get oldFieldName => newFieldName;

  // Add new fields with defaults
  String? newFieldName;

  // Custom serialization handling
  @override
  Map<String, dynamic> toDatumMap({MapTarget target = MapTarget.local}) {
    final map = super.toDatumMap(target: target);

    // Handle field name changes
    if (target == MapTarget.remote && oldFieldName != null) {
      map['legacyFieldName'] = oldFieldName;
    }

    return map;
  }

  factory BackwardCompatibleTask.fromMap(Map<String, dynamic> map) {
    // Handle both old and new field names
    final fieldValue = map['newFieldName'] ?? map['oldFieldName'];

    return BackwardCompatibleTask(
      newFieldName: fieldValue,
      // ... other fields
    );
  }
}

Data Transformation Issues#

Issue: Complex data restructuring#

Symptoms: Migration logic becomes too complex

Advanced Migration Patterns:

class ComplexMigration2To3 implements Migration {
  @override
  Future<Map<String, dynamic>> execute(Map<String, dynamic> data) async {
    final migratedData = Map<String, dynamic>.from(data);

    // Handle nested object restructuring
    if (migratedData['nestedObject'] is Map) {
      final nested = migratedData['nestedObject'] as Map<String, dynamic>;

      // Flatten nested structure
      migratedData['flattenedField'] = nested['deepField'];
      migratedData.remove('nestedObject');
    }

    // Handle array transformations
    if (migratedData['tags'] is List) {
      final tags = migratedData['tags'] as List;
      migratedData['tagObjects'] = tags.map((tag) => {
        'name': tag,
        'created': DateTime.now().toIso8601String(),
      }).toList();
    }

    return migratedData;
  }
}

Issue: Large dataset migration performance#

Symptoms: Migration takes too long for large datasets

Performance Optimization:

class BatchedMigration implements Migration {
  static const batchSize = 100;

  @override
  Future<Map<String, dynamic>> execute(Map<String, dynamic> data) async {
    // For large migrations, process in batches
    final allRecords = await getAllRecords();
    final batches = <List<Map<String, dynamic>>>[];

    for (var i = 0; i < allRecords.length; i += batchSize) {
      final end = (i + batchSize < allRecords.length) ? i + batchSize : allRecords.length;
      batches.add(allRecords.sublist(i, end));
    }

    for (final batch in batches) {
      await processBatch(batch);
      // Allow UI to remain responsive
      await Future.delayed(Duration(milliseconds: 10));
    }

    return data; // Return original for single record migrations
  }
}

Cross-Platform Migration Issues#

Issue: Platform-specific data incompatibility#

Symptoms: Data works on one platform but fails on another

Cross-Platform Solutions:

class CrossPlatformMigration implements Migration {
  @override
  Future<Map<String, dynamic>> execute(Map<String, dynamic> data) async {
    final migratedData = Map<String, dynamic>.from(data);

    // Normalize platform-specific data
    migratedData['platform'] = await detectCurrentPlatform();

    // Handle file path differences
    if (migratedData['filePath'] is String) {
      migratedData['filePath'] = normalizeFilePath(migratedData['filePath']);
    }

    // Standardize date formats
    if (migratedData['createdAt'] is String) {
      migratedData['createdAt'] = standardizeDateFormat(migratedData['createdAt']);
    }

    return migratedData;
  }

  Future<String> detectCurrentPlatform() async {
    if (Platform.isAndroid) return 'android';
    if (Platform.isIOS) return 'ios';
    if (Platform.isWindows) return 'windows';
    if (Platform.isMacOS) return 'macos';
    if (Platform.isLinux) return 'linux';
    return 'unknown';
  }
}

Testing Migration Changes#

Migration Testing Framework#

class MigrationTestSuite {
  static Future<void> testMigration(
    Migration migration,
    Map<String, dynamic> inputData,
    Map<String, dynamic> expectedOutput,
  ) async {
    // Test forward migration
    final migratedData = await migration.execute(inputData);
    expect(migratedData, equals(expectedOutput));

    // Test rollback
    final rolledBackData = await migration.rollback(migratedData);
    expect(rolledBackData, equals(inputData));
  }

  static Future<void> testLargeDatasetMigration(
    Migration migration,
    int recordCount,
  ) async {
    // Generate test data
    final testData = generateTestRecords(recordCount);

    final stopwatch = Stopwatch()..start();
    for (final record in testData) {
      await migration.execute(record);
    }
    stopwatch.stop();

    print('Migrated $recordCount records in ${stopwatch.elapsed.inSeconds}s');
    print('Average: ${(stopwatch.elapsed.inMilliseconds / recordCount).round()}ms per record');
  }
}

Rollback and Recovery#

Issue: Failed migration recovery#

Symptoms: Migration fails and app is left in broken state

Recovery Strategies:

class MigrationRecovery {
  static Future<void> recoverFromFailedMigration(
    LocalAdapter adapter,
    String backupPath,
  ) async {
    try {
      // Attempt to restore from backup
      await adapter.restoreFromBackup(backupPath);
      print('Successfully restored from backup');
    } catch (e) {
      // If backup fails, try emergency recovery
      await emergencyRecovery(adapter);
    }
  }

  static Future<void> emergencyRecovery(LocalAdapter adapter) async {
    // Clear corrupted data and start fresh
    await adapter.clearAllData();

    // Reinitialize with default state
    await adapter.initialize();

    print('Emergency recovery completed - data reset to defaults');
  }
}

Best Practices#

1. Test Migrations Thoroughly#

// Always test migrations with real data
void main() {
  test('Migration preserves data integrity', () async {
    final testData = createTestData();
    final migration = Migration1To2();

    final migrated = await migration.execute(testData);
    final rolledBack = await migration.rollback(migrated);

    expect(rolledBack, equals(testData));
  });
}

2. Version Control Migrations#

// Keep migrations in version control
// migrations/
//   v1_to_v2.dart
//   v2_to_v3.dart
//   v3_to_v4.dart

class MigrationRegistry {
  static final migrations = <String, Migration>{
    '1->2': Migration1To2(),
    '2->3': Migration2To3(),
    '3->4': Migration3To4(),
  };

  static List<Migration> getMigrationPath(int fromVersion, int toVersion) {
    final path = <Migration>[];
    for (var v = fromVersion; v < toVersion; v++) {
      final migrationKey = '$v->${v + 1}';
      final migration = migrations[migrationKey];
      if (migration != null) {
        path.add(migration);
      }
    }
    return path;
  }
}

3. Monitor Migration Performance#

class MigrationMonitor {
  static Future<void> monitorMigration(
    Migration migration,
    Map<String, dynamic> data,
  ) async {
    final stopwatch = Stopwatch()..start();
    final result = await migration.execute(data);
    stopwatch.stop();

    // Log performance metrics
    await logMigrationMetrics(
      migration.runtimeType.toString(),
      stopwatch.elapsed,
      data.length,
    );

    return result;
  }
}

For more migration patterns, check the Migration Module documentation.