Skip to content

fix(settings): enhance import data version compatibility with semantic versioning#226

Merged
ahmet-cetinkaya merged 4 commits into
mainfrom
fix/219-backup-restore-compatibility
Feb 6, 2026
Merged

fix(settings): enhance import data version compatibility with semantic versioning#226
ahmet-cetinkaya merged 4 commits into
mainfrom
fix/219-backup-restore-compatibility

Conversation

@ahmet-cetinkaya

@ahmet-cetinkaya ahmet-cetinkaya commented Feb 4, 2026

Copy link
Copy Markdown
Owner

🚀 Motivation and Context

Fixes #219 - Implements proper semantic version comparison for import data validation. Previously, importing data from any older version (even patch versions) would fail with a version mismatch error if the version strings didn't match exactly.

⚙️ Implementation Details

  • Import Command (import_data_command.dart): Added SemanticVersion comparison logic to distinguish between compatible older versions (allow import) and newer versions (block import to prevent downgrade)
  • Migration Service (import_data_migration_service.dart): Extended with 0.16.0 → 0.20.0 migration handling schema changes:
    • HabitRecord: Add status field (default: 0)
    • Task: Add recurrenceConfiguration and reminder offset fields (default: null)
    • Tag: Add type field (default: 0 - Label)
    • Relations: Add tagOrder field (default: 0)
  • Tests: Added comprehensive test coverage for:
    • Version compatibility scenarios (older, same, newer versions)
    • Migration transformations verification
    • Edge cases in semantic version parsing

📋 Checklist for Reviewer

  • Tests passed locally (1740 tests passed)
  • Commit history is clean and descriptive (1 commit)
  • Documentation updated (if applicable)
  • Code quality standards were met (linting passed)

🔗 Related

Summary by Sourcery

Improve backup import compatibility by adding a new data migration path and using semantic versioning to validate imported backup versions.

Bug Fixes:

  • Allow importing backups from older but schema-compatible app versions while blocking imports from newer versions to prevent downgrades.

Enhancements:

  • Add a 0.16.0 → 0.20.0 data migration to backfill newly introduced fields on habits, tasks, tags, and tag relations during import.

Tests:

  • Introduce unit tests for import version compatibility behavior, covering older, equal, and newer version scenarios, and for verifying real migration transformations including the new 0.16.0 → 0.20.0 path.

…c versioning

Implements proper semantic version comparison for import data validation,
allowing imports from older versions while blocking newer versions.
Adds comprehensive test coverage for version compatibility scenarios
and extends migration service with 0.16.0 to 0.20.0 schema updates
including habit status, task recurrence, tag type, and tag order fields.
@ahmet-cetinkaya ahmet-cetinkaya added the bug Something isn't working label Feb 4, 2026
@ahmet-cetinkaya ahmet-cetinkaya self-assigned this Feb 4, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @ahmet-cetinkaya, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly improves the robustness of the data import feature by introducing proper semantic version comparison. This change resolves an issue where minor version differences would incorrectly block data imports. Additionally, it includes a crucial data migration to handle schema changes between versions 0.16.0 and 0.20.0, ensuring that user data is correctly transformed and maintained during updates.

Highlights

  • Semantic Versioning for Data Import: The system now uses semantic versioning to compare imported data versions, allowing imports from older, compatible versions while preventing imports from newer versions.
  • New Data Migration (0.16.0 to 0.20.0): A new migration has been implemented to update the database schema for HabitRecord, Task, Tag, and various relation tables, ensuring data integrity for users upgrading from version 0.16.0 to 0.20.0.
  • Enhanced Test Coverage: Comprehensive tests have been added to validate version compatibility logic and the correctness of the new data migration transformations.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Changelog
  • src/lib/core/application/features/settings/commands/import_data_command.dart
    • Implemented SemanticVersion comparison to differentiate between older (allowed if no migration needed) and newer (blocked) imported data versions.
    • Updated error handling for version mismatches to be more specific about newer versions.
  • src/lib/core/application/features/settings/services/import_data_migration_service.dart
    • Registered a new migration path from 0.16.0 to 0.20.0.
    • Added migration logic to backfill new fields: status for HabitRecord, recurrenceConfiguration and reminder offsets for Task, type for Tag, and tagOrder for relation tables.
  • src/test/core/application/features/settings/commands/import_data_command_test.dart
    • Added new test cases to verify the ImportDataCommandHandler's behavior with different imported data versions (older, same, newer) and migration requirements.
  • src/test/services/import_data_migration_verification_test.dart
    • Introduced a new test file to verify the ImportDataMigrationService's isMigrationNeeded logic and the correctness of various migrateData transformations, including the newly added 0.16.0 to 0.20.0 migration.
Activity
  • The pull request was opened by ahmet-cetinkaya.
  • It addresses and closes issue [BUG] restore into 20.4 fails with backup from 20.3 #219.
  • The author has confirmed that all 1740 tests passed locally.
  • The commit history is clean and descriptive.
  • Code quality standards (linting) were met.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@sourcery-ai

sourcery-ai Bot commented Feb 4, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds semantic-version-aware import compatibility checks and a new 0.16.0→0.20.0 data migration, plus tests that cover version comparison behavior and migration correctness.

Sequence diagram for semantic-version-aware import validation

sequenceDiagram
  actor User
  participant UI
  participant ImportDataCommandHandler
  participant SemanticVersion
  participant ImportDataMigrationService
  participant Database

  User->>UI: Select import file
  UI->>ImportDataCommandHandler: execute(importDataCommand)

  ImportDataCommandHandler->>ImportDataCommandHandler: read importedVersion
  ImportDataCommandHandler->>ImportDataMigrationService: needsMigration(importedVersion, AppInfo_version)
  ImportDataMigrationService-->>ImportDataCommandHandler: migrationNeeded / none

  alt migration needed
    ImportDataCommandHandler->>ImportDataMigrationService: runMigrations(data, importedVersion, AppInfo_version)
    ImportDataMigrationService->>Database: persist migrated data
    Database-->>ImportDataMigrationService: ok
    ImportDataMigrationService-->>ImportDataCommandHandler: migrated data
  else no migration and versions differ
    ImportDataCommandHandler->>SemanticVersion: parse(importedVersion)
    SemanticVersion-->>ImportDataCommandHandler: importedSemVer
    ImportDataCommandHandler->>SemanticVersion: parse(AppInfo_version)
    SemanticVersion-->>ImportDataCommandHandler: currentSemVer

    alt importedSemVer > currentSemVer
      ImportDataCommandHandler-->>UI: throw BusinessException versionMismatchError
    else importedSemVer <= currentSemVer
      ImportDataCommandHandler->>Database: persist imported data
      Database-->>ImportDataCommandHandler: ok
      ImportDataCommandHandler-->>UI: ImportDataResult success
    end
  end
Loading

ER diagram for updated importable entities and relations

erDiagram
  HabitRecord {
    int id
    int status
  }

  Task {
    int id
    string recurrenceConfiguration
    int plannedDateReminderCustomOffset
    int deadlineDateReminderCustomOffset
  }

  Tag {
    int id
    int type
  }

  TaskTag {
    int id
    int tagOrder
  }

  NoteTag {
    int id
    int tagOrder
  }

  HabitTag {
    int id
    int tagOrder
  }

  AppUsageTag {
    int id
    int tagOrder
  }

  TaskTimeRecord {
    int id
  }

  Task ||--o{ TaskTag : has
  Tag ||--o{ TaskTag : classifies

  Note ||--o{ NoteTag : has
  Tag ||--o{ NoteTag : classifies

  Habit ||--o{ HabitTag : has
  Tag ||--o{ HabitTag : classifies

  AppUsage ||--o{ AppUsageTag : has
  Tag ||--o{ AppUsageTag : classifies

  Task ||--o{ TaskTimeRecord : trackedBy
Loading

Class diagram for import migration service and semantic version import command

classDiagram
  class IImportDataMigrationService {
    <<interface>>
    +Future~Map~ migrate(Map data, String fromVersion, String toVersion)
  }

  class ImportDataMigrationService {
    -MigrationRegistry _migrationRegistry
    +ImportDataMigrationService()
    +Future~Map~ migrate(Map data, String fromVersion, String toVersion)
    -Future~Map~ _migrate0_15_0to0_16_0(Map data)
    -Future~Map~ _migrate0_16_0to0_20_0(Map data)
  }

  class MigrationRegistry {
    +void registerMigration(String fromVersion, String toVersion, String description, Future~Map~ migrationFunction(Map data))
    +Future~Map~ runMigrations(Map data, String fromVersion, String toVersion)
    +bool hasMigration(String fromVersion, String toVersion)
  }

  class ImportDataCommandHandler {
    +Future~ImportDataResult~ handle(ImportDataCommand command)
    -void _validateVersion(String importedVersion, String currentVersion)
  }

  class SemanticVersion {
    +int major
    +int minor
    +int patch
    +SemanticVersion(int major, int minor, int patch)
    +static SemanticVersion parse(String value)
    +int compareTo(SemanticVersion other)
    +bool operator_greaterThan(SemanticVersion other)
    +bool operator_lessThan(SemanticVersion other)
    +bool operator_equals(SemanticVersion other)
  }

  class BusinessException {
    +String message
    +String translationKey
    +Map args
    +BusinessException(String message, String translationKey, Map args)
  }

  class AppInfo {
    +static String version
  }

  IImportDataMigrationService <|.. ImportDataMigrationService
  ImportDataMigrationService o-- MigrationRegistry
  ImportDataCommandHandler --> IImportDataMigrationService : uses
  ImportDataCommandHandler --> SemanticVersion : parses and compares
  ImportDataCommandHandler --> BusinessException : throws
  ImportDataCommandHandler --> AppInfo : reads version
Loading

File-Level Changes

Change Details Files
Implement semantic-version-aware import compatibility so older but compatible backups are allowed while newer backups are rejected.
  • Replaced strict string inequality version check with SemanticVersion parsing for both imported and current app versions.
  • Added comparison logic that only throws a BusinessException when the imported version is newer than the current app version and no migration is required.
  • Left behavior unchanged for equal versions and versions handled via the migration pipeline.
src/lib/core/application/features/settings/commands/import_data_command.dart
Introduce migration from 0.16.0 to 0.20.0 to backfill new schema fields in habits, tasks, tags, and tag relations.
  • Registered a new migration step in the migration registry from 0.16.0 to 0.20.0 with an appropriate description.
  • Implemented _migrate0_16_0to0_20_0 to clone the input data map and backfill missing HabitRecord.status with default 0.
  • Extended task migration to add recurrenceConfiguration and reminder offset fields when missing, defaulting them to null.
  • Updated tag entities to backfill missing type with default 0 (Label).
  • Iterated over tag-related relation tables to add tagOrder=0 where applicable, skipping taskTimeRecords and existing tagOrder fields.
src/lib/core/application/features/settings/services/import_data_migration_service.dart
Add tests for import command version compatibility behavior.
  • Created ImportDataCommandHandler tests with extensive repository and service mocks to exercise the import flow without full DI wiring.
  • Covered scenarios for importing from an older version with no migration required, a newer version that should be rejected, the same version, and an older version that requires migration.
  • Verified integration with IImportDataMigrationService and ICompressionService, including that migrations are conditionally invoked.
src/test/core/application/features/settings/commands/import_data_command_test.dart
src/test/core/application/features/settings/commands/import_data_command_test.mocks.dart
Add tests to verify real migration behavior including the new 0.16.0→0.20.0 migration.
  • Added tests for ImportDataMigrationService.isMigrationNeeded for several concrete historical versions and the current app version.
  • Added migration tests that validate the transformation of appUsageTimeRecords (0.6.9→0.6.10), habitRecords date→occurredAt (0.14.1→0.15.0), and the new 0.16.0→0.20.0 backfill of status, recurrenceConfiguration, reminder offsets, tag type, and tagOrder.
  • Ensured migration output appInfo.version is updated to the current AppInfo.version in all tested paths.
src/test/services/import_data_migration_verification_test.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#219 Allow restoring/importing data backups created with app version 20.3 into app version 20.4 without failing with a version mismatch ("version not found") error.
#219 Ensure import/restore logic correctly handles version differences between nearby releases (e.g., 20.3 → 20.4) by accepting compatible older versions without migration and applying migrations when needed.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In ImportDataCommandHandler, SemanticVersion.parse is called on the imported version without guarding against invalid or non-semver strings; consider handling parse errors explicitly so a malformed backup file produces a clear, controlled error instead of an unhandled exception.
  • In _migrate0_16_0to0_20_0, relationTables includes taskTimeRecords but then explicitly excludes it from tagOrder handling; either remove it from the list or document why it's treated differently to avoid confusion for future changes.
  • The migration verification tests hard-code specific version values (e.g., 0.20.3) and expectations about whether migration is needed; consider tying these expectations more directly to the configured migration registry or AppInfo.version semantics to reduce brittleness when versions change.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ImportDataCommandHandler`, `SemanticVersion.parse` is called on the imported version without guarding against invalid or non-semver strings; consider handling parse errors explicitly so a malformed backup file produces a clear, controlled error instead of an unhandled exception.
- In `_migrate0_16_0to0_20_0`, `relationTables` includes `taskTimeRecords` but then explicitly excludes it from `tagOrder` handling; either remove it from the list or document why it's treated differently to avoid confusion for future changes.
- The migration verification tests hard-code specific version values (e.g., `0.20.3`) and expectations about whether migration is needed; consider tying these expectations more directly to the configured migration registry or `AppInfo.version` semantics to reduce brittleness when versions change.

## Individual Comments

### Comment 1
<location> `src/lib/core/application/features/settings/services/import_data_migration_service.dart:319-322` </location>
<code_context>
+          if (!task.containsKey('recurrenceConfiguration')) {
+            task['recurrenceConfiguration'] = null;
+          }
+          if (!task.containsKey('plannedDateReminderCustomOffset')) {
+            task['plannedDateReminderCustomOffset'] = null;
+          }
+          if (!task.containsKey('deadlineDateReminderCustomOffset')) {
+            task['deadlineDateReminderCustomOffset'] = null;
+          }
</code_context>

<issue_to_address>
**nitpick:** Keep the migration description in sync with the fields actually handled here.

The doc comment for `_migrate0_16_0to0_20_0` only mentions `recurrenceConfiguration`, but this step also adds `plannedDateReminderCustomOffset` and `deadlineDateReminderCustomOffset`. Please update the description (or briefly justify why these fields are part of this migration) so future maintainers understand why they are introduced here.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +319 to +322
if (!task.containsKey('plannedDateReminderCustomOffset')) {
task['plannedDateReminderCustomOffset'] = null;
}
if (!task.containsKey('deadlineDateReminderCustomOffset')) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Keep the migration description in sync with the fields actually handled here.

The doc comment for _migrate0_16_0to0_20_0 only mentions recurrenceConfiguration, but this step also adds plannedDateReminderCustomOffset and deadlineDateReminderCustomOffset. Please update the description (or briefly justify why these fields are part of this migration) so future maintainers understand why they are introduced here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly improves the data import functionality by introducing semantic versioning for compatibility checks, which is a great enhancement. It correctly allows importing data from older compatible versions while preventing downgrades from newer versions. The addition of the 0.16.0 to 0.20.0 migration path is well-implemented and is accompanied by thorough and well-written tests that cover various scenarios, including version compatibility and data transformation verification. Overall, this is a solid contribution. I have a few minor suggestions to enhance error handling and code clarity.

Comment on lines +367 to +368
final importedSemVer = SemanticVersion.parse(importedVersion);
final currentSemVer = SemanticVersion.parse(AppInfo.version);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The SemanticVersion.parse() calls could throw a FormatException if the version string from the imported data is not a valid semantic version. This would be caught by the generic try-catch block in the call method, leading to a non-specific error message for the user. It would be better to handle this specific exception and throw a BusinessException with a more informative error, similar to the pattern used in ImportDataMigrationService.

Suggested change
final importedSemVer = SemanticVersion.parse(importedVersion);
final currentSemVer = SemanticVersion.parse(AppInfo.version);
final SemanticVersion importedSemVer;
final SemanticVersion currentSemVer;
try {
importedSemVer = SemanticVersion.parse(importedVersion);
currentSemVer = SemanticVersion.parse(AppInfo.version);
} catch (e) {
throw BusinessException(
'Invalid version format in imported data',
SettingsTranslationKeys.unsupportedVersionError,
args: {
'version': importedVersion,
},
);
}

for (var record in habitRecords) {
if (record is Map<String, dynamic>) {
if (!record.containsKey('status')) {
record['status'] = 0; // Default status: Pending/Skipped (0)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The comment // Default status: Pending/Skipped (0) is inconsistent with the HabitRecordStatus enum, where index 0 corresponds to complete. This could be confusing for future maintenance. The logic of defaulting to complete is correct, as the existence of a HabitRecord previously implied completion. The comment should be updated to reflect this.

Suggested change
record['status'] = 0; // Default status: Pending/Skipped (0)
record['status'] = 0; // Default status: complete (0)

}

// 4. Migrate Relations: Add tagOrder
final relationTables = ['taskTags', 'noteTags', 'habitTags', 'appUsageTags', 'taskTimeRecords'];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The relationTables list includes 'taskTimeRecords', but this table is intentionally skipped within the loop. To improve code clarity and avoid an unnecessary iteration, it's better to remove 'taskTimeRecords' from the list.

Suggested change
final relationTables = ['taskTags', 'noteTags', 'habitTags', 'appUsageTags', 'taskTimeRecords'];
final relationTables = ['taskTags', 'noteTags', 'habitTags', 'appUsageTags'];

Comment on lines +137 to +143
Uint8List _createMockBackup({required String version}) {
final jsonData = jsonEncode({
'appInfo': {'version': version},
'tasks': [],
});
return Uint8List.fromList(utf8.encode(jsonData));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The helper function _createMockBackup is defined but is not used in any of the tests. This appears to be dead code and should be removed to keep the test file clean.

@ahmet-cetinkaya ahmet-cetinkaya left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review Summary

Overall Assessment

This PR implements semantic versioning for backup/restore compatibility, which addresses issue #219. The core logic is well-designed and follows Clean Architecture principles. However, there are critical error handling issues that must be addressed before merging, particularly around silent failures that could lead to data loss.

Recommendation: Request Changes - Critical issues must be fixed


Critical Issues (Must Fix)

1. Silent Failure in Pre-Import Backup Creation

File: src/lib/core/application/features/settings/commands/import_data_command.dart:204-218

The _createPreImportBackup() method catches all exceptions and returns null, allowing import to proceed without a backup. If the import then fails, users have no recovery path.

Current code:

try {
  final backupFile = await dbInstance.createDatabaseBackup();
  if (kDebugMode) {
    print('Pre-import backup created: ${backupFile?.path}');
  }
  return backupFile;
} catch (e) {
  if (kDebugMode) {
    print('Failed to create pre-import backup: $e');
  }
  return null;  // Silent failure!
}

Suggested fix:

final backupFile = await dbInstance.createDatabaseBackup();
if (kDebugMode) {
  print('Pre-import backup created: ${backupFile?.path}');
}
return backupFile ?? throw BusinessException(
  'Failed to create pre-import backup. Cannot proceed with import.',
  SettingsTranslationKeys.backupCreateError,
);

2. Silent Data Loss from Empty IDs

File: src/lib/core/application/features/settings/commands/import_data_command.dart:512-525

Items with missing/empty IDs are silently skipped during import. Users get a "successful" import but are missing data.

Suggested fix:

int skippedCount = 0;
for (var item in items) {
  if (item['id'] == null || (item['id'] is String && (item['id'] as String).isEmpty)) {
    skippedCount++;
    Logger.warning('Skipping item with missing ID in ${config.name}');
    continue;
  }
  // ... rest of import logic
}

// Throw if too many items skipped (indicates corrupted backup)
if (skippedCount > items.length * 0.1) {
  throw BusinessException(
    'Too many items ($skippedCount/${items.length}) with missing IDs in ${config.name}',
    SettingsTranslationKeys.importDataIntegrityError,
  );
}

3. Invalid Version Returns False Instead of Throwing

File: src/lib/core/application/features/settings/services/import_data_migration_service.dart:140-143

When isMigrationNeeded() encounters an invalid version format, it returns false. The import proceeds without migration, causing potential data corruption.

Suggested fix:

} catch (e, stackTrace) {
  Logger.error(
    'Invalid version format: $sourceVersion',
    error: e,
    stackTrace: stackTrace,
  );
  throw BusinessException(
    'Invalid version format in backup data: $sourceVersion',
    SettingsTranslationKeys.backupInvalidFormatError,
    args: {'version': sourceVersion},
  );
}

4. Overly Broad Catch Block

File: src/lib/core/application/features/settings/commands/import_data_command.dart:370-378

Catches all exceptions but throws a generic "Invalid version format", hiding the root cause (whether it's FormatException, NullReferenceError, etc.).

Suggested fix:

try {
  importedSemVer = SemanticVersion.parse(importedVersion);
  currentSemVer = SemanticVersion.parse(AppInfo.version);
} on FormatException catch (e) {
  throw BusinessException(
    'Invalid version format in imported data',
    SettingsTranslationKeys.backupInvalidFormatError,
    args: {'version': importedVersion},
  );
} catch (e, stackTrace) {
  Logger.error('Unexpected error parsing versions', error: e, stackTrace: stackTrace);
  throw BusinessException(
    'Failed to parse version information',
    SettingsTranslationKeys.versionParseError,
  );
}

5. Missing Production Error Logging

File: src/lib/core/application/features/settings/commands/import_data_command.dart:434-445

Top-level catch only logs in debug mode. Production import failures have no traceability.

Suggested fix:

} catch (e, stackTrace) {
  Logger.error(
    'Import failed with strategy: ${request.strategy}',
    error: e,
    stackTrace: stackTrace,
  );
  if (kDebugMode) {
    print('CRITICAL: Import failed: $e');
  }
  rethrow;
}

Test Coverage Gaps (Should Add)

Invalid Version Format Not Tested

The critical error path for malformed version strings is not tested. This is a high-risk scenario.

Test needed:

test('should throw BusinessException when version format is invalid', () async {
  when(mockCompressionService.extractFromWhphFile(any)).thenAnswer((_) async {
    return jsonEncode({'appInfo': {'version': 'invalid.version.format'}, 'tasks': []});
  });
  
  expect(
    () => handler.call(ImportDataCommand(backupData, ImportStrategy.replace)),
    throwsA(predicate((e) =>
      e is BusinessException &&
      e.errorCode == SettingsTranslationKeys.backupInvalidFormatError))
  );
});

Also missing tests for:

  • Migration error logging verification
  • Null/missing collections in backup data
  • All relation tables (noteTags, habitTags, appUsageTags) - only taskTags is tested

Positive Observations

  • Semantic version comparison logic is well-implemented
  • Clean Architecture and CQRS patterns followed correctly
  • New 0.16.0 → 0.20.0 migration properly handles schema changes
  • Tests follow good naming conventions
  • Idempotent migration logic (checks containsKey() before adding fields)

Recommended Next Steps

  1. Fix the 5 critical issues above
  2. Add tests for invalid version format error path
  3. Re-run review after fixes are complete

Improve error handling in import data command with proper logging
and business exception throwing. Add comprehensive validation for
version parsing, backup creation, and data integrity checks. Update
translation keys and locale files to support new error messages.
Enhance test coverage for error scenarios including invalid version
formats and corrupted backups.
@ahmet-cetinkaya ahmet-cetinkaya merged commit 38fb771 into main Feb 6, 2026
4 checks passed
@ahmet-cetinkaya ahmet-cetinkaya deleted the fix/219-backup-restore-compatibility branch May 22, 2026 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] restore into 20.4 fails with backup from 20.3

1 participant