Showing 2 changed files with 25 additions and 15 deletions
+1 -1
HealthProbe/Doc/04-project/IMPLEMENTATION_STATUS.md
@@ -25,7 +25,7 @@ There are no real deployments, only test installations. Existing prototype datab
25 25
 |------|----------------|--------------------|
26 26
 | Product docs | Updated | Keep `HealthProbe/Doc/README.md` as canonical index |
27 27
 | HealthKit capture | Capture now opens one archive observation per user-visible snapshot, attaches HealthKit pages, deleted-object evidence, and type verification to that observation id before finishing it, no longer aborts initial full-history imports after a fixed 30-minute wall-clock cap while page-level HealthKit timeouts remain in place, defers grouped observation summary/daily aggregate rebuilds until per-type verification instead of rebuilding after every imported page, and persists large HealthKit pages in smaller archive chunks while using type-specific import strategies: conservative paging for the heaviest metrics, more aggressive pages/chunks for ordinary metrics, adaptive write chunk sizing, batched deleted-object persistence, explicit task yields, and lower-allocation streaming loops to avoid long monolithic SQLite stalls | Continue moving UI/cache reads to archive-backed observation ids and revisit full checkpoint/resume and background collection separately |
28
-| SQLite archive | Archive v2 schema, snapshot-level observation grouping, differential write path, v2 verification/delete bookkeeping, daily aggregate rebuilds, integrity report, v2 record reads, SQL diff/count/aggregate/provenance/consolidation-evidence APIs, large synthetic diff pagination coverage, formal timing/memory metrics, and XCTest coverage are in place; the legacy `archive_samples` mirror has been removed, the hot write path now reuses prepared SQLite statements within grouped page writes instead of reparsing the same SQL for every sample, caches repeated sample-type/source/source-revision/device/metadata id lookups within grouped writes, skips redundant visibility close/existence checks when grouped imports create a brand-new sample or payload version, reuses verification aggregates instead of rescanning them twice, drives per-type finalize queries from sample-type-filtered sample ids, processes sample rows in a lower-allocation streaming loop, batches same-page deleted-object evidence in one transaction, adds composite indexes for visibility-range and sample-uuid hot lookups, and opens SQLite connections with import-friendly busy timeout / synchronous / temp-store pragmas | Continue moving capture/Dashboard actions to archive/cache DTOs |
28
+| SQLite archive | Archive v2 schema, snapshot-level observation grouping, differential write path, v2 verification/delete bookkeeping, daily aggregate rebuilds, integrity report, v2 record reads, SQL diff/count/aggregate/provenance/consolidation-evidence APIs, large synthetic diff pagination coverage, formal timing/memory metrics, and XCTest coverage are in place; the legacy `archive_samples` mirror has been removed, the hot write path now reuses prepared SQLite statements within grouped page writes instead of reparsing the same SQL for every sample, caches repeated sample-type/source/source-revision/device/metadata id lookups within grouped writes, skips redundant visibility close/existence checks when grouped imports create a brand-new sample or payload version, skips follow-up id lookup queries when SQLite confirms new sample/sample-version inserts, reuses verification aggregates instead of rescanning them twice, drives per-type finalize queries from sample-type-filtered sample ids, processes sample rows in a lower-allocation streaming loop, batches same-page deleted-object evidence in one transaction, adds composite indexes for visibility-range and sample-uuid hot lookups, and opens SQLite connections with import-friendly busy timeout / synchronous / temp-store pragmas | Continue moving capture/Dashboard actions to archive/cache DTOs |
29 29
 | Core Data cache | Initial programmatic Core Data model, full-cache rebuild service, read DTOs for observation/type/diff/health rows, and Dashboard archive-cache status wiring are in place | Move remaining export/report paths to cache DTOs and add targeted partial invalidation |
30 30
 | SwiftData cache | Exists; test builds now reset legacy prototype UI/archive/cache stores once for archive v2 so old SwiftData-only snapshots are not treated as backed-up observations. Metric timeout calibration, local device profile settings, operation logging, ContentView preview, Settings data maintenance, legacy detail/PDF views, unused legacy repair/observer services, Dashboard view/view-model access, and legacy anomaly/count-drop review have moved outside SwiftData or been removed. Remaining SwiftData imports are inventoried in [`SwiftData-Retirement-Inventory.md`](SwiftData-Retirement-Inventory.md) | Treat as disposable prototype data; stop returning/storing `HealthSnapshot` bridge handles before removing `ModelContainer` |
31 31
 | UI | Prototype exists; Dashboard status reads archive/cache observation rows and shows cache health, and Dashboard view/view-model code no longer imports SwiftData or reads `ModelContext`; capture/review actions now route through DTOs and snapshot ids, with the remaining legacy bridge isolated in `HealthKitService`. Snapshots and Data Types tab roots no longer import SwiftData, load Core Data cached observation rows, and open archive/cache-backed detail rows; `SnapshotArchiveDetailView` and `DataTypeArchiveDetailView` read Core Data type/diff summaries and page record drill-down through SQLite; unused legacy SwiftData snapshot/type detail and PDF views have been deleted; record-change evolution and temporal distribution screens now receive DTO rows/cache input instead of querying SwiftData directly; export preview reads the archive export API before showing/exporting JSON; simplified detail mode replaces heavy charts with summary rows on small/accessibility layouts or when enabled in Settings; visible change labels now use neutral new/missing/change-review language; Settings can now schedule a full test-database reset for the next app launch | Stop writing prototype `HealthSnapshot` bridge rows during capture/review |
+24 -14
HealthProbe/Services/SQLiteHealthArchiveStore.swift
@@ -2659,13 +2659,18 @@ actor SQLiteHealthArchiveStore: HealthArchiveStore {
2659 2659
             }
2660 2660
         }
2661 2661
         let inserted = sqlite3_changes(db) > 0
2662
-        let id = try requiredInt64(
2663
-            "SELECT id FROM samples WHERE sample_type_id = ? AND strict_fingerprint = ? LIMIT 1",
2664
-            db: db,
2665
-            statementCache: statementCache
2666
-        ) { statement in
2667
-            bindInt64(sampleTypeID, to: 1, in: statement)
2668
-            bindText(row.strictFingerprint, to: 2, in: statement)
2662
+        let id: Int64
2663
+        if inserted {
2664
+            id = sqlite3_last_insert_rowid(db)
2665
+        } else {
2666
+            id = try requiredInt64(
2667
+                "SELECT id FROM samples WHERE sample_type_id = ? AND strict_fingerprint = ? LIMIT 1",
2668
+                db: db,
2669
+                statementCache: statementCache
2670
+            ) { statement in
2671
+                bindInt64(sampleTypeID, to: 1, in: statement)
2672
+                bindText(row.strictFingerprint, to: 2, in: statement)
2673
+            }
2669 2674
         }
2670 2675
         return (id, inserted)
2671 2676
     }
@@ -2721,13 +2726,18 @@ actor SQLiteHealthArchiveStore: HealthArchiveStore {
2721 2726
             }
2722 2727
         }
2723 2728
         let inserted = sqlite3_changes(db) > 0
2724
-        let id = try requiredInt64(
2725
-            "SELECT id FROM sample_versions WHERE sample_id = ? AND payload_hash = ? LIMIT 1",
2726
-            db: db,
2727
-            statementCache: statementCache
2728
-        ) { statement in
2729
-            bindInt64(sampleID, to: 1, in: statement)
2730
-            bindText(row.payloadHash, to: 2, in: statement)
2729
+        let id: Int64
2730
+        if inserted {
2731
+            id = sqlite3_last_insert_rowid(db)
2732
+        } else {
2733
+            id = try requiredInt64(
2734
+                "SELECT id FROM sample_versions WHERE sample_id = ? AND payload_hash = ? LIMIT 1",
2735
+                db: db,
2736
+                statementCache: statementCache
2737
+            ) { statement in
2738
+                bindInt64(sampleID, to: 1, in: statement)
2739
+                bindText(row.payloadHash, to: 2, in: statement)
2740
+            }
2731 2741
         }
2732 2742
         return (id, inserted)
2733 2743
     }