Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions Sources/SQLiteData/CloudKit/SyncEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1181,6 +1181,7 @@
try await userDatabase.read { db in
result =
try T
.unscoped
.where {
#sql("\($0.primaryKey) = \(bind: metadata.recordPrimaryKey)")
}
Expand Down Expand Up @@ -1427,7 +1428,7 @@
else { continue }
func open<T: PrimaryKeyedTable>(_: some SynchronizableTable<T>) {
withErrorReporting(.sqliteDataCloudKitFailure) {
try T.where { #sql("\($0.primaryKey)").in(primaryKeys) }.delete().execute(db)
try T.unscoped.where { #sql("\($0.primaryKey)").in(primaryKeys) }.delete().execute(db)
}
}
open(table)
Expand All @@ -1449,7 +1450,7 @@
func open<T>(_: some SynchronizableTable<T>) {
withErrorReporting(.sqliteDataCloudKitFailure) {
pendingRecordZoneChanges.append(
contentsOf: try T.select(\._recordName).fetchAll(db).map {
contentsOf: try T.unscoped.select(\._recordName).fetchAll(db).map {
.saveRecord(CKRecord.ID(recordName: $0, zoneID: zoneID))
}
)
Expand Down Expand Up @@ -1483,6 +1484,7 @@
await withErrorReporting(.sqliteDataCloudKitFailure) {
try await userDatabase.write { db in
try T
.unscoped
.where {
#sql("\($0.primaryKey)").in(
SyncMetadata.findAll(recordIDs)
Expand Down Expand Up @@ -1708,6 +1710,7 @@
switch foreignKey.onDelete {
case .cascade:
try T
.unscoped
.where { #sql("\($0.primaryKey) = \(bind: recordPrimaryKey)") }
.delete()
.execute(db)
Expand Down Expand Up @@ -1767,6 +1770,7 @@
} catch let error as CKError where error.code == .unknownItem {
try await userDatabase.write { db in
try T
.unscoped
.where { #sql("\($0.primaryKey) = \(bind: recordPrimaryKey)") }
.delete()
.execute(db)
Expand Down Expand Up @@ -1949,7 +1953,7 @@
var columnNames: [String] = T.TableColumns.writableColumns.map(\.name)
if !force,
let allFields = metadata._lastKnownServerRecordAllFields,
let row = try T.find(#sql("\(bind: metadata.recordPrimaryKey)")).fetchOne(db)
let row = try T.unscoped.find(#sql("\(bind: metadata.recordPrimaryKey)")).fetchOne(db)
{
serverRecord.update(
with: allFields,
Expand Down
33 changes: 33 additions & 0 deletions Tests/SQLiteDataTests/CloudKitTests/CloudKitTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,39 @@
type: "TEXT"
)
]
),
[12]: RecordType(
tableName: "scopedModels",
schema: """
CREATE TABLE "scopedModels" (
"id" INT PRIMARY KEY NOT NULL,
"title" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT '',
"isDeleted" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0
) STRICT
""",
tableInfo: [
[0]: TableInfo(
defaultValue: nil,
isPrimaryKey: true,
name: "id",
isNotNull: true,
type: "INT"
),
[1]: TableInfo(
defaultValue: "0",
isPrimaryKey: false,
name: "isDeleted",
isNotNull: true,
type: "INTEGER"
),
[2]: TableInfo(
defaultValue: "\'\'",
isPrimaryKey: false,
name: "title",
isNotNull: true,
type: "TEXT"
)
]
)
]
"""#
Expand Down
33 changes: 33 additions & 0 deletions Tests/SQLiteDataTests/CloudKitTests/RecordTypeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,39 @@
type: "TEXT"
)
]
),
[12]: RecordType(
tableName: "scopedModels",
schema: """
CREATE TABLE "scopedModels" (
"id" INT PRIMARY KEY NOT NULL,
"title" TEXT NOT NULL ON CONFLICT REPLACE DEFAULT '',
"isDeleted" INTEGER NOT NULL ON CONFLICT REPLACE DEFAULT 0
) STRICT
""",
tableInfo: [
[0]: TableInfo(
defaultValue: nil,
isPrimaryKey: true,
name: "id",
isNotNull: true,
type: "INT"
),
[1]: TableInfo(
defaultValue: "0",
isPrimaryKey: false,
name: "isDeleted",
isNotNull: true,
type: "INTEGER"
),
[2]: TableInfo(
defaultValue: "\'\'",
isPrimaryKey: false,
name: "title",
isNotNull: true,
type: "TEXT"
)
]
)
]
"""#
Expand Down
146 changes: 146 additions & 0 deletions Tests/SQLiteDataTests/CloudKitTests/ScopedTableSyncTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#if canImport(CloudKit)
import CloudKit
import CustomDump
import Foundation
import InlineSnapshotTesting
import SQLiteData
import SQLiteDataTestSupport
import SnapshotTestingCustomDump
import Testing

extension BaseCloudKitTests {
@MainActor
final class ScopedTableSyncTests: BaseCloudKitTests, @unchecked Sendable {
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test func outgoingSaveLookupIncludesScopedOutRow() async throws {
try await userDatabase.userWrite { db in
try db.seed { ScopedModel(id: 1, title: "Important", isDeleted: false) }
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
try await userDatabase.userWrite { db in
try ScopedModel.unscoped.find(1).update { $0.isDeleted = true }.execute(db)
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
assertInlineSnapshot(of: container.privateCloudDatabase, as: .customDump) {
"""
MockCloudDatabase(
databaseScope: .private,
storage: [
[0]: CKRecord(
recordID: CKRecord.ID(1:scopedModels/zone/__defaultOwner__),
recordType: "scopedModels",
parent: nil,
share: nil,
id: 1,
isDeleted: 1,
title: "Important"
)
]
)
"""
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test func zoneDeletionRemovesScopedOutRow() async throws {
try await userDatabase.userWrite { db in
try db.seed { ScopedModel(id: 1, title: "x", isDeleted: false) }
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
try await userDatabase.userWrite { db in
try ScopedModel.unscoped.find(1).update { $0.isDeleted = true }.execute(db)
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)

try await syncEngine.modifyRecordZones(
scope: .private,
deleting: [syncEngine.defaultZone.zoneID]
).notify()
try await syncEngine.processPendingDatabaseChanges(scope: .private)

try await userDatabase.read { db in
try #expect(ScopedModel.unscoped.fetchAll(db) == [])
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test func encryptedDataResetReuploadsScopedOutRow() async throws {
try await userDatabase.userWrite { db in
try db.seed { ScopedModel(id: 1, title: "x", isDeleted: false) }
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
try await userDatabase.userWrite { db in
try ScopedModel.unscoped.find(1).update { $0.isDeleted = true }.execute(db)
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)

await syncEngine.handleEvent(
SyncEngine.Event.fetchedDatabaseChanges(
modifications: [],
deletions: [(syncEngine.defaultZone.zoneID, .encryptedDataReset)]
),
syncEngine: syncEngine.private
)
try await syncEngine.processPendingRecordZoneChanges(scope: .private)

try await userDatabase.read { db in
try #expect(ScopedModel.unscoped.fetchAll(db).count == 1)
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test func serverDeleteRemovesScopedOutRow() async throws {
try await userDatabase.userWrite { db in
try db.seed { ScopedModel(id: 1, title: "x", isDeleted: false) }
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
try await userDatabase.userWrite { db in
try ScopedModel.unscoped.find(1).update { $0.isDeleted = true }.execute(db)
}

try await syncEngine.modifyRecords(
scope: .private,
deleting: [ScopedModel.recordID(for: 1)]
).notify()
try await syncEngine.processPendingRecordZoneChanges(scope: .private)

try await userDatabase.read { db in
try #expect(ScopedModel.unscoped.fetchAll(db) == [])
}
}

@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
@Test func serverModificationMergesScopedOutRow() async throws {
try await userDatabase.userWrite { db in
try db.seed { ScopedModel(id: 1, title: "x", isDeleted: false) }
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
try await userDatabase.userWrite { db in
try ScopedModel.unscoped.find(1).update { $0.isDeleted = true }.execute(db)
}
try await syncEngine.processPendingRecordZoneChanges(scope: .private)

let serverRecord = CKRecord(
recordType: ScopedModel.tableName,
recordID: ScopedModel.recordID(for: 1)
)
serverRecord["id"] = 1
serverRecord["title"] = "from-server"
serverRecord["isDeleted"] = 1
await syncEngine.handleEvent(
SyncEngine.Event.fetchedRecordZoneChanges(
modifications: [serverRecord],
deletions: []
),
syncEngine: syncEngine.private
)

try await userDatabase.read { db in
let rows = try ScopedModel.unscoped.fetchAll(db)
#expect(rows.count == 1)
#expect(rows.first?.isDeleted == true)
}
}
}
}
#endif
Loading