Skip to content

Commit d471d29

Browse files
authored
Private tables should always be saved in private db. (#310)
* Private tables should always be saved in private db. * snapshots
1 parent 0aba45e commit d471d29

4 files changed

Lines changed: 219 additions & 73 deletions

File tree

Sources/SQLiteData/CloudKit/Internal/Triggers.swift

Lines changed: 70 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,38 @@
66
extension PrimaryKeyedTable {
77
static func metadataTriggers(
88
parentForeignKey: ForeignKey?,
9-
defaultZone: CKRecordZone
9+
defaultZone: CKRecordZone,
10+
privateTables: [any SynchronizableTable]
1011
) -> [TemporaryTrigger<Self>] {
1112
[
12-
afterInsert(parentForeignKey: parentForeignKey, defaultZone: defaultZone),
13-
afterUpdate(parentForeignKey: parentForeignKey, defaultZone: defaultZone),
14-
afterDeleteFromUser(parentForeignKey: parentForeignKey, defaultZone: defaultZone),
13+
afterInsert(
14+
parentForeignKey: parentForeignKey,
15+
defaultZone: defaultZone,
16+
privateTables: privateTables
17+
),
18+
afterUpdate(
19+
parentForeignKey: parentForeignKey,
20+
defaultZone: defaultZone,
21+
privateTables: privateTables
22+
),
23+
afterDeleteFromUser(
24+
parentForeignKey: parentForeignKey,
25+
defaultZone: defaultZone,
26+
privateTables: privateTables
27+
),
1528
afterDeleteFromSyncEngine,
16-
afterPrimaryKeyChange(parentForeignKey: parentForeignKey, defaultZone: defaultZone),
29+
afterPrimaryKeyChange(
30+
parentForeignKey: parentForeignKey,
31+
defaultZone: defaultZone,
32+
privateTables: privateTables
33+
),
1734
]
1835
}
1936

2037
fileprivate static func afterPrimaryKeyChange(
2138
parentForeignKey: ForeignKey?,
22-
defaultZone: CKRecordZone
39+
defaultZone: CKRecordZone,
40+
privateTables: [any SynchronizableTable]
2341
) -> TemporaryTrigger<Self> {
2442
createTemporaryTrigger(
2543
"\(String.sqliteDataCloudKitSchemaName)_after_primary_key_change_on_\(tableName)",
@@ -28,7 +46,8 @@
2846
checkWritePermissions(
2947
alias: new,
3048
parentForeignKey: parentForeignKey,
31-
defaultZone: defaultZone
49+
defaultZone: defaultZone,
50+
privateTables: privateTables
3251
)
3352
SyncMetadata
3453
.where {
@@ -44,7 +63,8 @@
4463

4564
fileprivate static func afterInsert(
4665
parentForeignKey: ForeignKey?,
47-
defaultZone: CKRecordZone
66+
defaultZone: CKRecordZone,
67+
privateTables: [any SynchronizableTable]
4868
) -> TemporaryTrigger<Self> {
4969
createTemporaryTrigger(
5070
"\(String.sqliteDataCloudKitSchemaName)_after_insert_on_\(tableName)",
@@ -53,20 +73,23 @@
5373
checkWritePermissions(
5474
alias: new,
5575
parentForeignKey: parentForeignKey,
56-
defaultZone: defaultZone
76+
defaultZone: defaultZone,
77+
privateTables: privateTables
5778
)
5879
SyncMetadata.insert(
5980
new: new,
6081
parentForeignKey: parentForeignKey,
61-
defaultZone: defaultZone
82+
defaultZone: defaultZone,
83+
privateTables: privateTables
6284
)
6385
}
6486
)
6587
}
6688

6789
fileprivate static func afterUpdate(
6890
parentForeignKey: ForeignKey?,
69-
defaultZone: CKRecordZone
91+
defaultZone: CKRecordZone,
92+
privateTables: [any SynchronizableTable]
7093
) -> TemporaryTrigger<Self> {
7194
createTemporaryTrigger(
7295
"\(String.sqliteDataCloudKitSchemaName)_after_update_on_\(tableName)",
@@ -75,25 +98,29 @@
7598
checkWritePermissions(
7699
alias: new,
77100
parentForeignKey: parentForeignKey,
78-
defaultZone: defaultZone
101+
defaultZone: defaultZone,
102+
privateTables: privateTables
79103
)
80104
SyncMetadata.insert(
81105
new: new,
82106
parentForeignKey: parentForeignKey,
83-
defaultZone: defaultZone
107+
defaultZone: defaultZone,
108+
privateTables: privateTables
84109
)
85110
SyncMetadata.update(
86111
new: new,
87112
parentForeignKey: parentForeignKey,
88-
defaultZone: defaultZone
113+
defaultZone: defaultZone,
114+
privateTables: privateTables
89115
)
90116
}
91117
)
92118
}
93119

94120
fileprivate static func afterDeleteFromUser(
95121
parentForeignKey: ForeignKey?,
96-
defaultZone: CKRecordZone
122+
defaultZone: CKRecordZone,
123+
privateTables: [any SynchronizableTable]
97124
) -> TemporaryTrigger<
98125
Self
99126
> {
@@ -104,7 +131,8 @@
104131
checkWritePermissions(
105132
alias: old,
106133
parentForeignKey: parentForeignKey,
107-
defaultZone: defaultZone
134+
defaultZone: defaultZone,
135+
privateTables: privateTables
108136
)
109137
SyncMetadata
110138
.where {
@@ -141,12 +169,14 @@
141169
fileprivate static func insert<T: PrimaryKeyedTable, Name>(
142170
new: StructuredQueriesCore.TableAlias<T, Name>.TableColumns,
143171
parentForeignKey: ForeignKey?,
144-
defaultZone: CKRecordZone
172+
defaultZone: CKRecordZone,
173+
privateTables: [any SynchronizableTable]
145174
) -> some StructuredQueriesCore.Statement {
146175
let (parentRecordPrimaryKey, parentRecordType, zoneName, ownerName) = parentFields(
147176
alias: new,
148177
parentForeignKey: parentForeignKey,
149-
defaultZone: defaultZone
178+
defaultZone: defaultZone,
179+
privateTables: privateTables
150180
)
151181
let defaultZoneName = #sql(
152182
"\(quote: defaultZone.zoneID.zoneName, delimiter: .text)",
@@ -181,12 +211,14 @@
181211
fileprivate static func update<T: PrimaryKeyedTable, Name>(
182212
new: StructuredQueriesCore.TableAlias<T, Name>.TableColumns,
183213
parentForeignKey: ForeignKey?,
184-
defaultZone: CKRecordZone
214+
defaultZone: CKRecordZone,
215+
privateTables: [any SynchronizableTable]
185216
) -> some StructuredQueriesCore.Statement {
186217
let (parentRecordPrimaryKey, parentRecordType, zoneName, ownerName) = parentFields(
187218
alias: new,
188219
parentForeignKey: parentForeignKey,
189-
defaultZone: defaultZone
220+
defaultZone: defaultZone,
221+
privateTables: privateTables
190222
)
191223
return Self.where {
192224
$0.recordPrimaryKey.eq(#sql("\(new.primaryKey)"))
@@ -322,15 +354,24 @@
322354
private func parentFields<Base, Name>(
323355
alias: StructuredQueriesCore.TableAlias<Base, Name>.TableColumns,
324356
parentForeignKey: ForeignKey?,
325-
defaultZone: CKRecordZone
357+
defaultZone: CKRecordZone,
358+
privateTables: [any SynchronizableTable]
326359
) -> (
327360
parentRecordPrimaryKey: SQLQueryExpression<String>?,
328361
parentRecordType: SQLQueryExpression<String>?,
329362
zoneName: SQLQueryExpression<String?>,
330363
ownerName: SQLQueryExpression<String?>
331364
) {
332-
return
333-
parentForeignKey
365+
let zoneNameOverride: SQLQueryExpression<String?>
366+
let ownerNameOverride: SQLQueryExpression<String?>
367+
if privateTables.contains(where: { $0.base.tableName == Base.tableName }) {
368+
zoneNameOverride = #sql("\(quote: defaultZone.zoneID.zoneName, delimiter: .text)")
369+
ownerNameOverride = #sql("\(quote: defaultZone.zoneID.ownerName, delimiter: .text)")
370+
} else {
371+
zoneNameOverride = #sql("NULL")
372+
ownerNameOverride = #sql("NULL")
373+
}
374+
return parentForeignKey
334375
.map { foreignKey in
335376
let parentRecordPrimaryKey = #sql(
336377
#"\#(type(of: alias).QueryValue.self).\#(quote: foreignKey.from)"#,
@@ -344,8 +385,8 @@
344385
return (
345386
parentRecordPrimaryKey,
346387
parentRecordType,
347-
#sql("coalesce(\($currentZoneName()), (\(parentMetadata.select(\.zoneName))))"),
348-
#sql("coalesce(\($currentOwnerName()), (\(parentMetadata.select(\.ownerName))))")
388+
#sql("coalesce(\(zoneNameOverride), \($currentZoneName()), (\(parentMetadata.select(\.zoneName))))"),
389+
#sql("coalesce(\(ownerNameOverride), \($currentOwnerName()), (\(parentMetadata.select(\.ownerName))))")
349390
)
350391
}
351392
?? (
@@ -373,12 +414,14 @@
373414
private func checkWritePermissions<Base, Name>(
374415
alias: StructuredQueriesCore.TableAlias<Base, Name>.TableColumns,
375416
parentForeignKey: ForeignKey?,
376-
defaultZone: CKRecordZone
417+
defaultZone: CKRecordZone,
418+
privateTables: [any SynchronizableTable]
377419
) -> some StructuredQueriesCore.Statement<Never> {
378420
let (parentRecordPrimaryKey, parentRecordType, _, _) = parentFields(
379421
alias: alias,
380422
parentForeignKey: parentForeignKey,
381-
defaultZone: defaultZone
423+
defaultZone: defaultZone,
424+
privateTables: privateTables
382425
)
383426

384427
return With {

Sources/SQLiteData/CloudKit/SyncEngine.swift

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,7 @@
359359
foreignKeysByTableName: foreignKeysByTableName,
360360
tablesByName: tablesByName,
361361
defaultZone: defaultZone,
362+
privateTables: privateTables,
362363
db: db
363364
)
364365
}
@@ -686,7 +687,8 @@
686687
package func tearDownSyncEngine() throws {
687688
try userDatabase.write { db in
688689
for table in tables.reversed() {
689-
try table.base.dropTriggers(defaultZone: defaultZone, db: db)
690+
try table.base
691+
.dropTriggers(defaultZone: defaultZone, privateTables: privateTables, db: db)
690692
}
691693
for trigger in SyncMetadata.callbackTriggers(for: self).reversed() {
692694
try trigger.drop().execute(db)
@@ -886,22 +888,36 @@
886888
foreignKeysByTableName: [String: [ForeignKey]],
887889
tablesByName: [String: any SynchronizableTable],
888890
defaultZone: CKRecordZone,
891+
privateTables: [any SynchronizableTable],
889892
db: Database
890893
) throws {
891894
let parentForeignKey =
892895
foreignKeysByTableName[tableName]?.count == 1
893896
? foreignKeysByTableName[tableName]?.first
894897
: nil
895898

896-
for trigger in metadataTriggers(parentForeignKey: parentForeignKey, defaultZone: defaultZone)
899+
for trigger in metadataTriggers(
900+
parentForeignKey: parentForeignKey,
901+
defaultZone: defaultZone,
902+
privateTables: privateTables
903+
)
897904
{
898905
try trigger.execute(db)
899906
}
900907
}
901908

902909
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
903-
fileprivate static func dropTriggers(defaultZone: CKRecordZone, db: Database) throws {
904-
for trigger in metadataTriggers(parentForeignKey: nil, defaultZone: defaultZone).reversed() {
910+
fileprivate static func dropTriggers(
911+
defaultZone: CKRecordZone,
912+
privateTables: [any SynchronizableTable],
913+
db: Database
914+
) throws {
915+
for trigger in metadataTriggers(
916+
parentForeignKey: nil,
917+
defaultZone: defaultZone,
918+
privateTables: privateTables
919+
)
920+
.reversed() {
905921
try trigger.drop().execute(db)
906922
}
907923
}

Tests/SQLiteDataTests/CloudKitTests/SharingTests.swift

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,93 @@
203203
}
204204
}
205205

206+
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
207+
@Test func privateTablesStayInPrivateDatabase() async throws {
208+
let externalZone = CKRecordZone(
209+
zoneID: CKRecordZone.ID(
210+
zoneName: "external.zone",
211+
ownerName: "external.owner"
212+
)
213+
)
214+
try await syncEngine.modifyRecordZones(scope: .shared, saving: [externalZone]).notify()
215+
216+
let remindersListRecord = CKRecord(
217+
recordType: RemindersList.tableName,
218+
recordID: RemindersList.recordID(for: 1, zoneID: externalZone.zoneID)
219+
)
220+
remindersListRecord.setValue(1, forKey: "id", at: now)
221+
remindersListRecord.setValue("Personal", forKey: "title", at: now)
222+
let share = CKShare(
223+
rootRecord: remindersListRecord,
224+
shareID: CKRecord.ID(
225+
recordName: "share-\(remindersListRecord.recordID.recordName)",
226+
zoneID: remindersListRecord.recordID.zoneID
227+
)
228+
)
229+
_ = try syncEngine.modifyRecords(scope: .shared, saving: [share, remindersListRecord])
230+
let freshShare = try syncEngine.shared.database.record(for: share.recordID) as! CKShare
231+
let freshRemindersListRecord = try syncEngine.shared.database.record(
232+
for: remindersListRecord.recordID
233+
)
234+
235+
try await syncEngine
236+
.acceptShare(
237+
metadata: ShareMetadata(
238+
containerIdentifier: container.containerIdentifier!,
239+
hierarchicalRootRecordID: freshRemindersListRecord.recordID,
240+
rootRecord: freshRemindersListRecord,
241+
share: freshShare
242+
)
243+
)
244+
245+
try await userDatabase.userWrite { db in
246+
try RemindersListPrivate.insert {
247+
RemindersListPrivate(remindersListID: 1, position: 42)
248+
}
249+
.execute(db)
250+
}
251+
try await syncEngine.processPendingRecordZoneChanges(scope: .private)
252+
253+
assertInlineSnapshot(of: container, as: .customDump) {
254+
"""
255+
MockCloudContainer(
256+
privateCloudDatabase: MockCloudDatabase(
257+
databaseScope: .private,
258+
storage: [
259+
[0]: CKRecord(
260+
recordID: CKRecord.ID(1:remindersListPrivates/zone/__defaultOwner__),
261+
recordType: "remindersListPrivates",
262+
parent: nil,
263+
share: nil,
264+
position: 42,
265+
remindersListID: 1
266+
)
267+
]
268+
),
269+
sharedCloudDatabase: MockCloudDatabase(
270+
databaseScope: .shared,
271+
storage: [
272+
[0]: CKRecord(
273+
recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner),
274+
recordType: "cloudkit.share",
275+
parent: nil,
276+
share: nil
277+
),
278+
[1]: CKRecord(
279+
recordID: CKRecord.ID(1:remindersLists/external.zone/external.owner),
280+
recordType: "remindersLists",
281+
parent: nil,
282+
share: CKReference(recordID: CKRecord.ID(share-1:remindersLists/external.zone/external.owner)),
283+
id: 1,
284+
title: "Personal"
285+
)
286+
]
287+
)
288+
)
289+
"""
290+
}
291+
}
292+
206293
@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *)
207294
@Test func shareRecordBeforeSync() async throws {
208295
let error = await #expect(throws: (any Error).self) {

0 commit comments

Comments
 (0)