Skip to content

Commit 689110d

Browse files
Added implementation to persist datastore method
1 parent 157e291 commit 689110d

File tree

5 files changed

+256
-1
lines changed

5 files changed

+256
-1
lines changed

Sources/CodableDatastore/Datastore/Datastore.swift

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ public actor Datastore<
2323
let directIndexes: [IndexPath<CodedType>]
2424
let computedIndexes: [IndexPath<CodedType>]
2525

26+
var updatedDescriptor: DatastoreDescriptor?
27+
2628
fileprivate var warmupStatus: TaskStatus = .waiting
2729
fileprivate var warmupProgressHandlers: [ProgressHandler] = []
2830

@@ -74,6 +76,33 @@ public actor Datastore<
7476
}
7577
}
7678

79+
// MARK: - Helper Methods
80+
81+
extension Datastore {
82+
func updatedDescriptor(for instance: CodedType) throws -> DatastoreDescriptor {
83+
if let updatedDescriptor {
84+
return updatedDescriptor
85+
}
86+
87+
let descriptor = try DatastoreDescriptor(
88+
version: version,
89+
sampleInstance: instance,
90+
identifierType: IdentifierType.self,
91+
directIndexes: directIndexes,
92+
computedIndexes: computedIndexes
93+
)
94+
updatedDescriptor = descriptor
95+
return descriptor
96+
}
97+
98+
func decoder(for version: Version) throws -> (_ data: Data) async throws -> CodedType {
99+
guard let decoder = decoders[version] else {
100+
throw DatastoreError.missingDecoder(version: String(describing: version))
101+
}
102+
return decoder
103+
}
104+
}
105+
77106
// MARK: - Warmup
78107

79108
extension Datastore {
@@ -242,11 +271,193 @@ extension Datastore {
242271
}
243272
}
244273

245-
// MARK: - Writting
274+
// MARK: - Writing
246275

247276
extension Datastore where AccessMode == ReadWrite {
277+
/// Persist an instance for a given identifier.
278+
///
279+
/// - Note: If you instance conforms to Identifiable, it it preferable to use ``persist(_:)`` instead.
248280
public func persist(_ instance: CodedType, to idenfifier: IdentifierType) async throws {
281+
try await warmupIfNeeded()
282+
283+
// TODO: Start an implicit transaction
284+
285+
/// Create any missing indexes or prime the datastore for writing.
286+
let updatedDescriptor = try updatedDescriptor(for: instance)
287+
try await persistence.apply(descriptor: updatedDescriptor, for: key)
288+
289+
let versionData = try Data(version)
290+
let instanceData = try await encoder(instance)
291+
292+
let existingEntry: (cursor: any InstanceCursorProtocol, instance: CodedType)? = try await {
293+
do {
294+
let existingEntry = try await persistence.primaryIndexCursor(for: idenfifier, datastoreKey: key)
295+
296+
let existingVersion = try Version(existingEntry.versionData)
297+
let decoder = try decoder(for: existingVersion)
298+
let existingInstance = try await decoder(existingEntry.instanceData)
299+
300+
return (cursor: existingEntry.cursor, instance: existingInstance)
301+
} catch PersistenceError.instanceNotFound {
302+
return nil
303+
} catch {
304+
throw error
305+
}
306+
}()
307+
308+
/// Grab the insertion cursor in the primary index.
309+
let existingInstance = existingEntry?.instance
310+
let insertionCursor: any InsertionCursorProtocol = try await {
311+
if let existingEntry { return existingEntry.cursor }
312+
return try await persistence.primaryIndexCursor(inserting: idenfifier, datastoreKey: key)
313+
}()
314+
315+
/// Persist the entry in the primary index
316+
try await persistence.persistPrimaryIndexEntry(
317+
versionData: versionData,
318+
identifierValue: idenfifier,
319+
instanceData: instanceData,
320+
cursor: insertionCursor,
321+
datastoreKey: key
322+
)
323+
324+
var queriedIndexes: Set<String> = []
325+
326+
/// Persist the direct indexes with full copies
327+
for indexPath in directIndexes {
328+
let indexName = indexPath.path
329+
guard !queriedIndexes.contains(indexName) else { continue }
330+
queriedIndexes.insert(indexName)
331+
332+
let existingValue = existingInstance?[keyPath: indexPath]
333+
let updatedValue = instance[keyPath: indexPath]
334+
// let indexType = updatedValue.indexedType
335+
336+
if let existingValue {
337+
/// Grab a cursor to the old value in the index.
338+
let existingValueCursor = try await persistence.directIndexCursor(
339+
for: existingValue.indexed,
340+
identifier: idenfifier,
341+
indexName: indexName,
342+
datastoreKey: key
343+
)
344+
345+
/// Delete it.
346+
try await persistence.deleteDirectIndexEntry(
347+
cursor: existingValueCursor.cursor,
348+
indexName: indexName,
349+
datastoreKey: key
350+
)
351+
}
352+
353+
/// Grab a cursor to insert the new value in the index.
354+
let updatedValueCursor = try await persistence.directIndexCursor(
355+
inserting: updatedValue.indexed,
356+
identifier: idenfifier,
357+
indexName: indexName,
358+
datastoreKey: key
359+
)
360+
361+
/// Insert it.
362+
try await persistence.persistDirectIndexEntry(
363+
versionData: versionData,
364+
indexValue: updatedValue.indexed,
365+
identifierValue: idenfifier,
366+
instanceData: instanceData,
367+
cursor: updatedValueCursor,
368+
indexName: indexName,
369+
datastoreKey: key
370+
)
371+
}
372+
373+
/// Next, go through any remaining computed indexes as secondary indexes.
374+
for indexPath in computedIndexes {
375+
let indexName = indexPath.path
376+
guard !queriedIndexes.contains(indexName) else { continue }
377+
queriedIndexes.insert(indexName)
378+
379+
let existingValue = existingInstance?[keyPath: indexPath]
380+
let updatedValue = instance[keyPath: indexPath]
381+
// let indexType = updatedValue.indexedType
382+
383+
if let existingValue {
384+
/// Grab a cursor to the old value in the index.
385+
let existingValueCursor = try await persistence.secondaryIndexCursor(
386+
for: existingValue.indexed,
387+
identifier: idenfifier,
388+
indexName: indexName,
389+
datastoreKey: key
390+
)
391+
392+
/// Delete it.
393+
try await persistence.deleteSecondaryIndexEntry(
394+
cursor: existingValueCursor,
395+
indexName: indexName,
396+
datastoreKey: key
397+
)
398+
}
399+
400+
/// Grab a cursor to insert the new value in the index.
401+
let updatedValueCursor = try await persistence.secondaryIndexCursor(
402+
inserting: updatedValue.indexed,
403+
identifier: idenfifier,
404+
indexName: indexName,
405+
datastoreKey: key
406+
)
407+
408+
/// Insert it.
409+
try await persistence.persistSecondaryIndexEntry(
410+
indexValue: updatedValue.indexed,
411+
identifierValue: idenfifier,
412+
cursor: updatedValueCursor,
413+
indexName: indexName,
414+
datastoreKey: key
415+
)
416+
}
417+
418+
/// Remove any remaining indexed values from the old instance.
419+
try await Mirror.indexedChildren(from: existingInstance) { indexName, value in
420+
guard !queriedIndexes.contains(indexName) else { return }
421+
422+
/// Grab a cursor to the old value in the index.
423+
let existingValueCursor = try await persistence.secondaryIndexCursor(
424+
for: value,
425+
identifier: idenfifier,
426+
indexName: indexName,
427+
datastoreKey: key
428+
)
429+
430+
/// Delete it.
431+
try await persistence.deleteSecondaryIndexEntry(
432+
cursor: existingValueCursor,
433+
indexName: indexName,
434+
datastoreKey: key
435+
)
436+
}
437+
438+
/// Re-insert those indexes from the new index.
439+
try await Mirror.indexedChildren(from: instance, assertIdentifiable: true) { indexName, value in
440+
guard !queriedIndexes.contains(indexName) else { return }
441+
442+
/// Grab a cursor to insert the new value in the index.
443+
let updatedValueCursor = try await persistence.secondaryIndexCursor(
444+
inserting: value,
445+
identifier: idenfifier,
446+
indexName: indexName,
447+
datastoreKey: key
448+
)
449+
450+
/// Insert it.
451+
try await persistence.persistSecondaryIndexEntry(
452+
indexValue: value,
453+
identifierValue: idenfifier,
454+
cursor: updatedValueCursor,
455+
indexName: indexName,
456+
datastoreKey: key
457+
)
458+
}
249459

460+
// TODO: Close implicit transaction
250461
}
251462

252463
public func delete(_ idenfifier: IdentifierType) async throws {
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//
2+
// DatastoreError.swift
3+
// CodableDatastore
4+
//
5+
// Created by Dimitri Bouniol on 2023-06-18.
6+
// Copyright © 2023 Mochi Development, Inc. All rights reserved.
7+
//
8+
9+
import Foundation
10+
11+
/// A ``Datastore``-specific error.
12+
public enum DatastoreError: LocalizedError {
13+
/// A decoder was missing for the specified version.
14+
case missingDecoder(version: String)
15+
16+
public var errorDescription: String? {
17+
switch self {
18+
case .missingDecoder(let version):
19+
return "The decoder for version \(version) is missing."
20+
}
21+
}
22+
}

Sources/CodableDatastore/Persistence/Disk Persistence/DiskPersistence.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,13 @@ extension DiskPersistence {
430430
return datastoreInfo.descriptor
431431
}
432432
}
433+
434+
public func apply(
435+
descriptor: DatastoreDescriptor,
436+
for datastoreKey: String
437+
) async throws {
438+
preconditionFailure("Unimplemented")
439+
}
433440
}
434441

435442
// MARK: - Cursor Lookups

Sources/CodableDatastore/Persistence/Memory Persistence/MemoryPersistence.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,13 @@ extension MemoryPersistence: _Persistence {
2525
preconditionFailure("Unimplemented")
2626
}
2727

28+
public func apply(
29+
descriptor: DatastoreDescriptor,
30+
for datastoreKey: String
31+
) async throws {
32+
preconditionFailure("Unimplemented")
33+
}
34+
2835
public func primaryIndexCursor<IdentifierType: Indexable>(
2936
for identifier: IdentifierType,
3037
datastoreKey: String

Sources/CodableDatastore/Persistence/Persistence.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ public protocol _Persistence {
3131
/// - Returns: A descriptor of the datastore as the persistence knows it.
3232
func datastoreDescriptor<Version, CodedType, IdentifierType, AccessMode>(for datastore: Datastore<Version, CodedType, IdentifierType, AccessMode>) async throws -> DatastoreDescriptor?
3333

34+
/// Apply a descriptor for a given datastore.
35+
///
36+
/// - Note: The persistence may choose which values to keep and which to ignore. For instance, an index version may be ignored if the index already exists.
37+
/// - Parameters:
38+
/// - descriptor: A descriptor of the Datastore as it should exist.
39+
/// - datastoreKey: The key of the datastore the descriptor belongs to.
40+
func apply(descriptor: DatastoreDescriptor, for datastoreKey: String) async throws
41+
3442
/// Load a cursor for the specified identifier in the primary index of the specified datastore key.
3543
///
3644
/// - Throws: ``PersistenceError/instanceNotFound`` if an instance for the specified identifier could not be found.

0 commit comments

Comments
 (0)