Store & Search

Keep collection data in sync.

Replace complete datasets, apply individual changes, or synchronize SwiftData and other durable sources.

Choose How Your App Supplies Records

Your app remains the source of truth. Primordial keeps a searchable projection of that data and does not replace your app database.

App data Use
A complete in-memory dataset replaceAll(with:)
Explicit inserts, updates, and removals apply(_:)
A durable database or another paged source synchronize(from:options:)
SwiftData models PrimordialSwiftDataSource

Replace a Complete Dataset

Use replaceAll(with:) when your app already has the complete set of records. Records omitted from the new snapshot are removed from the collection.

let currentRecords = [record]

for try await event in notes.replaceAll(with: currentRecords) {
    switch event {
    case .progress(let progress):
        print(progress.phase, progress.changedRecords)
    case .completed(let report):
        print(report.generatedEmbeddings)
        print(report.reusedEmbeddings)
    }
}

Search continues to use the previous complete snapshot until the replacement commits. A failed or cancelled replacement does not expose a partial dataset, and unchanged text can reuse its embedding.

Apply Individual Changes

Use apply(_:) when your app already knows which complete records changed. Update your source of truth first, then apply an upsert or removal to its searchable projection.

let changes: [PrimordialRecordChange] = [
    .upsert(record),
    .remove(id: "old-record")
]

for try await event in notes.apply(changes) {
    if case .completed(let report) = event {
        print(report.changedRecords, report.removedRecords)
    }
}

Partial field updates are intentionally not part of the collection API. Change the application record, then pass its complete current fields with .upsert.

Synchronize SwiftData

Use PrimordialSwiftDataSource to map a SwiftData model into searchable records. Mark the stable record ID with .preserveValueOnDeletion so removals can be read from SwiftData history.

import Primordial
import SwiftData

@Model
final class Note {
    @Attribute(.unique, .preserveValueOnDeletion)
    var id: UUID

    var title: String
    var body: String
    var modifiedAt: Date
}
let source = try PrimordialSwiftDataSource<Note>(
    identifier: "app.notes",
    schemaVersion: "1",
    container: modelContainer,
    recordID: \Note.id,
    encodeID: { $0.uuidString }
) { note in
    [
        "title": .text(note.title),
        "body": .text(note.body),
        "modifiedAt": .date(note.modifiedAt)
    ]
}

for try await event in notes.synchronize(from: source) {
    switch event {
    case .progress(let progress):
        print(progress.phase, progress.changedRecords)
    case .completed(let report):
        print(report.changedRecords, report.removedRecords)
    }
}

Continue using your normal ModelContext and @Query code. Primordial reads the supplied ModelContainer, creates the first snapshot, and uses saved history checkpoints on later synchronizations.

  • Keep identifier stable for the same logical data source.
  • Increase schemaVersion when the record or field mapping changes meaning.
  • The encoded record ID must be nonempty and unique.
  • Filtered SwiftData fetch descriptors are not supported.

Choose a Synchronization Mode

let options = PrimordialSynchronizationOptions(
    mode: .automatic,
    batchSize: 100
)

for try await _ in notes.synchronize(
    from: source,
    options: options
) { }
Mode Behavior
.automatic Uses incremental changes when possible and falls back to a complete snapshot when required.
.incremental Requires a valid saved checkpoint and fails instead of rebuilding automatically.
.snapshot Forces a complete snapshot from the source.

The default batch size is 100. Valid values are 1...1000; Primordial may reduce work per batch when the device is under resource pressure.

Inspect Synchronization Status

let status = try await notes.synchronizationStatus()

print(status.sourceIdentifier)
print(status.sourceSchemaVersion)
print(status.hasCheckpoint)
print(status.requiresSnapshot)
print(status.lastCompletedAt)
print(status.lastReport)

Use the status for diagnostics or application UI. Your app decides when to synchronize—for example, after model availability, after committing meaningful source changes, when the app becomes active, or from an app-owned background task when the system grants execution time.

Reset or Change the Source

A synchronized collection stays bound to its source identifier. Reset it before binding that collection name to a different logical source.

try await notes.reset()

Reset removes the collection's searchable records, synchronization checkpoint, and source binding.