Skip to content

Commit 21343f3

Browse files
Fixed an issue where the incorrect cursor may be returned when the next entry spans multiple pages
1 parent bafb2de commit 21343f3

File tree

2 files changed

+100
-6
lines changed

2 files changed

+100
-6
lines changed

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ extension DiskPersistence.Datastore.Index {
580580

581581
var bytesForEntry: Bytes?
582582
var isEntryComplete = false
583-
var previousBlock: DiskPersistence.CursorBlock? = nil
583+
var previousCompleteBlock: DiskPersistence.CursorBlock? = nil
584584
var currentBlock: DiskPersistence.CursorBlock? = nil
585585
var pageIndex = startingPageIndex
586586

@@ -613,8 +613,15 @@ extension DiskPersistence.Datastore.Index {
613613
/// In the final position, lets save and continue.
614614
bytesForEntry?.append(contentsOf: bytes)
615615
case .tail(let bytes):
616-
/// In the first position, lets skip it.
617-
guard bytesForEntry != nil else { continue }
616+
/// In the first position, lets skip it, but add it as the last complete block in case the next one is a step too far.
617+
guard bytesForEntry != nil else {
618+
previousCompleteBlock = DiskPersistence.CursorBlock(
619+
pageIndex: pageIndex,
620+
page: page,
621+
blockIndex: blockIndex
622+
)
623+
continue
624+
}
618625
/// In the final position, lets save and stop.
619626
bytesForEntry?.append(contentsOf: bytes)
620627
isEntryComplete = true
@@ -625,7 +632,14 @@ extension DiskPersistence.Datastore.Index {
625632
page: page,
626633
blockIndex: blockIndex
627634
)
628-
defer { previousBlock = currentBlock }
635+
636+
/// Make sure to only keep a reference to the end of the last complete block, so if we roll back, we'll have a valid cursor
637+
defer {
638+
switch block {
639+
case .complete, .tail: previousCompleteBlock = currentBlock
640+
case .head, .slice: break
641+
}
642+
}
629643

630644
if let bytes = bytesForEntry, isEntryComplete {
631645
let entry = try DatastorePageEntry(bytes: bytes, isPartial: false)
@@ -643,7 +657,7 @@ extension DiskPersistence.Datastore.Index {
643657
persistence: datastore.snapshot.persistence,
644658
datastore: datastore,
645659
index: self,
646-
insertAfter: previousBlock
660+
insertAfter: previousCompleteBlock
647661
)
648662
}
649663

@@ -658,7 +672,7 @@ extension DiskPersistence.Datastore.Index {
658672
persistence: datastore.snapshot.persistence,
659673
datastore: datastore,
660674
index: self,
661-
insertAfter: previousBlock
675+
insertAfter: previousCompleteBlock
662676
)
663677
}
664678
}

Tests/CodableDatastoreTests/DiskPersistenceDatastoreIndexTests.swift

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,66 @@ final class DiskPersistenceDatastoreIndexTests: XCTestCase {
7373
XCTAssertEqual(result, expectedIndex, file: file, line: line)
7474
}
7575

76+
func assertInsertionCursor(
77+
proposedEntry: UInt8,
78+
pages: [[DatastorePageEntryBlock]],
79+
pageIndex: Int?,
80+
blockIndex: Int?,
81+
file: StaticString = #filePath,
82+
line: UInt = #line
83+
) async throws {
84+
let persistence = DiskPersistence(readOnlyURL: temporaryStoreURL)
85+
let snapshot = Snapshot(
86+
id: .init(rawValue: "Snapshot"),
87+
persistence: persistence
88+
)
89+
let datastore = DiskPersistence.Datastore(
90+
id: .init(rawValue: "Datastore"),
91+
snapshot: snapshot
92+
)
93+
let index = DiskPersistence.Datastore.Index(
94+
datastore: datastore,
95+
id: .primary(manifest: .init(rawValue: "Index")),
96+
manifest: DatastoreIndexManifest(
97+
id: .init(rawValue: "Index"),
98+
orderedPages: pages.enumerated().map { (index, _) in
99+
.existing(.init(rawValue: "Page \(index)"))
100+
}
101+
)
102+
)
103+
104+
var pageLookup: [DatastorePageIdentifier : DiskPersistence<ReadOnly>.Datastore.Page] = [:]
105+
var pageInfos: [DatastoreIndexManifest.PageInfo] = []
106+
107+
for (index, blocks) in pages.enumerated() {
108+
let pageID = DatastorePageIdentifier(rawValue: "Page \(index)")
109+
let page = DiskPersistence<ReadOnly>.Datastore.Page(
110+
datastore: datastore,
111+
id: .init(
112+
index: .primary(manifest: .init(rawValue: "Index")),
113+
page: pageID
114+
),
115+
blocks: blocks
116+
)
117+
pageLookup[pageID] = page
118+
pageInfos.append(.existing(pageID))
119+
}
120+
121+
let result = try await index.insertionCursor(for: RangeBoundExpression.including(proposedEntry), in: pageInfos) { pageID in
122+
pageLookup[pageID]!
123+
} comparator: { lhs, rhs in
124+
lhs.sortOrder(comparedTo: rhs.headers[0][0], order: .ascending)
125+
}
126+
127+
XCTAssertTrue(result.persistence === persistence, file: file, line: line)
128+
XCTAssertTrue(result.datastore === datastore, file: file, line: line)
129+
XCTAssertEqual(result.index, index, file: file, line: line)
130+
XCTAssertEqual(result.insertAfter?.pageIndex, pageIndex, file: file, line: line)
131+
let pageID = await result.insertAfter?.page.id.page
132+
XCTAssertEqual(pageID, pageIndex.map({ DatastorePageIdentifier(rawValue: "Page \($0)") }), file: file, line: line)
133+
XCTAssertEqual(result.insertAfter?.blockIndex, blockIndex, file: file, line: line)
134+
}
135+
76136
func testEmptyPagesSearch() async throws {
77137
try await assertPageSearch(proposedEntry: 0, pages: [], expectedIndex: nil)
78138
try await assertPageSearch(proposedEntry: 0, pages: [], expectedIndex: nil)
@@ -330,4 +390,24 @@ final class DiskPersistenceDatastoreIndexTests: XCTestCase {
330390
wait(for: [exp], timeout: 200.0)
331391
}
332392
}
393+
394+
func testNonContiguousInsertionCursor() async throws {
395+
let entry1 = DatastorePageEntry(headers: [[1]], content: [1]).blocks(remainingPageSpace: 1024, maxPageSpace: 1024)
396+
let entry2 = DatastorePageEntry(headers: [[2]], content: [2]).blocks(remainingPageSpace: 1024, maxPageSpace: 1024)
397+
let entry3 = DatastorePageEntry(headers: [[3]], content: Array(repeating: 3, count: 100)).blocks(remainingPageSpace: 20, maxPageSpace: 60)
398+
let entry4 = DatastorePageEntry(headers: [[4]], content: [4]).blocks(remainingPageSpace: 1024, maxPageSpace: 1024)
399+
400+
let pages = [
401+
[entry1[0], entry2[0], entry3[0]],
402+
[entry3[1]],
403+
[entry3[2], entry4[0]]
404+
]
405+
406+
try await assertInsertionCursor(proposedEntry: 0, pages: pages, pageIndex: nil, blockIndex: nil)
407+
try await assertInsertionCursor(proposedEntry: 1, pages: pages, pageIndex: nil, blockIndex: nil)
408+
try await assertInsertionCursor(proposedEntry: 2, pages: pages, pageIndex: 0, blockIndex: 0)
409+
try await assertInsertionCursor(proposedEntry: 3, pages: pages, pageIndex: 0, blockIndex: 1)
410+
try await assertInsertionCursor(proposedEntry: 4, pages: pages, pageIndex: 2, blockIndex: 0)
411+
try await assertInsertionCursor(proposedEntry: 5, pages: pages, pageIndex: 2, blockIndex: 1)
412+
}
333413
}

0 commit comments

Comments
 (0)