trackslash
VAULT-57 P2

Make the SwiftData store's override import all-or-nothing

0
All issues

Description

Found while writing VAULT-42.

The problem: PersistedLocalVaultStore's override import deletes and saves the old vault before it inserts the backup's items. If the insert fails part way through (a full disk, or an item that doesn't encode), the vault is left empty or partial. The user is restoring from a backup at that point, which limits the damage, but they lose whatever wasn't in it.

What to change: do the delete and the inserts in one save, or roll back to the old contents on failure, so an override import either fully replaces the vault or leaves it untouched. RecordVaultStore already builds the new state before swapping it in; the encrypted store (VAULT-45) inherits that.

Tests: fault injection mid-import leaves the original vault intact.

Sub-issues

0

Linked issues

0

GitHub

0

No branches or pull requests linked.

Comments

1
Bradley

Fixed in https://github.com/badbundle/vault-app/pull/635 (merged as 1455524a).

The change: Import & Override is now all or nothing. PersistedLocalVaultStore.importAndOverrideVault stages the whole replacement in a separate ModelContext on the same container, then saves once, in one SQLite transaction. If an item doesn't encode or the save fails, the staged context is discarded, so the vault is exactly as it was in memory and on disk.

Why not one save plus rollback(): that was the first approach, and it crashes inside SwiftData's rollback() once a context has deleted the old items and tags and then inserted or updated others. A discarded context is never rolled back.

Details:

  • Items the backup shares with the vault are updated in place, and the rest are deleted or inserted. That avoids SwiftData's unique-id upsert merging old tag relationships.
  • #627's scrub still runs after a successful import.
  • RecordVaultStore, and so the encrypted store, already built the new state before swapping it in.

Tests:

  • Contract suite on all three engines: a failed override keeps the same items, tags and order; an import succeeds after a failed one; an override replaces loaded items; merge failure is pinned.
  • A SQLite trigger that fails the save part way leaves the vault intact, before and after reopening.
  • A residue test for items overwritten in place.
  • A mutation check: putting back the old import fails both fault-injection tests.