fix FK constraint: skip duplicate messages before thread reconstruction

- insertMessages checks (mailboxId, uid) existence before INSERT, returns
  only actually inserted records
- syncMailbox only runs ThreadReconstructor on newly inserted messages,
  preventing FK violations from stale UUIDs referencing ignored records
- improve error logging to show full error description

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 12:02:55 +01:00
parent ff91e397e8
commit 01605a01ec
3 changed files with 23 additions and 11 deletions

View File

@@ -207,10 +207,10 @@ final class MailViewModel {
try await coordinator.syncNow()
syncState = coordinator.syncState
} catch {
let desc = "\(type(of: error)): \(error.localizedDescription)"
let desc = "\(error)"
print("[MailViewModel] syncNow failed: \(desc)")
errorMessage = desc
syncState = .error(desc)
errorMessage = error.localizedDescription
syncState = .error(error.localizedDescription)
}
}

View File

@@ -55,12 +55,23 @@ public final class MailStore: Sendable {
// MARK: - Messages
public func insertMessages(_ messages: [MessageRecord]) throws {
/// Insert messages, skipping duplicates by (mailboxId, uid). Returns only the records that were actually inserted.
@discardableResult
public func insertMessages(_ messages: [MessageRecord]) throws -> [MessageRecord] {
try dbWriter.write { db in
var inserted: [MessageRecord] = []
for message in messages {
// INSERT OR IGNORE: skip if (mailboxId, uid) already exists from a prior sync
try message.insert(db, onConflict: .ignore)
// Check if a message with this (mailboxId, uid) already exists
let exists = try MessageRecord.fetchOne(db, sql:
"SELECT id FROM message WHERE mailboxId = ? AND uid = ?",
arguments: [message.mailboxId, message.uid]
)
if exists == nil {
try message.insert(db)
inserted.append(message)
}
}
return inserted
}
}

View File

@@ -163,12 +163,13 @@ public final class SyncCoordinator {
let records = envelopes.map { envelope -> MessageRecord in
envelopeToRecord(envelope, accountId: accountConfig.id, mailboxId: mailboxId)
}
try store.insertMessages(records)
let inserted = try store.insertMessages(records)
let reconstructor = ThreadReconstructor(store: store)
try reconstructor.processMessages(records)
emit(.newMessages(count: envelopes.count, mailbox: remoteMailbox.name))
if !inserted.isEmpty {
let reconstructor = ThreadReconstructor(store: store)
try reconstructor.processMessages(inserted)
emit(.newMessages(count: inserted.count, mailbox: remoteMailbox.name))
}
}
// Reconcile flags for recent existing messages (read/unread state from other devices)