Просмотр исходного кода

Dedupe pump events via batched find-or-create + constraint backstop

Replace the per-event duplicate fetch in storePumpEvents with a single
batched fetch (timestamp IN ...) plus an in-memory (timestamp, type)
lookup, eliminating the N+1 query. Newly created events are tracked in
the same map so within-batch duplicates are caught too.

Change the PumpEventStored uniqueness constraint to (timestamp, type)
so it actually acts as a race-safe backstop; the previous (id) composite never fired because id is a fresh UUID per insert. id is kept
as its own constraint.
Marvin Polscheit 5 дней назад
Родитель
Сommit
e929893aa2

+ 5 - 1
Model/TrioCoreDataPersistentContainer.xcdatamodeld/TrioCoreDataPersistentContainer.xcdatamodel/contents

@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
-<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="25B78" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
+<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24837" systemVersion="25D125" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithSwiftData="YES" userDefinedModelVersionIdentifier="">
     <entity name="BolusStored" representedClassName="BolusStored" syncable="YES">
         <attribute name="amount" optional="YES" attributeType="Decimal" defaultValueString="0"/>
         <attribute name="isExternal" optional="YES" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
@@ -215,6 +215,10 @@
             <uniquenessConstraint>
                 <constraint value="id"/>
             </uniquenessConstraint>
+            <uniquenessConstraint>
+                <constraint value="timestamp"/>
+                <constraint value="type"/>
+            </uniquenessConstraint>
         </uniquenessConstraints>
     </entity>
     <entity name="StatsData" representedClassName="StatsData" syncable="YES">

+ 71 - 115
Trio/Sources/APS/Storage/PumpHistoryStorage.swift

@@ -50,50 +50,81 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
         let context = makeContext()
         context.name = "storePumpEvents"
         try await context.perform {
-            for event in events {
-                let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
-                    ofType: PumpEventStored.self,
-                    onContext: context,
-                    predicate: NSPredicate.duplicates(event.date),
-                    key: "timestamp",
-                    ascending: false,
-                    batchSize: 50
-                ) as? [PumpEventStored] ?? []
+            // Batched de-duplication: fetch every already-stored event whose timestamp matches one of the
+            // incoming events in a single request, instead of one fetch per event (avoids an N+1 query).
+            // The (timestamp, type) key mirrors the uniqueness constraint on PumpEventStored, which acts as
+            // a race-safe backstop should a concurrent context insert the same event.
+            let incomingTimestamps = events.map { $0.date as NSDate }
+            let existingEvents: [PumpEventStored] = try CoreDataStack.shared.fetchEntities(
+                ofType: PumpEventStored.self,
+                onContext: context,
+                predicate: NSPredicate(format: "timestamp IN %@", incomingTimestamps),
+                key: "timestamp",
+                ascending: false,
+                batchSize: 50
+            ) as? [PumpEventStored] ?? []
+
+            // (timestamp, type) -> stored event, for O(1) look-up inside the loop. Newly created events are
+            // inserted here too, so duplicates within the same batch are caught as well.
+            func dedupKey(timestamp: Date?, type: String?) -> String {
+                "\(timestamp?.timeIntervalSinceReferenceDate ?? 0)|\(type ?? "")"
+            }
+            var existingByKey: [String: PumpEventStored] = [:]
+            for stored in existingEvents {
+                existingByKey[dedupKey(timestamp: stored.timestamp, type: stored.type)] = stored
+            }
 
+            // Creates a bare PumpEventStored unless a (timestamp, type) duplicate already exists.
+            // Returns the new event, or nil if it was a duplicate.
+            @discardableResult func makeEventIfNew(timestamp: Date, type: PumpEvent) -> PumpEventStored? {
+                let key = dedupKey(timestamp: timestamp, type: type.rawValue)
+                guard existingByKey[key] == nil else {
+                    debug(.coreData, "Duplicate event found with timestamp: \(timestamp)")
+                    return nil
+                }
+                let newPumpEvent = PumpEventStored(context: context)
+                newPumpEvent.id = UUID().uuidString
+                newPumpEvent.timestamp = timestamp
+                newPumpEvent.type = type.rawValue
+                newPumpEvent.isUploadedToNS = false
+                newPumpEvent.isUploadedToHealth = false
+                newPumpEvent.isUploadedToTidepool = false
+                existingByKey[key] = newPumpEvent
+                return newPumpEvent
+            }
+
+            for event in events {
                 switch event.type {
                 case .bolus:
-
                     guard let dose = event.dose else { continue }
                     let amount = self.roundDose(
                         dose.unitsInDeliverableIncrements,
                         toIncrement: Double(self.settings.preferences.bolusIncrement)
                     )
+                    // restrict entry to now or past
+                    let timestamp = event.date > Date() ? Date() : event.date
+                    let key = dedupKey(timestamp: timestamp, type: PumpEvent.bolus.rawValue)
 
-                    guard existingEvents.isEmpty else {
+                    if let existingEvent = existingByKey[key] {
                         // Duplicate found, do not store the event
                         debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
 
-                        if let existingEvent = existingEvents.first(where: { $0.type == EventType.bolus.rawValue }) {
-                            if existingEvent.timestamp == event.date {
-                                if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
-                                    // Update existing event with new smaller value
-                                    existingEvent.bolus?.amount = amount as NSDecimalNumber
-                                    existingEvent.bolus?.isSMB = dose.automatic ?? true
-                                    existingEvent.isUploadedToNS = false
-                                    existingEvent.isUploadedToHealth = false
-                                    existingEvent.isUploadedToTidepool = false
-
-                                    debug(.coreData, "Updated existing event with smaller value: \(amount)")
-                                }
-                            }
+                        if let existingAmount = existingEvent.bolus?.amount, amount < existingAmount as Decimal {
+                            // Update existing event with new smaller value (e.g. a cancelled / partial bolus)
+                            existingEvent.bolus?.amount = amount as NSDecimalNumber
+                            existingEvent.bolus?.isSMB = dose.automatic ?? true
+                            existingEvent.isUploadedToNS = false
+                            existingEvent.isUploadedToHealth = false
+                            existingEvent.isUploadedToTidepool = false
+
+                            debug(.coreData, "Updated existing event with smaller value: \(amount)")
                         }
                         continue
                     }
 
                     let newPumpEvent = PumpEventStored(context: context)
                     newPumpEvent.id = UUID().uuidString
-                    // restrict entry to now or past
-                    newPumpEvent.timestamp = event.date > Date() ? Date() : event.date
+                    newPumpEvent.timestamp = timestamp
                     newPumpEvent.type = PumpEvent.bolus.rawValue
                     newPumpEvent.isUploadedToNS = false
                     newPumpEvent.isUploadedToHealth = false
@@ -105,30 +136,21 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     newBolusEntry.isExternal = dose.manuallyEntered
                     newBolusEntry.isSMB = dose.automatic ?? true
 
+                    existingByKey[key] = newPumpEvent
+
                 case .tempBasal:
                     guard let dose = event.dose else { continue }
 
-                    guard existingEvents.isEmpty else {
-                        // Duplicate found, do not store the event
-                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
+                    let delivered = dose.deliveredUnits
+                    let isCancel = delivered != nil
+                    guard !isCancel else { continue }
+
+                    guard let newPumpEvent = makeEventIfNew(timestamp: event.date, type: .tempBasal) else {
                         continue
                     }
 
                     let rate = Decimal(dose.unitsPerHour)
                     let minutes = (dose.endDate - dose.startDate).timeInterval / 60
-                    let delivered = dose.deliveredUnits
-                    let date = event.date
-
-                    let isCancel = delivered != nil
-                    guard !isCancel else { continue }
-
-                    let newPumpEvent = PumpEventStored(context: context)
-                    newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = date
-                    newPumpEvent.type = PumpEvent.tempBasal.rawValue
-                    newPumpEvent.isUploadedToNS = false
-                    newPumpEvent.isUploadedToHealth = false
-                    newPumpEvent.isUploadedToTidepool = false
 
                     let newTempBasal = TempBasalStored(context: context)
                     newTempBasal.pumpEvent = newPumpEvent
@@ -137,90 +159,24 @@ final class BasePumpHistoryStorage: PumpHistoryStorage, Injectable {
                     newTempBasal.tempType = TempType.absolute.rawValue
 
                 case .suspend:
-                    guard existingEvents.isEmpty else {
-                        // Duplicate found, do not store the event
-                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
-                        continue
-                    }
-                    let newPumpEvent = PumpEventStored(context: context)
-                    newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = event.date
-                    newPumpEvent.type = PumpEvent.pumpSuspend.rawValue
-                    newPumpEvent.isUploadedToNS = false
-                    newPumpEvent.isUploadedToHealth = false
-                    newPumpEvent.isUploadedToTidepool = false
+                    makeEventIfNew(timestamp: event.date, type: .pumpSuspend)
 
                 case .resume:
-                    guard existingEvents.isEmpty else {
-                        // Duplicate found, do not store the event
-                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
-                        continue
-                    }
-                    let newPumpEvent = PumpEventStored(context: context)
-                    newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = event.date
-                    newPumpEvent.type = PumpEvent.pumpResume.rawValue
-                    newPumpEvent.isUploadedToNS = false
-                    newPumpEvent.isUploadedToHealth = false
-                    newPumpEvent.isUploadedToTidepool = false
+                    makeEventIfNew(timestamp: event.date, type: .pumpResume)
 
                 case .rewind:
-                    guard existingEvents.isEmpty else {
-                        // Duplicate found, do not store the event
-                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
-                        continue
-                    }
-                    let newPumpEvent = PumpEventStored(context: context)
-                    newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = event.date
-                    newPumpEvent.type = PumpEvent.rewind.rawValue
-                    newPumpEvent.isUploadedToNS = false
-                    newPumpEvent.isUploadedToHealth = false
-                    newPumpEvent.isUploadedToTidepool = false
+                    makeEventIfNew(timestamp: event.date, type: .rewind)
 
                 case .prime:
-                    guard existingEvents.isEmpty else {
-                        // Duplicate found, do not store the event
-                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
-                        continue
-                    }
-                    let newPumpEvent = PumpEventStored(context: context)
-                    newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = event.date
-                    newPumpEvent.type = PumpEvent.prime.rawValue
-                    newPumpEvent.isUploadedToNS = false
-                    newPumpEvent.isUploadedToHealth = false
-                    newPumpEvent.isUploadedToTidepool = false
+                    makeEventIfNew(timestamp: event.date, type: .prime)
 
                 case .alarm:
-                    guard existingEvents.isEmpty else {
-                        // Duplicate found, do not store the event
-                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
-                        continue
-                    }
-                    let newPumpEvent = PumpEventStored(context: context)
-                    newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = event.date
-                    newPumpEvent.type = PumpEvent.pumpAlarm.rawValue
-                    newPumpEvent.isUploadedToNS = false
-                    newPumpEvent.isUploadedToHealth = false
-                    newPumpEvent.isUploadedToTidepool = false
-                    newPumpEvent.note = event.title
+                    let newPumpEvent = makeEventIfNew(timestamp: event.date, type: .pumpAlarm)
+                    newPumpEvent?.note = event.title
 
                 case .replaceComponent(componentType: .infusionSet),
                      .replaceComponent(componentType: .pump):
-                    guard existingEvents.isEmpty else {
-                        // Duplicate found, do not store the event
-                        debug(.coreData, "Duplicate event found with timestamp: \(event.date)")
-                        continue
-                    }
-                    let newPumpEvent = PumpEventStored(context: context)
-                    newPumpEvent.id = UUID().uuidString
-                    newPumpEvent.timestamp = event.date
-                    newPumpEvent.type = PumpEvent.siteChange.rawValue
-                    newPumpEvent.isUploadedToNS = false
-                    newPumpEvent.isUploadedToHealth = false
-                    newPumpEvent.isUploadedToTidepool = false
+                    makeEventIfNew(timestamp: event.date, type: .siteChange)
 
                 default:
                     continue