From 57dfa6a1bb13f64c260867546893f82f539b3ac9 Mon Sep 17 00:00:00 2001 From: Johan Kool Date: Tue, 25 Nov 2025 20:51:04 +0100 Subject: [PATCH 1/3] Store small data as value --- .../CloudKit/CloudKit+StructuredQueries.swift | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift index 6e9eacd9..698bf38e 100644 --- a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift +++ b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift @@ -178,6 +178,9 @@ encryptedValues[hash: key] != hash else { return false } + if encryptedValues[key] != nil { + encryptedValues.setObject(nil, forKey: key) + } self[key] = newValue encryptedValues[hash: key] = hash encryptedValues[at: key] = userModificationTime @@ -194,6 +197,20 @@ guard encryptedValues[at: key] <= userModificationTime else { return false } + if newValue.isSmall { + guard + encryptedValues[at: key] <= userModificationTime, + encryptedValues[key] != newValue + else { return false } + if self[key] != nil { + self.setObject(nil, forKey: key) + } + encryptedValues[key] = newValue + encryptedValues[at: key] = userModificationTime + self.userModificationTime = userModificationTime + return true + } + @Dependency(\.dataManager) var dataManager let hash = newValue.sha256 let fileURL = dataManager.temporaryDirectory.appending( @@ -301,7 +318,13 @@ var isRowValueModified: Bool { switch Value(queryOutput: row[keyPath: keyPath]).queryBinding { case .blob(let value): - return other.encryptedValues[hash: key] != value.sha256 + if value.isSmall { + return other.encryptedValues[key] != value + } else if let otherHash = other.encryptedValues[hash: key] { + return otherHash != value.sha256 + } else { + return true + } case .bool(let value): return other.encryptedValues[key] != value case .double(let value): @@ -357,6 +380,8 @@ return value.queryFragment } else if let value = self as? Date { return value.queryFragment + } else if let value = self as? [UInt8] { + return value.queryFragment } else { return "\(.invalid(Unbindable()))" } @@ -376,5 +401,10 @@ fileprivate var sha256: Data { Data(SHA256.hash(data: self)) } + + fileprivate var isSmall: Bool { + count * MemoryLayout.stride < 16384 + } } + #endif From 192b400e3004ff015495822fd446bb1f72292400 Mon Sep 17 00:00:00 2001 From: Johan Kool Date: Fri, 28 Nov 2025 00:31:37 +0100 Subject: [PATCH 2/3] Restore assets tests but with bigger (> 16 kb) test data --- Package.swift | 4 + .../CloudKitTests/AssetsTests.swift | 90 +++-- .../CloudKitTests/DataTests.swift | 325 ++++++++++++++++++ .../SQLiteDataTests/Resources/test-black.svg | 6 + Tests/SQLiteDataTests/Resources/test-red.svg | 6 + 5 files changed, 402 insertions(+), 29 deletions(-) create mode 100644 Tests/SQLiteDataTests/CloudKitTests/DataTests.swift create mode 100644 Tests/SQLiteDataTests/Resources/test-black.svg create mode 100644 Tests/SQLiteDataTests/Resources/test-red.svg diff --git a/Package.swift b/Package.swift index 0d607e9d..503f61c1 100644 --- a/Package.swift +++ b/Package.swift @@ -82,6 +82,10 @@ let package = Package( .product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"), .product(name: "SnapshotTestingCustomDump", package: "swift-snapshot-testing"), .product(name: "StructuredQueries", package: "swift-structured-queries"), + ], + resources: [ + .copy("Resources/test-black.svg"), + .copy("Resources/test-red.svg") ] ), ], diff --git a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift index 7744f34b..8d8904a1 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift @@ -14,17 +14,20 @@ final class AssetsTests: BaseCloudKitTests, @unchecked Sendable { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func basics() async throws { + let blackImageURL = Bundle.module.url(forResource: "test-black", withExtension: "svg")! + let blackCoverImage = try Data(contentsOf: blackImageURL) + try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Personal") - RemindersListAsset(remindersListID: 1, coverImage: Data("image".utf8)) + RemindersListAsset(remindersListID: 1, coverImage: blackCoverImage) } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { - """ + #""" MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, @@ -37,8 +40,15 @@ coverImage_hash: Data(32 bytes), remindersListID: 1, coverImage: CKAsset( - fileURL: URL(file:///6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d), - dataString: "image" + fileURL: URL(file:///fb007a3c86694eb9c1122a4af617772e2396d8b093fd6e79981bff2221d78874), + dataString: """ + + + + + + + """ ) ), [1]: CKRecord( @@ -56,23 +66,26 @@ storage: [] ) ) - """ + """# } inMemoryDataManager.storage.withValue { storage in let url = URL( - string: "file:///6105d6cc76af400325e94d588ce511be5bfdbb73b437dc51eca43917d7a43e3d" + string: "file:///fb007a3c86694eb9c1122a4af617772e2396d8b093fd6e79981bff2221d78874" )! - #expect(storage[url] == Data("image".utf8)) + #expect(storage[url] == blackCoverImage) } + let redImageURL = Bundle.module.url(forResource: "test-red", withExtension: "svg")! + let redCoverImage = try Data(contentsOf: redImageURL) + try await withDependencies { $0.currentTime.now += 1 } operation: { try await userDatabase.userWrite { db in try RemindersListAsset .find(1) - .update { $0.coverImage = Data("new-image".utf8) } + .update { $0.coverImage = redCoverImage } .execute(db) } } @@ -80,7 +93,7 @@ try await syncEngine.processPendingRecordZoneChanges(scope: .private) assertInlineSnapshot(of: container, as: .customDump) { - """ + #""" MockCloudContainer( privateCloudDatabase: MockCloudDatabase( databaseScope: .private, @@ -93,8 +106,15 @@ coverImage_hash: Data(32 bytes), remindersListID: 1, coverImage: CKAsset( - fileURL: URL(file:///97e67a5645969953f1a4cfe2ea75649864ff99789189cdd3f6db03e59f8a8ebf), - dataString: "new-image" + fileURL: URL(file:///944321b4de38bc7785f6941a4adf4c0fbd370ed4c0e9746327d1a17f4031ba60), + dataString: """ + + + + + + + """ ) ), [1]: CKRecord( @@ -112,14 +132,14 @@ storage: [] ) ) - """ + """# } inMemoryDataManager.storage.withValue { storage in let url = URL( - string: "file:///97e67a5645969953f1a4cfe2ea75649864ff99789189cdd3f6db03e59f8a8ebf" + string: "file:///944321b4de38bc7785f6941a4adf4c0fbd370ed4c0e9746327d1a17f4031ba60" )! - #expect(storage[url] == Data("new-image".utf8)) + #expect(storage[url] == redCoverImage) } } @@ -127,6 +147,8 @@ // => Stored in database as bytes @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func receiveAsset() async throws { + let blackImageURL = Bundle.module.url(forResource: "test-black", withExtension: "svg")! + let blackCoverImage = try Data(contentsOf: blackImageURL) let remindersListRecord = CKRecord( recordType: RemindersList.tableName, recordID: RemindersList.recordID(for: 1) @@ -135,7 +157,7 @@ remindersListRecord.setValue("Personal", forKey: "title", at: now) let fileURL = URL(fileURLWithPath: UUID().uuidString) - try inMemoryDataManager.save(Data("image".utf8), to: fileURL) + try inMemoryDataManager.save(blackCoverImage, to: fileURL) let remindersListAssetRecord = CKRecord( recordType: RemindersListAsset.tableName, recordID: RemindersListAsset.recordID(for: 1) @@ -158,7 +180,7 @@ let remindersListAsset = try #require( try RemindersListAsset.find(1).fetchOne(db) ) - #expect(remindersListAsset.coverImage == Data("image".utf8)) + #expect(remindersListAsset.coverImage == blackCoverImage) } } @@ -166,19 +188,23 @@ // => Stored in database as bytes @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func receiveUpdatedAsset() async throws { + let blackImageURL = Bundle.module.url(forResource: "test-black", withExtension: "svg")! + let blackCoverImage = try Data(contentsOf: blackImageURL) try await userDatabase.userWrite { db in try db.seed { RemindersList(id: 1, title: "Personal") - RemindersListAsset(remindersListID: 1, coverImage: Data("image".utf8)) + RemindersListAsset(remindersListID: 1, coverImage: blackCoverImage) } } try await syncEngine.processPendingRecordZoneChanges(scope: .private) + let redImageURL = Bundle.module.url(forResource: "test-red", withExtension: "svg")! + let redCoverImage = try Data(contentsOf: redImageURL) try await withDependencies { $0.currentTime.now += 1 } operation: { let fileURL = URL(fileURLWithPath: UUID().uuidString) - try inMemoryDataManager.save(Data("new-image".utf8), to: fileURL) + try inMemoryDataManager.save(redCoverImage, to: fileURL) let remindersListAssetRecord = try syncEngine.private.database.record( for: RemindersListAsset.recordID(for: 1) ) @@ -198,7 +224,7 @@ let remindersListAsset = try #require( try RemindersListAsset.find(1).fetchOne(db) ) - #expect(remindersListAsset.coverImage == Data("new-image".utf8)) + #expect(remindersListAsset.coverImage == redCoverImage) } } @@ -208,6 +234,8 @@ @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func receiveAssetThenReceiveUpdate() async throws { do { + let blackImageURL = Bundle.module.url(forResource: "test-black", withExtension: "svg")! + let blackCoverImage = try Data(contentsOf: blackImageURL) let remindersListRecord = CKRecord( recordType: RemindersList.tableName, recordID: RemindersList.recordID(for: 1) @@ -216,7 +244,7 @@ remindersListRecord.setValue("Personal", forKey: "title", at: now) let fileURL = URL(fileURLWithPath: UUID().uuidString) - try inMemoryDataManager.save(Data("image".utf8), to: fileURL) + try inMemoryDataManager.save(blackCoverImage, to: fileURL) let remindersListAssetRecord = CKRecord( recordType: RemindersListAsset.tableName, recordID: RemindersListAsset.recordID(for: 1) @@ -240,11 +268,13 @@ .notify() } + let redImageURL = Bundle.module.url(forResource: "test-red", withExtension: "svg")! + let redCoverImage = try Data(contentsOf: redImageURL) try await withDependencies { $0.currentTime.now += 1 } operation: { let fileURL = URL(fileURLWithPath: UUID().uuidString) - try inMemoryDataManager.save(Data("new-image".utf8), to: fileURL) + try inMemoryDataManager.save(redCoverImage, to: fileURL) let remindersListAssetRecord = try syncEngine.private.database.record( for: RemindersListAsset.recordID(for: 1) ) @@ -264,7 +294,7 @@ let remindersListAsset = try #require( try RemindersListAsset.find(1).fetchOne(db) ) - #expect(remindersListAsset.coverImage == Data("new-image".utf8)) + #expect(remindersListAsset.coverImage == redCoverImage) } } @@ -273,6 +303,8 @@ // => Both records (and the image data) should be synchronized @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Test func assetReceivedBeforeParentRecord() async throws { + let blackImageURL = Bundle.module.url(forResource: "test-black", withExtension: "svg")! + let blackCoverImage = try Data(contentsOf: blackImageURL) let remindersListRecord = CKRecord( recordType: RemindersList.tableName, recordID: RemindersList.recordID(for: 1) @@ -286,7 +318,7 @@ ) remindersListAssetRecord.setValue("1", forKey: "id", at: now) remindersListAssetRecord.setValue( - Array("image".utf8), + blackCoverImage, forKey: "coverImage", at: now ) @@ -320,12 +352,12 @@ } assertQuery(RemindersListAsset.all, database: userDatabase.database) { """ - ┌─────────────────────────────┐ - │ RemindersListAsset( │ - │ remindersListID: 1, │ - │ coverImage: Data(5 bytes) │ - │ ) │ - └─────────────────────────────┘ + ┌──────────────────────────────────┐ + │ RemindersListAsset( │ + │ remindersListID: 1, │ + │ coverImage: Data(18.164 bytes) │ + │ ) │ + └──────────────────────────────────┘ """ } diff --git a/Tests/SQLiteDataTests/CloudKitTests/DataTests.swift b/Tests/SQLiteDataTests/CloudKitTests/DataTests.swift new file mode 100644 index 00000000..549e711b --- /dev/null +++ b/Tests/SQLiteDataTests/CloudKitTests/DataTests.swift @@ -0,0 +1,325 @@ +#if canImport(CloudKit) + import CloudKit + import ConcurrencyExtras + import CustomDump + import InlineSnapshotTesting + import OrderedCollections + import SQLiteData + import SQLiteDataTestSupport + import SnapshotTestingCustomDump + import Testing + + extension BaseCloudKitTests { + @MainActor + final class DataTests: BaseCloudKitTests, @unchecked Sendable { + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func basics() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + RemindersListAsset(remindersListID: 1, coverImage: Data("image".utf8)) + } + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersListAssets/zone/__defaultOwner__), + recordType: "remindersListAssets", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), + share: nil, + coverImage: [ + [0]: 105, + [1]: 109, + [2]: 97, + [3]: 103, + [4]: 101 + ], + remindersListID: 1 + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Personal" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + try await userDatabase.userWrite { db in + try RemindersListAsset + .find(1) + .update { $0.coverImage = Data("new-image".utf8) } + .execute(db) + } + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(1:remindersListAssets/zone/__defaultOwner__), + recordType: "remindersListAssets", + parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), + share: nil, + coverImage: [ + [0]: 110, + [1]: 101, + [2]: 119, + [3]: 45, + [4]: 105, + [5]: 109, + [6]: 97, + [7]: 103, + [8]: 101 + ], + remindersListID: 1 + ), + [1]: CKRecord( + recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 1, + title: "Personal" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } + + // * Receive record with CKAsset from CloudKit + // => Stored in database as bytes + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.disabled()) func receiveData() async throws { + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1) + ) + remindersListRecord.setValue("1", forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + + let remindersListAssetRecord = CKRecord( + recordType: RemindersListAsset.tableName, + recordID: RemindersListAsset.recordID(for: 1) + ) + remindersListAssetRecord.setValue("1", forKey: "id", at: now) + remindersListAssetRecord.setValue(Data("image".utf8), forKey: "coverImage", at: now) + remindersListAssetRecord.setValue("1", forKey: "remindersListID", at: now) + remindersListAssetRecord.parent = CKRecord.Reference( + record: remindersListRecord, + action: .none + ) + + try await syncEngine.modifyRecords( + scope: .private, + saving: [remindersListAssetRecord, remindersListRecord] + ) + .notify() + + try await userDatabase.read { db in + let remindersListAsset = try #require( + try RemindersListAsset.find(1).fetchOne(db) + ) + #expect(remindersListAsset.coverImage == Data("image".utf8)) + } + } + + // * Receive record with Data from CloudKit when local asset exists + // => Stored in database as bytes + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.disabled()) func receiveUpdatedData() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + RemindersListAsset(remindersListID: 1, coverImage: Data("image".utf8)) + } + } + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + let remindersListAssetRecord = try syncEngine.private.database.record( + for: RemindersListAsset.recordID(for: 1) + ) + remindersListAssetRecord.setValue( + Data("new-image".utf8), + forKey: "coverImage", + at: now + ) + try await syncEngine.modifyRecords( + scope: .private, + saving: [remindersListAssetRecord] + ) + .notify() + } + + try await userDatabase.read { db in + let remindersListAsset = try #require( + try RemindersListAsset.find(1).fetchOne(db) + ) + #expect(remindersListAsset.coverImage == Data("new-image".utf8)) + } + } + + // * Receive record with CKAsset from CloudKit when local asset does not exist + // * Receive updated asset from CloudKit + // => Local database has freshest asset + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.disabled()) func receiveAssetThenReceiveUpdate() async throws { + do { + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1) + ) + remindersListRecord.setValue("1", forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + + let fileURL = URL(fileURLWithPath: UUID().uuidString) + try inMemoryDataManager.save(Data("image".utf8), to: fileURL) + let remindersListAssetRecord = CKRecord( + recordType: RemindersListAsset.tableName, + recordID: RemindersListAsset.recordID(for: 1) + ) + remindersListAssetRecord.setValue("1", forKey: "id", at: now) + remindersListAssetRecord.setAsset( + CKAsset(fileURL: fileURL), + forKey: "coverImage", + at: now + ) + remindersListAssetRecord.setValue("1", forKey: "remindersListID", at: now) + remindersListAssetRecord.parent = CKRecord.Reference( + record: remindersListRecord, + action: .none + ) + + try await syncEngine.modifyRecords( + scope: .private, + saving: [remindersListAssetRecord, remindersListRecord] + ) + .notify() + } + + try await withDependencies { + $0.currentTime.now += 1 + } operation: { + let fileURL = URL(fileURLWithPath: UUID().uuidString) + try inMemoryDataManager.save(Data("new-image".utf8), to: fileURL) + let remindersListAssetRecord = try syncEngine.private.database.record( + for: RemindersListAsset.recordID(for: 1) + ) + remindersListAssetRecord.setAsset( + CKAsset(fileURL: fileURL), + forKey: "coverImage", + at: now + ) + try await syncEngine.modifyRecords( + scope: .private, + saving: [remindersListAssetRecord] + ) + .notify() + } + + try await userDatabase.read { db in + let remindersListAsset = try #require( + try RemindersListAsset.find(1).fetchOne(db) + ) + #expect(remindersListAsset.coverImage == Data("new-image".utf8)) + } + } + + // * Client receives RemindersListAsset with image data + // * A moment later client receives the parent RemindersList + // => Both records (and the image data) should be synchronized + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test(.disabled()) func assetReceivedBeforeParentRecord() async throws { + let remindersListRecord = CKRecord( + recordType: RemindersList.tableName, + recordID: RemindersList.recordID(for: 1) + ) + remindersListRecord.setValue("1", forKey: "id", at: now) + remindersListRecord.setValue("Personal", forKey: "title", at: now) + + let remindersListAssetRecord = CKRecord( + recordType: RemindersListAsset.tableName, + recordID: RemindersListAsset.recordID(for: 1) + ) + remindersListAssetRecord.setValue("1", forKey: "id", at: now) + remindersListAssetRecord.setValue( + Array("image".utf8), + forKey: "coverImage", + at: now + ) + remindersListAssetRecord.setValue( + "1", + forKey: "remindersListID", + at: now + ) + remindersListAssetRecord.parent = CKRecord.Reference( + record: remindersListRecord, + action: .none + ) + + let remindersListModification = try syncEngine.modifyRecords( + scope: .private, + saving: [remindersListRecord] + ) + try await syncEngine.modifyRecords(scope: .private, saving: [remindersListAssetRecord]) + .notify() + await remindersListModification.notify() + + assertQuery(RemindersList.all, database: userDatabase.database) { + """ + ┌─────────────────────┐ + │ RemindersList( │ + │ id: 1, │ + │ title: "Personal" │ + │ ) │ + └─────────────────────┘ + """ + } + assertQuery(RemindersListAsset.all, database: userDatabase.database) { + """ + ┌─────────────────────────────┐ + │ RemindersListAsset( │ + │ remindersListID: 1, │ + │ coverImage: Data(5 bytes) │ + │ ) │ + └─────────────────────────────┘ + """ + } + + } + } + } +#endif diff --git a/Tests/SQLiteDataTests/Resources/test-black.svg b/Tests/SQLiteDataTests/Resources/test-black.svg new file mode 100644 index 00000000..26c9a845 --- /dev/null +++ b/Tests/SQLiteDataTests/Resources/test-black.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Tests/SQLiteDataTests/Resources/test-red.svg b/Tests/SQLiteDataTests/Resources/test-red.svg new file mode 100644 index 00000000..eb63523c --- /dev/null +++ b/Tests/SQLiteDataTests/Resources/test-red.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file From 94ff2f28feb9937f7920eca27d7fae8a11abd96c Mon Sep 17 00:00:00 2001 From: Johan Kool Date: Fri, 28 Nov 2025 09:52:39 +0100 Subject: [PATCH 3/3] Use separate key for data --- .../CloudKit/CloudKit+StructuredQueries.swift | 18 +- .../CloudKitTests/AssetsTests.swift | 188 ++++++++++++++++-- .../CloudKitTests/DataTests.swift | 20 +- .../SQLiteDataTests/Resources/test-black.svg | 89 ++++++++- Tests/SQLiteDataTests/Resources/test-red.svg | 89 ++++++++- 5 files changed, 355 insertions(+), 49 deletions(-) diff --git a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift index 698bf38e..fc0422b3 100644 --- a/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift +++ b/Sources/SQLiteData/CloudKit/CloudKit+StructuredQueries.swift @@ -142,6 +142,10 @@ get { self["\(key)_hash"] as? Data } set { self["\(key)_hash"] = newValue } } + fileprivate subscript(data key: String) -> Data? { + get { self["\(key)_data"] as? Data } + set { self["\(key)_data"] = newValue } + } } @available(macOS 13, iOS 16, tvOS 16, watchOS 9, *) @@ -178,8 +182,8 @@ encryptedValues[hash: key] != hash else { return false } - if encryptedValues[key] != nil { - encryptedValues.setObject(nil, forKey: key) + if encryptedValues[data: key] != nil { + encryptedValues[data: key] = nil } self[key] = newValue encryptedValues[hash: key] = hash @@ -198,14 +202,16 @@ else { return false } if newValue.isSmall { + let newData = Data(newValue) guard encryptedValues[at: key] <= userModificationTime, - encryptedValues[key] != newValue + encryptedValues[data: key] != newData else { return false } if self[key] != nil { - self.setObject(nil, forKey: key) + self[key] = nil + encryptedValues[hash: key] = nil } - encryptedValues[key] = newValue + encryptedValues[data: key] = newData encryptedValues[at: key] = userModificationTime self.userModificationTime = userModificationTime return true @@ -319,7 +325,7 @@ switch Value(queryOutput: row[keyPath: keyPath]).queryBinding { case .blob(let value): if value.isSmall { - return other.encryptedValues[key] != value + return other.encryptedValues[key] != Data(value) } else if let otherHash = other.encryptedValues[hash: key] { return otherHash != value.sha256 } else { diff --git a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift index 8d8904a1..1d400a58 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/AssetsTests.swift @@ -40,13 +40,92 @@ coverImage_hash: Data(32 bytes), remindersListID: 1, coverImage: CKAsset( - fileURL: URL(file:///fb007a3c86694eb9c1122a4af617772e2396d8b093fd6e79981bff2221d78874), + fileURL: URL(file:///4eb74bd60d41b48bd682896ff4ba846da5051a1e190159414b3ba177a9dbe482), dataString: """ - - - - - + + + + + + + + + """ ) @@ -71,7 +150,7 @@ inMemoryDataManager.storage.withValue { storage in let url = URL( - string: "file:///fb007a3c86694eb9c1122a4af617772e2396d8b093fd6e79981bff2221d78874" + string: "file:///4eb74bd60d41b48bd682896ff4ba846da5051a1e190159414b3ba177a9dbe482" )! #expect(storage[url] == blackCoverImage) } @@ -106,13 +185,92 @@ coverImage_hash: Data(32 bytes), remindersListID: 1, coverImage: CKAsset( - fileURL: URL(file:///944321b4de38bc7785f6941a4adf4c0fbd370ed4c0e9746327d1a17f4031ba60), + fileURL: URL(file:///43aba58d3830c6821f433a10c9fd554e53c257ebfd9c451514ea2c27c774b79f), dataString: """ - - - - - + + + + + + + + + """ ) @@ -137,7 +295,7 @@ inMemoryDataManager.storage.withValue { storage in let url = URL( - string: "file:///944321b4de38bc7785f6941a4adf4c0fbd370ed4c0e9746327d1a17f4031ba60" + string: "file:///43aba58d3830c6821f433a10c9fd554e53c257ebfd9c451514ea2c27c774b79f" )! #expect(storage[url] == redCoverImage) } @@ -355,7 +513,7 @@ ┌──────────────────────────────────┐ │ RemindersListAsset( │ │ remindersListID: 1, │ - │ coverImage: Data(18.164 bytes) │ + │ coverImage: Data(16,811 bytes) │ │ ) │ └──────────────────────────────────┘ """ diff --git a/Tests/SQLiteDataTests/CloudKitTests/DataTests.swift b/Tests/SQLiteDataTests/CloudKitTests/DataTests.swift index 549e711b..6da43f38 100644 --- a/Tests/SQLiteDataTests/CloudKitTests/DataTests.swift +++ b/Tests/SQLiteDataTests/CloudKitTests/DataTests.swift @@ -34,13 +34,7 @@ recordType: "remindersListAssets", parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), share: nil, - coverImage: [ - [0]: 105, - [1]: 109, - [2]: 97, - [3]: 103, - [4]: 101 - ], + coverImage_data: Data(5 bytes), remindersListID: 1 ), [1]: CKRecord( @@ -85,17 +79,7 @@ recordType: "remindersListAssets", parent: CKReference(recordID: CKRecord.ID(1:remindersLists/zone/__defaultOwner__)), share: nil, - coverImage: [ - [0]: 110, - [1]: 101, - [2]: 119, - [3]: 45, - [4]: 105, - [5]: 109, - [6]: 97, - [7]: 103, - [8]: 101 - ], + coverImage_data: Data(9 bytes), remindersListID: 1 ), [1]: CKRecord( diff --git a/Tests/SQLiteDataTests/Resources/test-black.svg b/Tests/SQLiteDataTests/Resources/test-black.svg index 26c9a845..1f42b9bf 100644 --- a/Tests/SQLiteDataTests/Resources/test-black.svg +++ b/Tests/SQLiteDataTests/Resources/test-black.svg @@ -1,6 +1,85 @@ - - - - - + + + + + + + + + \ No newline at end of file diff --git a/Tests/SQLiteDataTests/Resources/test-red.svg b/Tests/SQLiteDataTests/Resources/test-red.svg index eb63523c..dc16c928 100644 --- a/Tests/SQLiteDataTests/Resources/test-red.svg +++ b/Tests/SQLiteDataTests/Resources/test-red.svg @@ -1,6 +1,85 @@ - - - - - + + + + + + + + + \ No newline at end of file