Stop silent save failures from locking out textbook re-seeds
Two bugs were causing chapters to disappear on every relaunch: 1. seedTextbookData used `try? context.save()` (swallowing errors) and returned `inserted > 0`, so a failed save still reported success. Callers then bumped UserDefaults textbookDataVersion and subsequent launches skipped the re-seed entirely — with no rows on disk. 2. refreshTextbookDataIfNeeded wiped chapters via the batch-delete API `context.delete(model: TextbookChapter.self)`, which hits the store directly without clearing the context's .unique-id index. Re-inserting chapters with the same ids could then throw a unique-constraint error on save — also silently eaten by `try?`. Fixes: - seedTextbookData now uses do/catch around save(), returns false on error, and verifies persistence via fetchCount before returning true. - refreshTextbookDataIfNeeded fetches and deletes chapters individually so the context tracks the deletion cleanly; wipe save is also now checked and bails early on failure. - Bumped textbookDataVersion to 11 so devices poisoned by the previous silent-failure path retry on next launch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ actor DataLoader {
|
||||
static let courseDataVersion = 7
|
||||
static let courseDataKey = "courseDataVersion"
|
||||
|
||||
static let textbookDataVersion = 10
|
||||
static let textbookDataVersion = 11
|
||||
static let textbookDataKey = "textbookDataVersion"
|
||||
|
||||
/// Quick check: does the DB need seeding or course data refresh?
|
||||
@@ -156,17 +156,26 @@ actor DataLoader {
|
||||
print("Textbook data version outdated — re-seeding...")
|
||||
let context = ModelContext(container)
|
||||
|
||||
// Only wipe textbook chapters and our textbook-scoped CourseDecks
|
||||
// (not the LanGo decks, which live in the same tables).
|
||||
try? context.delete(model: TextbookChapter.self)
|
||||
// Fetch + delete individually instead of batch delete. SwiftData's
|
||||
// context.delete(model:) hits the store directly and doesn't always
|
||||
// clear the unique-constraint index before the reseed's save runs,
|
||||
// so re-inserting rows with the same .unique id can throw.
|
||||
let textbookCourseName = "Complete Spanish Step-by-Step"
|
||||
if let existing = try? context.fetch(FetchDescriptor<TextbookChapter>()) {
|
||||
for chapter in existing { context.delete(chapter) }
|
||||
}
|
||||
let deckDescriptor = FetchDescriptor<CourseDeck>(
|
||||
predicate: #Predicate<CourseDeck> { $0.courseName == textbookCourseName }
|
||||
)
|
||||
if let decks = try? context.fetch(deckDescriptor) {
|
||||
for deck in decks { context.delete(deck) }
|
||||
}
|
||||
try? context.save()
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print("[DataLoader] ERROR: textbook wipe save failed: \(error)")
|
||||
return
|
||||
}
|
||||
|
||||
if seedTextbookData(context: context) {
|
||||
shared.set(textbookDataVersion, forKey: textbookDataKey)
|
||||
@@ -480,14 +489,27 @@ actor DataLoader {
|
||||
inserted += 1
|
||||
}
|
||||
|
||||
try? context.save()
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
print("[DataLoader] ERROR: textbook chapter save failed: \(error)")
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify rows actually hit the store — guards against the case where
|
||||
// save returned cleanly but no rows were persisted.
|
||||
let persisted = (try? context.fetchCount(FetchDescriptor<TextbookChapter>())) ?? 0
|
||||
guard persisted > 0 else {
|
||||
print("[DataLoader] ERROR: textbook seeded \(inserted) chapters but persisted count is 0")
|
||||
return false
|
||||
}
|
||||
|
||||
// Seed textbook-derived vocabulary flashcards as CourseDecks so the
|
||||
// existing Course UI can surface them alongside LanGo decks.
|
||||
seedTextbookVocabDecks(context: context, courseName: courseName)
|
||||
|
||||
print("Textbook seeding complete: \(inserted) chapters")
|
||||
return inserted > 0
|
||||
print("Textbook seeding complete: \(inserted) chapters inserted, \(persisted) persisted")
|
||||
return true
|
||||
}
|
||||
|
||||
private static func seedTextbookVocabDecks(context: ModelContext, courseName: String) {
|
||||
|
||||
Reference in New Issue
Block a user