From 9f09f7ba215d871d9f392180d312949361e591f1 Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Fri, 11 Oct 2024 06:27:24 -0700 Subject: [PATCH 1/2] Fixed an issue where datastore root references were incomplete Closes #230 --- .../Datastore/DatastoreRoot.swift | 34 +++++++ .../Disk Persistence/DiskPersistence.swift | 4 +- .../Snapshot/SnapshotIteration.swift | 4 +- .../Transaction/Transaction.swift | 4 +- .../SnapshotIterationTests.swift | 92 +++++++++++++++++++ 5 files changed, 132 insertions(+), 6 deletions(-) create mode 100644 Tests/CodableDatastoreTests/SnapshotIterationTests.swift diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift index 76f0cc8..4f48547 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift @@ -10,12 +10,46 @@ import Foundation typealias DatastoreRootIdentifier = DatedIdentifier.Datastore.RootObject> +/// A reference to a particular root within a datastore. +/// +/// Prior to version 0.4 (2024-10-11), only the root ID was stored in the snapshot iteration manifest's root file, which meant one would need to guess which datastore it belonged to. This type thus tries to decode both a single ``DatastoreRootIdentifier`` and a pair of ``DatastoreIdentifier`` and ``DatastoreRootIdentifier``s. +struct DatastoreRootReference: Codable, Hashable { + /// The datastore ID the reference points to. + /// + /// This may be nil when decoding from disk as a result of older iterations — when initializing a reference directly, it must not be nil. + var datastoreID: DatastoreIdentifier? + + /// The datastore root ID the reference points to. + var datastoreRootID: DatastoreRootIdentifier + + init(datastoreID: DatastoreIdentifier, datastoreRootID: DatastoreRootIdentifier) { + self.datastoreID = datastoreID + self.datastoreRootID = datastoreRootID + } + + init(from decoder: any Decoder) throws { + /// Attempt to decode a full object, otherwise fall back to a single value as it was prior to version 0.4 (2024-10-11). + do { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + self.datastoreID = try container.decodeIfPresent(DatastoreIdentifier.self, forKey: .datastoreID) + self.datastoreRootID = try container.decode(DatastoreRootIdentifier.self, forKey: .datastoreRootID) + } catch { + self.datastoreID = nil + self.datastoreRootID = try decoder.singleValueContainer().decode(DatastoreRootIdentifier.self) + } + } +} + extension DiskPersistence.Datastore { actor RootObject: Identifiable { let datastore: DiskPersistence.Datastore let id: DatastoreRootIdentifier + nonisolated var referenceID: DatastoreRootReference { + DatastoreRootReference(datastoreID: datastore.id, datastoreRootID: id) + } + var _rootObject: DatastoreRootManifest? var isPersisted: Bool diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift index ba11304..8306943 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift @@ -496,8 +496,8 @@ extension DiskPersistence { func persist( actionName: String?, roots: [DatastoreKey : Datastore.RootObject], - addedDatastoreRoots: Set, - removedDatastoreRoots: Set + addedDatastoreRoots: Set, + removedDatastoreRoots: Set ) async throws { let containsEdits = try await readingCurrentSnapshot { snapshot in try await snapshot.readingManifest { manifest, iteration in diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift index 4feb622..ec63fe6 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift @@ -48,10 +48,10 @@ struct SnapshotIteration: Codable, Equatable, Identifiable { var removedDatastores: Set = [] /// The datastore roots that have been added in this iteration of the snapshot. - var addedDatastoreRoots: Set = [] + var addedDatastoreRoots: Set = [] /// The datastore roots that have been replaced in this iteration of the snapshot. - var removedDatastoreRoots: Set = [] + var removedDatastoreRoots: Set = [] } extension SnapshotIteration { diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift index 240cfc5..ec64246 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift @@ -172,8 +172,8 @@ extension DiskPersistence { try await root.persistIfNeeded() } - let addedDatastoreRoots = Set(createdRootObjects.map(\.id)) - let removedDatastoreRoots = Set(deletedRootObjects.map(\.id)) + let addedDatastoreRoots = Set(createdRootObjects.map(\.referenceID)) + let removedDatastoreRoots = Set(deletedRootObjects.map(\.referenceID)) try await persistence.persist( actionName: actionName, diff --git a/Tests/CodableDatastoreTests/SnapshotIterationTests.swift b/Tests/CodableDatastoreTests/SnapshotIterationTests.swift new file mode 100644 index 0000000..1753da7 --- /dev/null +++ b/Tests/CodableDatastoreTests/SnapshotIterationTests.swift @@ -0,0 +1,92 @@ +// +// SnapshotIterationTests.swift +// CodableDatastore +// +// Created by Dimitri Bouniol on 2025-02-16. +// Copyright © 2023-25 Mochi Development, Inc. All rights reserved. +// + +import XCTest +@testable import CodableDatastore + +final class SnapshotIterationTests: XCTestCase, @unchecked Sendable { + func testDecodingLegacyDatastoreRootReferences() throws { + let data = Data(""" + { + "addedDatastoreRoots" : [ + "2025-02-12 00-00-00-046 44BBE608B9CBF788" + ], + "addedDatastores" : [ + + ], + "creationDate" : "2025-02-12T00:00:00.057Z", + "dataStores" : { + "Store" : { + "id" : "Store-FD9BA6F1BD3667C8", + "key" : "Store", + "root" : "2024-08-24 09-39-57-775 66004A6BA331B89C" + } + }, + "id" : "2025-02-12 00-00-00-057 0130730F8F6A1ACC", + "precedingIteration" : "2025-02-11 23-59-54-727 447A1A1E1CF82177", + "removedDatastoreRoots" : [ + "2025-02-11 23-59-54-721 2AAEA12A38303055" + ], + "removedDatastores" : [ + + ], + "successiveIterations" : [ + + ], + "version" : "alpha" + } + """.utf8) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601WithMilliseconds + _ = try decoder.decode(SnapshotIteration.self, from: data) + } + + func testDecodingCurrentDatastoreRootReferences() throws { + let data = Data(""" + { + "addedDatastoreRoots" : [ + { + "datastoreID" : "Store-FD9BA6F1BD3667C8", + "datastoreRootID" : "2025-02-12 00-00-00-046 44BBE608B9CBF788" + } + ], + "addedDatastores" : [ + + ], + "creationDate" : "2025-02-12T00:00:00.057Z", + "dataStores" : { + "Store" : { + "id" : "Store-FD9BA6F1BD3667C8", + "key" : "Store", + "root" : "2024-08-24 09-39-57-775 66004A6BA331B89C" + } + }, + "id" : "2025-02-12 00-00-00-057 0130730F8F6A1ACC", + "precedingIteration" : "2025-02-11 23-59-54-727 447A1A1E1CF82177", + "removedDatastoreRoots" : [ + { + "datastoreID" : "Store-FD9BA6F1BD3667C8", + "datastoreRootID" : "2025-02-11 23-59-54-721 2AAEA12A38303055" + } + ], + "removedDatastores" : [ + + ], + "successiveIterations" : [ + + ], + "version" : "alpha" + } + """.utf8) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601WithMilliseconds + _ = try decoder.decode(SnapshotIteration.self, from: data) + } +} From fada7c2e014c8579d1abbe7d82c1249532a62c0a Mon Sep 17 00:00:00 2001 From: Dimitri Bouniol Date: Mon, 17 Feb 2025 04:20:03 -0800 Subject: [PATCH 2/2] Added a reference to the preceding snapshot in an iteration --- .../Snapshot/SnapshotIteration.swift | 5 +++ .../SnapshotIterationTests.swift | 44 +++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift index ec63fe6..1663c5f 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Snapshot/SnapshotIteration.swift @@ -30,6 +30,11 @@ struct SnapshotIteration: Codable, Equatable, Identifiable { /// The iteration this one replaces. var precedingIteration: SnapshotIterationIdentifier? + /// The snapshot the preceding iteration belongs to. + /// + /// When set, the specified snapshot should be referenced to load the ``precedingIteration`` from. When `nil`, the current snapshot should be used. + var precedingSnapshot: SnapshotIdentifier? + /// The iterations that replace this one. /// /// If changes branched at this point in time, there may be more than one iteration to choose from. In this case, the first entry will be the oldest successor, while the last entry will be the most recent. diff --git a/Tests/CodableDatastoreTests/SnapshotIterationTests.swift b/Tests/CodableDatastoreTests/SnapshotIterationTests.swift index 1753da7..e877970 100644 --- a/Tests/CodableDatastoreTests/SnapshotIterationTests.swift +++ b/Tests/CodableDatastoreTests/SnapshotIterationTests.swift @@ -89,4 +89,48 @@ final class SnapshotIterationTests: XCTestCase, @unchecked Sendable { decoder.dateDecodingStrategy = .iso8601WithMilliseconds _ = try decoder.decode(SnapshotIteration.self, from: data) } + + func testDecodingOptionalPrecedingSnapshotIdentifiers() throws { + let data = Data(""" + { + "addedDatastoreRoots" : [ + { + "datastoreID" : "Store-FD9BA6F1BD3667C8", + "datastoreRootID" : "2025-02-12 00-00-00-046 44BBE608B9CBF788" + } + ], + "addedDatastores" : [ + + ], + "creationDate" : "2025-02-12T00:00:00.057Z", + "dataStores" : { + "Store" : { + "id" : "Store-FD9BA6F1BD3667C8", + "key" : "Store", + "root" : "2024-08-24 09-39-57-775 66004A6BA331B89C" + } + }, + "id" : "2025-02-12 00-00-00-057 0130730F8F6A1ACC", + "precedingIteration" : "2025-02-11 23-59-54-727 447A1A1E1CF82177", + "precedingSnapshot" : "2024-04-14 13-09-27-739 A1EEB1A3AF102F15", + "removedDatastoreRoots" : [ + { + "datastoreID" : "Store-FD9BA6F1BD3667C8", + "datastoreRootID" : "2025-02-11 23-59-54-721 2AAEA12A38303055" + } + ], + "removedDatastores" : [ + + ], + "successiveIterations" : [ + + ], + "version" : "alpha" + } + """.utf8) + + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = .iso8601WithMilliseconds + _ = try decoder.decode(SnapshotIteration.self, from: data) + } }