Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,46 @@ import Foundation

typealias DatastoreRootIdentifier = DatedIdentifier<DiskPersistence<ReadOnly>.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<CodingKeys> = 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<AccessMode>.Datastore

let id: DatastoreRootIdentifier

nonisolated var referenceID: DatastoreRootReference {
DatastoreRootReference(datastoreID: datastore.id, datastoreRootID: id)
}

var _rootObject: DatastoreRootManifest?

var isPersisted: Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -496,8 +496,8 @@ extension DiskPersistence {
func persist(
actionName: String?,
roots: [DatastoreKey : Datastore.RootObject],
addedDatastoreRoots: Set<DatastoreRootIdentifier>,
removedDatastoreRoots: Set<DatastoreRootIdentifier>
addedDatastoreRoots: Set<DatastoreRootReference>,
removedDatastoreRoots: Set<DatastoreRootReference>
) async throws {
let containsEdits = try await readingCurrentSnapshot { snapshot in
try await snapshot.readingManifest { manifest, iteration in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -48,10 +53,10 @@ struct SnapshotIteration: Codable, Equatable, Identifiable {
var removedDatastores: Set<DatastoreIdentifier> = []

/// The datastore roots that have been added in this iteration of the snapshot.
var addedDatastoreRoots: Set<DatastoreRootIdentifier> = []
var addedDatastoreRoots: Set<DatastoreRootReference> = []

/// The datastore roots that have been replaced in this iteration of the snapshot.
var removedDatastoreRoots: Set<DatastoreRootIdentifier> = []
var removedDatastoreRoots: Set<DatastoreRootReference> = []
}

extension SnapshotIteration {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
136 changes: 136 additions & 0 deletions Tests/CodableDatastoreTests/SnapshotIterationTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// 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)
}

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)
}
}