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 courseDataVersion = 7
|
||||||
static let courseDataKey = "courseDataVersion"
|
static let courseDataKey = "courseDataVersion"
|
||||||
|
|
||||||
static let textbookDataVersion = 10
|
static let textbookDataVersion = 11
|
||||||
static let textbookDataKey = "textbookDataVersion"
|
static let textbookDataKey = "textbookDataVersion"
|
||||||
|
|
||||||
/// Quick check: does the DB need seeding or course data refresh?
|
/// Quick check: does the DB need seeding or course data refresh?
|
||||||
@@ -156,17 +156,26 @@ actor DataLoader {
|
|||||||
print("Textbook data version outdated — re-seeding...")
|
print("Textbook data version outdated — re-seeding...")
|
||||||
let context = ModelContext(container)
|
let context = ModelContext(container)
|
||||||
|
|
||||||
// Only wipe textbook chapters and our textbook-scoped CourseDecks
|
// Fetch + delete individually instead of batch delete. SwiftData's
|
||||||
// (not the LanGo decks, which live in the same tables).
|
// context.delete(model:) hits the store directly and doesn't always
|
||||||
try? context.delete(model: TextbookChapter.self)
|
// 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"
|
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>(
|
let deckDescriptor = FetchDescriptor<CourseDeck>(
|
||||||
predicate: #Predicate<CourseDeck> { $0.courseName == textbookCourseName }
|
predicate: #Predicate<CourseDeck> { $0.courseName == textbookCourseName }
|
||||||
)
|
)
|
||||||
if let decks = try? context.fetch(deckDescriptor) {
|
if let decks = try? context.fetch(deckDescriptor) {
|
||||||
for deck in decks { context.delete(deck) }
|
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) {
|
if seedTextbookData(context: context) {
|
||||||
shared.set(textbookDataVersion, forKey: textbookDataKey)
|
shared.set(textbookDataVersion, forKey: textbookDataKey)
|
||||||
@@ -480,14 +489,27 @@ actor DataLoader {
|
|||||||
inserted += 1
|
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
|
// Seed textbook-derived vocabulary flashcards as CourseDecks so the
|
||||||
// existing Course UI can surface them alongside LanGo decks.
|
// existing Course UI can surface them alongside LanGo decks.
|
||||||
seedTextbookVocabDecks(context: context, courseName: courseName)
|
seedTextbookVocabDecks(context: context, courseName: courseName)
|
||||||
|
|
||||||
print("Textbook seeding complete: \(inserted) chapters")
|
print("Textbook seeding complete: \(inserted) chapters inserted, \(persisted) persisted")
|
||||||
return inserted > 0
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func seedTextbookVocabDecks(context: ModelContext, courseName: String) {
|
private static func seedTextbookVocabDecks(context: ModelContext, courseName: String) {
|
||||||
|
|||||||
Reference in New Issue
Block a user