diff --git a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift index 13e6ed0..240cfc5 100644 --- a/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift +++ b/Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift @@ -358,9 +358,7 @@ extension DiskPersistence.Transaction: DatastoreInterfaceProtocol { rootObject: rootManifest ) createdRootObjects.insert(newRootObject) - if createdRootObjects.contains(existingRootObject) { - createdRootObjects.remove(existingRootObject) - } else { + if createdRootObjects.remove(existingRootObject) == nil { deletedRootObjects.insert(existingRootObject) } await datastore.adopt(rootObject: newRootObject) @@ -925,14 +923,12 @@ extension DiskPersistence.Transaction { let datastore = existingRootObject.datastore /// Depending on the cursor type, insert or replace the entry in the index, capturing the new manifesr, added and removed pages, and change in the number of entries. - let ((indexManifest, newPages, removedPages), newEntryCount) = try await { - switch try cursor(for: someCursor) { - case .insertion(let cursor): - return (try await existingIndex.manifest(inserting: entry, at: cursor), 1) - case .instance(let cursor): - return (try await existingIndex.manifest(replacing: entry, at: cursor), 0) - } - }() + let ((indexManifest, newPages, removedPages), newEntryCount) = switch try cursor(for: someCursor) { + case .insertion(let cursor): + (try await existingIndex.manifest(inserting: entry, at: cursor), 1) + case .instance(let cursor): + (try await existingIndex.manifest(replacing: entry, at: cursor), 0) + } /// No change occured, bail early guard existingIndex.id.manifestID != indexManifest.id else { return } @@ -949,9 +945,7 @@ extension DiskPersistence.Transaction { manifest: indexManifest ) createdIndexes.insert(newIndex) - if createdIndexes.contains(existingIndex) { - createdIndexes.insert(existingIndex) - } else { + if createdIndexes.remove(existingIndex) == nil { deletedIndexes.insert(existingIndex) } await datastore.adopt(index: newIndex) @@ -972,9 +966,7 @@ extension DiskPersistence.Transaction { rootObject: rootManifest ) createdRootObjects.insert(newRootObject) - if createdRootObjects.contains(existingRootObject) { - createdRootObjects.remove(existingRootObject) - } else { + if createdRootObjects.remove(existingRootObject) == nil { deletedRootObjects.insert(existingRootObject) } await datastore.adopt(rootObject: newRootObject) @@ -1043,9 +1035,7 @@ extension DiskPersistence.Transaction { manifest: indexManifest ) createdIndexes.insert(newIndex) - if createdIndexes.contains(existingIndex) { - createdIndexes.insert(existingIndex) - } else { + if createdIndexes.remove(existingIndex) == nil { deletedIndexes.insert(existingIndex) } await datastore.adopt(index: newIndex) @@ -1066,9 +1056,7 @@ extension DiskPersistence.Transaction { rootObject: rootManifest ) createdRootObjects.insert(newRootObject) - if createdRootObjects.contains(existingRootObject) { - createdRootObjects.remove(existingRootObject) - } else { + if createdRootObjects.remove(existingRootObject) == nil { deletedRootObjects.insert(existingRootObject) } await datastore.adopt(rootObject: newRootObject) @@ -1119,9 +1107,7 @@ extension DiskPersistence.Transaction { manifest: indexManifest ) createdIndexes.insert(newIndex) - if createdIndexes.contains(existingIndex) { - createdIndexes.insert(existingIndex) - } else { + if createdIndexes.remove(existingIndex) == nil { deletedIndexes.insert(existingIndex) } await datastore.adopt(index: newIndex) @@ -1140,9 +1126,7 @@ extension DiskPersistence.Transaction { rootObject: rootManifest ) createdRootObjects.insert(newRootObject) - if createdRootObjects.contains(existingRootObject) { - createdRootObjects.remove(existingRootObject) - } else { + if createdRootObjects.remove(existingRootObject) == nil { deletedRootObjects.insert(existingRootObject) } await datastore.adopt(rootObject: newRootObject) @@ -1227,9 +1211,7 @@ extension DiskPersistence.Transaction { rootObject: rootManifest ) createdRootObjects.insert(newRootObject) - if createdRootObjects.contains(existingRootObject) { - createdRootObjects.remove(existingRootObject) - } else { + if createdRootObjects.remove(existingRootObject) == nil { deletedRootObjects.insert(existingRootObject) } await datastore.adopt(rootObject: newRootObject) @@ -1310,9 +1292,7 @@ extension DiskPersistence.Transaction { rootObject: rootManifest ) createdRootObjects.insert(newRootObject) - if createdRootObjects.contains(existingRootObject) { - createdRootObjects.remove(existingRootObject) - } else { + if createdRootObjects.remove(existingRootObject) == nil { deletedRootObjects.insert(existingRootObject) } await datastore.adopt(rootObject: newRootObject) diff --git a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift index 325dfea..c3a25ec 100644 --- a/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift +++ b/Tests/CodableDatastoreTests/DiskPersistenceDatastoreTests.swift @@ -23,6 +23,163 @@ final class DiskPersistenceDatastoreTests: XCTestCase, @unchecked Sendable { try? FileManager.default.removeItem(at: temporaryStoreURL) } + func testCreatingEmptyPersistence() async throws { + struct TestFormat: DatastoreFormat { + enum Version: Int, CaseIterable { + case zero + } + + struct Instance: Codable, Identifiable { + var id: String + var value: String + var index: Int + var bucket: Int + } + + static let defaultKey: DatastoreKey = "test" + static let currentVersion = Version.zero + + let index = OneToOneIndex(\.index) + @Direct var bucket = Index(\.bucket) + } + + let persistence = try DiskPersistence(readWriteURL: temporaryStoreURL) + + _ = Datastore.JSONStore( + persistence: persistence, + format: TestFormat.self, + migrations: [ + .zero: { data, decoder in + try decoder.decode(TestFormat.Instance.self, from: data) + } + ] + ) + + try await persistence.createPersistenceIfNecessary() + + let snapshotContents = try FileManager().contentsOfDirectory(at: temporaryStoreURL.appendingPathComponent("Snapshots", isDirectory: true), includingPropertiesForKeys: nil) + XCTAssertEqual(snapshotContents.count, 0) + } + + func testCreatingEmptyDatastoreIndexesAfterRead() async throws { + struct TestFormat: DatastoreFormat { + enum Version: Int, CaseIterable { + case zero + } + + struct Instance: Codable, Identifiable { + var id: String + var value: String + var index: Int + var bucket: Int + } + + static let defaultKey: DatastoreKey = "test" + static let currentVersion = Version.zero + + let index = OneToOneIndex(\.index) + @Direct var bucket = Index(\.bucket) + } + + let persistence = try DiskPersistence(readWriteURL: temporaryStoreURL) + + let datastore = Datastore.JSONStore( + persistence: persistence, + format: TestFormat.self, + migrations: [ + .zero: { data, decoder in + try decoder.decode(TestFormat.Instance.self, from: data) + } + ] + ) + + let count = try await datastore.count + XCTAssertEqual(count, 0) + + // TODO: Add code to verify that the Datastores directory is empty. This is true as of 2024-10-10, but has only been validated manually. + } + + func testCreatingEmptyDatastoreIndexesAfterSingleWrite() async throws { + struct TestFormat: DatastoreFormat { + enum Version: Int, CaseIterable { + case zero + } + + struct Instance: Codable, Identifiable { + var id: String + var value: String + var index: Int + var bucket: Int + } + + static let defaultKey: DatastoreKey = "test" + static let currentVersion = Version.zero + + let index = OneToOneIndex(\.index) + @Direct var bucket = Index(\.bucket) + } + + let persistence = try DiskPersistence(readWriteURL: temporaryStoreURL) + + let datastore = Datastore.JSONStore( + persistence: persistence, + format: TestFormat.self, + migrations: [ + .zero: { data, decoder in + try decoder.decode(TestFormat.Instance.self, from: data) + } + ] + ) + + try await datastore.persist(.init(id: "0", value: "0", index: 0, bucket: 0)) + + let count = try await datastore.count + XCTAssertEqual(count, 1) + + // TODO: Add code to verify that the Index directories have a single manifest each. This is true as of 2024-10-10, but has only been validated manually. + } + + func testCreatingUnreferencedDatastoreIndexesAfterUpdate() async throws { + struct TestFormat: DatastoreFormat { + enum Version: Int, CaseIterable { + case zero + } + + struct Instance: Codable, Identifiable { + var id: String + var value: String + var index: Int + var bucket: Int + } + + static let defaultKey: DatastoreKey = "test" + static let currentVersion = Version.zero + + let index = OneToOneIndex(\.index) + @Direct var bucket = Index(\.bucket) + } + + let persistence = try DiskPersistence(readWriteURL: temporaryStoreURL) + + let datastore = Datastore.JSONStore( + persistence: persistence, + format: TestFormat.self, + migrations: [ + .zero: { data, decoder in + try decoder.decode(TestFormat.Instance.self, from: data) + } + ] + ) + + try await datastore.persist(.init(id: "0", value: "0", index: 0, bucket: 0)) + try await datastore.persist(.init(id: "0", value: "0", index: 0, bucket: 0)) + + let count = try await datastore.count + XCTAssertEqual(count, 1) + + // TODO: Add code to verify that the Index directories have exactly two index manifests each. This is true as of 2024-10-10, but has only been validated manually. + } + func testWritingEntry() async throws { struct TestFormat: DatastoreFormat { enum Version: Int, CaseIterable {