Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d99d88e73c | |||
| 8e1c9b6bf1 |
@@ -0,0 +1,5 @@
|
||||
# Project rules
|
||||
|
||||
## Git
|
||||
|
||||
- **Never run `git commit` or `git push` without an explicit request from the user in the current turn.** File edits are fine; committing and pushing are not. Wait to be told.
|
||||
@@ -6,7 +6,7 @@ actor DataLoader {
|
||||
static let courseDataVersion = 7
|
||||
static let courseDataKey = "courseDataVersion"
|
||||
|
||||
static let textbookDataVersion = 11
|
||||
static let textbookDataVersion = 12
|
||||
static let textbookDataKey = "textbookDataVersion"
|
||||
|
||||
/// Quick check: does the DB need seeding or course data refresh?
|
||||
@@ -148,13 +148,24 @@ actor DataLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-seed textbook data if the version has changed.
|
||||
/// Re-seed textbook data if the version has changed OR if the rows are
|
||||
/// missing on disk. The row-count check exists because anything opening
|
||||
/// this store with a subset schema (e.g. an out-of-date widget extension)
|
||||
/// can destructively drop the rows without touching UserDefaults — so a
|
||||
/// pure version-flag trigger would leave us permanently empty.
|
||||
static func refreshTextbookDataIfNeeded(container: ModelContainer) async {
|
||||
let shared = UserDefaults.standard
|
||||
if shared.integer(forKey: textbookDataKey) >= textbookDataVersion { return }
|
||||
|
||||
print("Textbook data version outdated — re-seeding...")
|
||||
let context = ModelContext(container)
|
||||
let existingCount = (try? context.fetchCount(FetchDescriptor<TextbookChapter>())) ?? 0
|
||||
let versionCurrent = shared.integer(forKey: textbookDataKey) >= textbookDataVersion
|
||||
|
||||
if versionCurrent && existingCount > 0 { return }
|
||||
|
||||
if versionCurrent {
|
||||
print("Textbook data version current but store has \(existingCount) chapters — re-seeding...")
|
||||
} else {
|
||||
print("Textbook data version outdated — re-seeding...")
|
||||
}
|
||||
|
||||
// Fetch + delete individually instead of batch delete. SwiftData's
|
||||
// context.delete(model:) hits the store directly and doesn't always
|
||||
|
||||
@@ -26,12 +26,14 @@ enum StoreInspector {
|
||||
let hasZVERBFORM = tables.contains("ZVERBFORM")
|
||||
let hasZTENSEGUIDE = tables.contains("ZTENSEGUIDE")
|
||||
let hasZVOCABCARD = tables.contains("ZVOCABCARD")
|
||||
let hasZTEXTBOOKCHAPTER = tables.contains("ZTEXTBOOKCHAPTER")
|
||||
|
||||
var summary = "[StoreInspector:\(label)] \(tables.count) tables"
|
||||
summary += " | ZVERB=\(hasZVERB ? queryInt(db: db, sql: "SELECT COUNT(*) FROM ZVERB") : -1)"
|
||||
summary += " ZVERBFORM=\(hasZVERBFORM ? queryInt(db: db, sql: "SELECT COUNT(*) FROM ZVERBFORM") : -1)"
|
||||
summary += " ZTENSEGUIDE=\(hasZTENSEGUIDE ? queryInt(db: db, sql: "SELECT COUNT(*) FROM ZTENSEGUIDE") : -1)"
|
||||
summary += " ZVOCABCARD=\(hasZVOCABCARD ? queryInt(db: db, sql: "SELECT COUNT(*) FROM ZVOCABCARD") : -1)"
|
||||
summary += " ZTEXTBOOKCHAPTER=\(hasZTEXTBOOKCHAPTER ? queryInt(db: db, sql: "SELECT COUNT(*) FROM ZTEXTBOOKCHAPTER") : -1)"
|
||||
print(summary)
|
||||
|
||||
// Log all Z-tables (SwiftData entity tables start with Z, minus Core Data system tables)
|
||||
|
||||
@@ -41,14 +41,16 @@ struct CombinedProvider: TimelineProvider {
|
||||
private func fetchWordOfDay(for date: Date) -> WordOfDay? {
|
||||
guard let localURL = SharedStore.localStoreURL() else { return nil }
|
||||
|
||||
// MUST declare all 6 local entities to match the main app's schema.
|
||||
// Declaring a subset would cause SwiftData to destructively migrate the store
|
||||
// on open, dropping the entities not listed here.
|
||||
// MUST declare all 7 local entities to match the main app's schema.
|
||||
// Declaring a subset would cause SwiftData to destructively migrate the
|
||||
// store on open, dropping the entities not listed here (this is how we
|
||||
// previously lost all TextbookChapter rows on every widget refresh).
|
||||
let config = ModelConfiguration(
|
||||
"local",
|
||||
schema: Schema([
|
||||
Verb.self, VerbForm.self, IrregularSpan.self,
|
||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
||||
TextbookChapter.self,
|
||||
]),
|
||||
url: localURL,
|
||||
cloudKitDatabase: .none
|
||||
@@ -56,6 +58,7 @@ struct CombinedProvider: TimelineProvider {
|
||||
guard let container = try? ModelContainer(
|
||||
for: Verb.self, VerbForm.self, IrregularSpan.self,
|
||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
||||
TextbookChapter.self,
|
||||
configurations: config
|
||||
) else { return nil }
|
||||
|
||||
|
||||
@@ -32,14 +32,16 @@ struct WordOfDayProvider: TimelineProvider {
|
||||
private func fetchWordOfDay(for date: Date) -> WordOfDay? {
|
||||
guard let localURL = SharedStore.localStoreURL() else { return nil }
|
||||
|
||||
// MUST declare all 6 local entities to match the main app's schema.
|
||||
// Declaring a subset would cause SwiftData to destructively migrate the store
|
||||
// on open, dropping the entities not listed here.
|
||||
// MUST declare all 7 local entities to match the main app's schema.
|
||||
// Declaring a subset would cause SwiftData to destructively migrate the
|
||||
// store on open, dropping the entities not listed here (this is how we
|
||||
// previously lost all TextbookChapter rows on every widget refresh).
|
||||
let config = ModelConfiguration(
|
||||
"local",
|
||||
schema: Schema([
|
||||
Verb.self, VerbForm.self, IrregularSpan.self,
|
||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
||||
TextbookChapter.self,
|
||||
]),
|
||||
url: localURL,
|
||||
cloudKitDatabase: .none
|
||||
@@ -47,6 +49,7 @@ struct WordOfDayProvider: TimelineProvider {
|
||||
guard let container = try? ModelContainer(
|
||||
for: Verb.self, VerbForm.self, IrregularSpan.self,
|
||||
TenseGuide.self, CourseDeck.self, VocabCard.self,
|
||||
TextbookChapter.self,
|
||||
configurations: config
|
||||
) else { return nil }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user