Skip to content

Commit 31e28d6

Browse files
Added disk persistence support for removing indexes
1 parent 83b3508 commit 31e28d6

File tree

5 files changed

+213
-14
lines changed

5 files changed

+213
-14
lines changed

README.md

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,12 @@ targets: [
4545
`CodableDatastore` is a collection of types that make it easy to interface with large data stores of independent types without loading the entire data store in memory.
4646

4747
> **Warning**
48-
> DO NOT USE THIS IN PRODUCTION PROJECTS. As this project is currently still in its alpha phase, I cannot stress how important it is to not ship anything that relies on this code, or you will experience data loss. There is a chance the underlying model may continue to change day to day, or I will not be able to ever finish it.
49-
> Until then, please enjoy the code as a spectator or play around with it in toy projects to submit feedback!
50-
51-
### Road to 0.1 Betas
52-
53-
As this project matures towards its first beta, a number of features still need to be fleshed out:
54-
- Index deletion
55-
56-
The above list will be kept up to date during development and will likely see additions during that process.
48+
> THINK CAREFULLY ABOUT USING THIS IN PRODUCTION PROJECTS. As this project only just entered its beta phase, I cannot stress how important it is to be very careful about shipping anything that relies on this code, as you may experience data loss migrating to a newer version. Although less likely, there is a chance the underlying model may change in an incompatible way that is not worth supporting with migrations.
49+
> Until then, please enjoy the code as a spectator or play around with it in toy projects to submit feedback! If you would like to be notified when `CodableDatastore` enters a production-ready state, please follow [#CodableDatastore](https://mastodon.social/tags/CodableDatastore) on Mastodon.
5750
5851
### Road to 1.0
5952

60-
Once an initial beta is released, the project will start focussing on the functionality and work listed below:
53+
As this project matures towards release, the project will focus on the functionality and work listed below:
6154
- Force migration methods
6255
- Composite indexes (via macros?)
6356
- Cleaning up old resources in memory
@@ -71,6 +64,8 @@ Once an initial beta is released, the project will start focussing on the functi
7164
- A memory persistence useful for testing apps with
7265
- A pre-configured data store tuned to storing pure Data, useful for types like Images
7366

67+
The above list will be kept up to date during development and will likely see additions during that process.
68+
7469
### Beyond 1.0
7570

7671
Once the 1.0 release has been made, it'll be time to start working on additional features:

Sources/CodableDatastore/Datastore/Dictionary+RawRepresentable.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,10 @@ extension Dictionary {
2222
yield &self[key.rawValue]
2323
}
2424
}
25+
26+
@discardableResult
27+
@usableFromInline
28+
mutating func removeValue(forKey key: some RawRepresentable<Key>) -> Value? {
29+
removeValue(forKey: key.rawValue)
30+
}
2531
}

Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreIndex.swift

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1399,11 +1399,51 @@ extension DiskPersistence.Datastore.Index {
13991399
}
14001400
}
14011401

1402-
if !createdPages.isEmpty {
1402+
if !createdPages.isEmpty || !removedPages.isEmpty {
14031403
manifest.id = newIndexID.manifestID
14041404
manifest.orderedPages = newOrderedPages
14051405
}
14061406

14071407
return (manifest: manifest, createdPages: createdPages, removedPages: removedPages)
14081408
}
1409+
1410+
func manifestDeletingAllEntries() async throws -> (
1411+
manifest: DatastoreIndexManifest,
1412+
removedPages: Set<DiskPersistence.Datastore.Page>
1413+
) {
1414+
var manifest = try await manifest
1415+
1416+
let newIndexID = id.with(manifestID: DatastoreIndexManifestIdentifier())
1417+
var removedPages: Set<DiskPersistence.Datastore.Page> = []
1418+
1419+
let originalOrderedPages = manifest.orderedPages
1420+
var newOrderedPages: [DatastoreIndexManifest.PageInfo] = []
1421+
newOrderedPages.reserveCapacity(originalOrderedPages.count)
1422+
1423+
for pageInfo in originalOrderedPages {
1424+
switch pageInfo {
1425+
case .removed:
1426+
/// Skip previously removed entries, unless this index is based on a transient index, and the removed entry was from before the transaction began.
1427+
if !isPersisted {
1428+
newOrderedPages.append(pageInfo)
1429+
}
1430+
continue
1431+
case .existing(let pageID), .added(let pageID):
1432+
let existingPage = await datastore.page(for: .init(index: self.id, page: pageID))
1433+
/// If the index had data on disk, or it existed prior to the transaction, mark it as removed.
1434+
/// Otherwise, simply skip the page, since we added it in a transient index.
1435+
removedPages.insert(existingPage)
1436+
if isPersisted || pageInfo.isExisting {
1437+
newOrderedPages.append(.removed(pageID))
1438+
}
1439+
}
1440+
}
1441+
1442+
if !removedPages.isEmpty {
1443+
manifest.id = newIndexID.manifestID
1444+
manifest.orderedPages = newOrderedPages
1445+
}
1446+
1447+
return (manifest: manifest, removedPages: removedPages)
1448+
}
14091449
}

Sources/CodableDatastore/Persistence/Disk Persistence/Datastore/DatastoreRoot.swift

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,5 +304,62 @@ extension DiskPersistence.Datastore.RootObject {
304304
}
305305
return updatedManifest
306306
}
307+
308+
func manifest(
309+
deleting index: DiskPersistence.Datastore.Index.ID
310+
) async throws -> DatastoreRootManifest {
311+
let manifest = try await manifest
312+
var updatedManifest = manifest
313+
314+
var removedIndex: DatastoreRootManifest.IndexManifestID
315+
var addedIndex: DatastoreRootManifest.IndexManifestID?
316+
317+
switch index {
318+
case .primary(let manifestID):
319+
removedIndex = .primary(manifest: manifestID)
320+
/// Primary index must have _a_ root, so make a new one.
321+
let newManifestID = DatastoreIndexManifestIdentifier()
322+
addedIndex = .primary(manifest: newManifestID)
323+
updatedManifest.primaryIndexManifest = newManifestID
324+
case .direct(let indexID, let manifestID):
325+
removedIndex = .direct(index: indexID, manifest: manifestID)
326+
if let entryIndex = updatedManifest.directIndexManifests.firstIndex(where: { $0.id == indexID }) {
327+
let indexName = updatedManifest.directIndexManifests[entryIndex].key
328+
updatedManifest.directIndexManifests.remove(at: entryIndex)
329+
updatedManifest.descriptor.directIndexes.removeValue(forKey: indexName)
330+
}
331+
case .secondary(let indexID, let manifestID):
332+
removedIndex = .secondary(index: indexID, manifest: manifestID)
333+
if let entryIndex = updatedManifest.secondaryIndexManifests.firstIndex(where: { $0.id == indexID }) {
334+
let indexName = updatedManifest.secondaryIndexManifests[entryIndex].key
335+
updatedManifest.secondaryIndexManifests.remove(at: entryIndex)
336+
updatedManifest.descriptor.secondaryIndexes.removeValue(forKey: indexName)
337+
}
338+
}
339+
340+
if manifest != updatedManifest {
341+
let modificationDate = Date()
342+
updatedManifest.id = DatastoreRootIdentifier(date: modificationDate)
343+
updatedManifest.modificationDate = modificationDate
344+
345+
if isPersisted {
346+
updatedManifest.removedIndexes = []
347+
updatedManifest.removedIndexManifests = []
348+
updatedManifest.addedIndexes = []
349+
updatedManifest.addedIndexManifests = []
350+
}
351+
352+
if updatedManifest.addedIndexManifests.contains(removedIndex) {
353+
updatedManifest.addedIndexManifests.remove(removedIndex)
354+
} else {
355+
updatedManifest.removedIndexManifests.insert(removedIndex)
356+
}
357+
358+
if let addedIndex {
359+
updatedManifest.addedIndexManifests.insert(addedIndex)
360+
}
361+
}
362+
return updatedManifest
363+
}
307364
}
308365

Sources/CodableDatastore/Persistence/Disk Persistence/Transaction/Transaction.swift

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1003,7 +1003,54 @@ extension DiskPersistence.Transaction {
10031003
) async throws {
10041004
try checkIsActive()
10051005

1006-
preconditionFailure("Unimplemented")
1006+
guard let existingRootObject = try await rootObject(for: datastoreKey)
1007+
else { throw DatastoreInterfaceError.datastoreKeyNotFound }
1008+
1009+
let existingIndex = try await existingRootObject.primaryIndex
1010+
1011+
let datastore = existingRootObject.datastore
1012+
1013+
let (indexManifest, removedPages) = try await existingIndex.manifestDeletingAllEntries()
1014+
1015+
/// No change occured, bail early
1016+
guard existingIndex.id.manifestID != indexManifest.id else { return }
1017+
1018+
deletedPages.formUnion(removedPages)
1019+
1020+
let newIndex = DiskPersistence.Datastore.Index(
1021+
datastore: datastore,
1022+
id: existingIndex.id.with(manifestID: indexManifest.id),
1023+
manifest: indexManifest
1024+
)
1025+
createdIndexes.insert(newIndex)
1026+
if createdIndexes.contains(existingIndex) {
1027+
createdIndexes.insert(existingIndex)
1028+
} else {
1029+
deletedIndexes.insert(existingIndex)
1030+
}
1031+
await datastore.adopt(index: newIndex)
1032+
1033+
var rootManifest = try await existingRootObject.manifest(replacing: newIndex.id)
1034+
1035+
/// Reset the number of entries we are managing.
1036+
rootManifest.descriptor.size = 0
1037+
1038+
/// No change occured, bail early
1039+
guard existingRootObject.id != rootManifest.id else { return }
1040+
1041+
let newRootObject = DiskPersistence.Datastore.RootObject(
1042+
datastore: existingRootObject.datastore,
1043+
id: rootManifest.id,
1044+
rootObject: rootManifest
1045+
)
1046+
createdRootObjects.insert(newRootObject)
1047+
if createdRootObjects.contains(existingRootObject) {
1048+
createdRootObjects.remove(existingRootObject)
1049+
} else {
1050+
deletedRootObjects.insert(existingRootObject)
1051+
}
1052+
await datastore.adopt(rootObject: newRootObject)
1053+
rootObjects[datastoreKey] = newRootObject
10071054
}
10081055

10091056
func persistDirectIndexEntry<IndexType: Indexable, IdentifierType: Indexable>(
@@ -1060,7 +1107,34 @@ extension DiskPersistence.Transaction {
10601107
) async throws {
10611108
try checkIsActive()
10621109

1063-
preconditionFailure("Unimplemented")
1110+
guard let existingRootObject = try await rootObject(for: datastoreKey)
1111+
else { throw DatastoreInterfaceError.datastoreKeyNotFound }
1112+
1113+
guard let existingIndex = try await existingRootObject.directIndexes[indexName]
1114+
else { throw DatastoreInterfaceError.indexNotFound }
1115+
1116+
let datastore = existingRootObject.datastore
1117+
1118+
deletedIndexes.insert(existingIndex)
1119+
1120+
let rootManifest = try await existingRootObject.manifest(deleting: existingIndex.id)
1121+
1122+
/// No change occured, bail early
1123+
guard existingRootObject.id != rootManifest.id else { return }
1124+
1125+
let newRootObject = DiskPersistence.Datastore.RootObject(
1126+
datastore: existingRootObject.datastore,
1127+
id: rootManifest.id,
1128+
rootObject: rootManifest
1129+
)
1130+
createdRootObjects.insert(newRootObject)
1131+
if createdRootObjects.contains(existingRootObject) {
1132+
createdRootObjects.remove(existingRootObject)
1133+
} else {
1134+
deletedRootObjects.insert(existingRootObject)
1135+
}
1136+
await datastore.adopt(rootObject: newRootObject)
1137+
rootObjects[datastoreKey] = newRootObject
10641138
}
10651139

10661140
func persistSecondaryIndexEntry<IndexType: Indexable, IdentifierType: Indexable>(
@@ -1113,7 +1187,34 @@ extension DiskPersistence.Transaction {
11131187
) async throws {
11141188
try checkIsActive()
11151189

1116-
preconditionFailure("Unimplemented")
1190+
guard let existingRootObject = try await rootObject(for: datastoreKey)
1191+
else { throw DatastoreInterfaceError.datastoreKeyNotFound }
1192+
1193+
guard let existingIndex = try await existingRootObject.secondaryIndexes[indexName]
1194+
else { throw DatastoreInterfaceError.indexNotFound }
1195+
1196+
let datastore = existingRootObject.datastore
1197+
1198+
deletedIndexes.insert(existingIndex)
1199+
1200+
let rootManifest = try await existingRootObject.manifest(deleting: existingIndex.id)
1201+
1202+
/// No change occured, bail early
1203+
guard existingRootObject.id != rootManifest.id else { return }
1204+
1205+
let newRootObject = DiskPersistence.Datastore.RootObject(
1206+
datastore: existingRootObject.datastore,
1207+
id: rootManifest.id,
1208+
rootObject: rootManifest
1209+
)
1210+
createdRootObjects.insert(newRootObject)
1211+
if createdRootObjects.contains(existingRootObject) {
1212+
createdRootObjects.remove(existingRootObject)
1213+
} else {
1214+
deletedRootObjects.insert(existingRootObject)
1215+
}
1216+
await datastore.adopt(rootObject: newRootObject)
1217+
rootObjects[datastoreKey] = newRootObject
11171218
}
11181219
}
11191220

0 commit comments

Comments
 (0)