From 455df18dadc18a44a733f2fe9d724dec017a7dda Mon Sep 17 00:00:00 2001 From: Trey t Date: Tue, 21 Apr 2026 22:33:23 -0500 Subject: [PATCH] Stop silent save failures from locking out textbook re-seeds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- Conjuga/Conjuga/Services/DataLoader.swift | 38 ++++++++++++++++++----- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Conjuga/Conjuga/Services/DataLoader.swift b/Conjuga/Conjuga/Services/DataLoader.swift index cafa1e8..b7808c7 100644 --- a/Conjuga/Conjuga/Services/DataLoader.swift +++ b/Conjuga/Conjuga/Services/DataLoader.swift @@ -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()) { + for chapter in existing { context.delete(chapter) } + } let deckDescriptor = FetchDescriptor( predicate: #Predicate { $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())) ?? 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) {