From f753d6201831b32553429d8cffec658f7fd71934 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 2 Sep 2025 15:39:16 -0500 Subject: [PATCH 1/3] Lots of updates to docs. --- .../CloudKit/CloudKitSharing.swift | 30 +++-- .../SharingGRDBCore/CloudKit/SyncEngine.swift | 75 ++++------- .../Documentation.docc/Articles/CloudKit.md | 118 ++++++++++++++---- .../Articles/CloudKitSharing.md | 58 ++++++++- .../SharingPermissionsTests.swift | 1 + .../SyncEngineValidationTests.swift | 55 +++++++- 6 files changed, 235 insertions(+), 102 deletions(-) diff --git a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift index a74e54b5..4a945c38 100644 --- a/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift +++ b/Sources/SharingGRDBCore/CloudKit/CloudKitSharing.swift @@ -185,34 +185,30 @@ let availablePermissions: UICloudSharingController.PermissionOptions let didFinish: (Result) -> Void let didStopSharing: () -> Void - public init( - sharedRecord: SharedRecord, - availablePermissions: UICloudSharingController.PermissionOptions = [] - ) { - self.init( - sharedRecord: sharedRecord, - availablePermissions: availablePermissions, - didFinish: { _ in }, - didStopSharing: {} - ) - } + let syncEngine: SyncEngine public init( sharedRecord: SharedRecord, availablePermissions: UICloudSharingController.PermissionOptions = [], - didFinish: @escaping (Result) -> Void, - didStopSharing: @escaping () -> Void + didFinish: @escaping (Result) -> Void = { _ in }, + didStopSharing: @escaping () -> Void = { }, + syncEngine: SyncEngine = { + @Dependency(\.defaultSyncEngine) var defaultSyncEngine + return defaultSyncEngine + }() ) { self.sharedRecord = sharedRecord self.didFinish = didFinish self.didStopSharing = didStopSharing self.availablePermissions = availablePermissions + self.syncEngine = syncEngine } public func makeCoordinator() -> CloudSharingDelegate { CloudSharingDelegate( share: sharedRecord.share, didFinish: didFinish, - didStopSharing: didStopSharing + didStopSharing: didStopSharing, + syncEngine: syncEngine ) } @@ -238,14 +234,17 @@ let share: CKShare let didFinish: (Result) -> Void let didStopSharing: () -> Void + let syncEngine: SyncEngine init( share: CKShare, didFinish: @escaping (Result) -> Void, - didStopSharing: @escaping () -> Void + didStopSharing: @escaping () -> Void, + syncEngine: SyncEngine ) { self.share = share self.didFinish = didFinish self.didStopSharing = didStopSharing + self.syncEngine = syncEngine } public func itemThumbnailData(for csc: UICloudSharingController) -> Data? { @@ -261,7 +260,6 @@ } public func cloudSharingControllerDidStopSharing(_ csc: UICloudSharingController) { - @Dependency(\.defaultSyncEngine) var syncEngine withErrorReporting(.sqliteDataCloudKitFailure) { try syncEngine.deleteShare(recordID: share.recordID) } diff --git a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift index feac3959..b0950801 100644 --- a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift +++ b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift @@ -1757,10 +1757,14 @@ /// } /// ``` /// + /// By default this method will use the container identifier assigned in your app's + /// entitlements. If you wish to use a different container identifier then you can provide + /// the `containerIdentifier` argument. + /// /// See for more information on preparing your database. /// - /// - Parameter containerIdentifier: The identifier of the CloudKit container used to synchronize - /// data. + /// - Parameter containerIdentifier: The identifier of the CloudKit container used to + /// synchronize data. Defaults to the value set in the app's entitlements. public func attachMetadatabase(containerIdentifier: String? = nil) throws { let containerIdentifier = containerIdentifier @@ -1824,6 +1828,7 @@ case noCloudKitContainer case nonNullColumnsWithoutDefault(tableName: String, columnNames: [String]) case unknown + case uniquenessConstraint } let reason: Reason let debugDescription: String @@ -1834,28 +1839,6 @@ } } - // TODO: Private, opaque error - // public struct UniqueConstraintDisallowed: Error { - // let localizedDescription: String - // init(table: any PrimaryKeyedTable.Type, columns: [String]) { - // localizedDescription = """ - // Table '\(table.tableName)' has column\(columns.count == 1 ? "" : "s") with unique \ - // constraints: \(columns.map { "'\($0)'" }.joined(separator: ", ")) - // """ - // } - // } - - // TODO: Private, opaque error - // public struct NonNullColumnMustHaveDefault: Error { - // let localizedDescription: String - // init(table: any PrimaryKeyedTable.Type, columns: [String]) { - // localizedDescription = """ - // Table '\(table.tableName)' has non-null column\(columns.count == 1 ? "" : "s") with no \ - // default: \(columns.map { "'\($0)'" }.joined(separator: ", ")) - // """ - // } - // } - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) private func validateSchema( tables: [any PrimaryKeyedTable.Type], @@ -1888,33 +1871,23 @@ } for table in tables { - // // TODO: write tests for this - // let columnsWithUniqueConstraints = - // try SQLQueryExpression( - // """ - // SELECT "name" FROM pragma_index_list(\(quote: table.tableName, delimiter: .text)) - // WHERE "unique" = 1 AND "origin" <> 'pk' - // """, - // as: String.self - // ) - // .fetchAll(db) - // if !columnsWithUniqueConstraints.isEmpty { - // throw UniqueConstraintDisallowed(table: table, columns: columnsWithUniqueConstraints) - // } - - // // TODO: write tests for this - // let nonNullColumnsWithNoDefault = - // try SQLQueryExpression( - // """ - // SELECT "name" FROM pragma_table_info(\(quote: table.tableName, delimiter: .text)) - // WHERE "notnull" = 1 AND "dflt_value" IS NULL - // """, - // as: String.self - // ) - // .fetchAll(db) - // if !nonNullColumnsWithNoDefault.isEmpty { - // throw NonNullColumnMustHaveDefault(table: table, columns: nonNullColumnsWithNoDefault) - // } + let columnsWithUniqueConstraints = + try SQLQueryExpression( + """ + SELECT "name" FROM pragma_index_list(\(quote: table.tableName, delimiter: .text)) + WHERE "unique" = 1 AND "origin" <> 'pk' + """, + as: String.self + ) + .fetchAll(db) + if !columnsWithUniqueConstraints.isEmpty { + throw SyncEngine.SchemaError( + reason: .uniquenessConstraint, + debugDescription: """ + Uniqueness constraints are not supported for synchronized tables. + """ + ) + } } } } diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKit.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKit.md index 98d0c80e..986fadb9 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKit.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKit.md @@ -183,6 +183,9 @@ CREATE TABLE "reminders" ( ) ``` +> Tip: If you want the database to generate random UUID's in a deterministic fashion for tests +> you can register a custom database function to be used. + #### Primary keys on every table > TL;DR: Each synchronized table must have a single, non-compound primary key to aid in @@ -204,24 +207,6 @@ CREATE TABLE "reminderTags" ( Note that the `id` column might not be needed for your application's logic, but it is necessary to facilitate synchronizing to CloudKit. - +when a ``SyncEngine`` is first created. If a uniqueness constraint is detected an error will be +thrown. #### Foreign key relationships @@ -289,7 +273,7 @@ has been added to the schema, it will populate the table with the cached records #### Adding columns -> TL;DR: When adding columns to a table that has already been deployed to user's devices, you will +> TL;DR: When adding columns to a table that has already been deployed to users' devices, you will either need to make the column nullable, or it can be `NOT NULL` but a default value must be provided with an `ON CONFLICT REPLACE` clause. @@ -491,7 +475,8 @@ exposed for you to query it in whichever way you want. > Important: In order to query the `SyncMetadata` table from your database connection you will need to attach the metadatabase to your database connection. This can be done with the -``GRDB/Database/attachMetadatabase(containerIdentifier:)`` method defined on `Database`. +``GRDB/Database/attachMetadatabase(containerIdentifier:)`` method defined on `Database`. See + for more information on how to do this. With that done you can use the ``StructuredQueriesCore/PrimaryKeyedTable/metadata(for:)`` method to construct a SQL query for fetching the meta data associated with one of your records. @@ -506,6 +491,7 @@ let lastKnownServerRecord = try database.read { db in .metadata(for: remindersListID) .select(\.lastKnownServerRecord) .fetchOne(db) + ?? nil } guard let lastKnownServerRecord else { return } @@ -544,10 +530,32 @@ let ckRecord = try await container.sharedCloudDatabase appropriate to use when fetching the details of a `CKShare` as they are always stored in the shared database. - +It is also possible to join the ``SyncMetadata`` table directly to your tables so that you can +select this additional information on a per-record basis. For example, if you want to select all +reminders lists, along with a boolean that determines if it is shared or not, you can do the +following: + +```swift +@Selection struct Row { + let remindersList: RemindersList + let isShared: Bool +} + +@FetchAll( + RemindersList + .leftJoin(SyncMetadata.all) { $0.recordName.eq($1.recordName) } + .select { + Row.Columns( + remindersList: $0, + isShared: $1.isShared ?? false + ) + } +) +var rows +``` + +Here we have used the ``StructuredQueriesCore/PrimaryKeyedTableDefinition/recordName`` helper that +is defined on all primary key tables so that we can join ``SyncMetadata`` to `RemindersList`. ## How SharingGRDB handles distributed schema scenarios @@ -555,6 +563,62 @@ TODO: finish ## Unit testing and Xcode previews +It is possible to run your features in tests and previews even when using the ``SyncEngine``. You +will need to prepare it for dependencies exactly as you do in the entry point of your app. This +can lead to some code duplication, and so you may want to extract that work to a mutating +`bootstrapDatabase` method on `DependencyValues` like so: + +```swift +extension DependencyValues { + mutating func bootstrapDatabase() throws { + defaultDatabase = try Reminders.appDatabase() + defaultSyncEngine = try SyncEngine( + for: defaultDatabase, + tables: RemindersList.self, + RemindersListAsset.self, + Reminder.self, + Tag.self, + ReminderTag.self + ) + } +} +``` + +Then in your app entry point you can use it like so: + +```swift +@main +struct MyApp: App { + init() { + try! prepareDependencies { + try! $0.bootstrapDatabase() + } + } + + // ... +} +``` + +In tests you can use it like so: + +```swift +@Suite(.dependencies { try! $0.bootstrapDatabase() }) +struct MySuite { + // ... +} +``` + +And in preivews you can use it like so: + +```swift +#Preview { + try! prepareDependencies { + try! $0.bootstrapDatabase() + } + // ... +} +``` + ## Preparing an existing schema for synchronization diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md index c9b68901..19d8efd1 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md @@ -15,7 +15,17 @@ Info.plist with a value of `true`. This is subtly documented in [Apple's documen [Apple's documentation for sharing]: https://developer.apple.com/documentation/cloudkit/sharing-cloudkit-data-with-other-icloud-users#Create-and-Share-a-Topic -TODO: ToC + - [Creating CKShare records](#Creating-CKShare-records) + - [Accepting shared records](#Accepting-shared-records) + - [Diving deeper into sharing](#Diving-deeper-into-sharing) + - [Sharing root records](#Sharing-root-records) + - [Sharing foreign key relationships](#Sharing-foreign-key-relationships) + - [One-to-many relationships](#One-to-many-relationships) + - [Many-to-many relationships](#Many-to-many-relationships) + - [One-to-"at most one" relationships](#One-to-at-most-one-relationships) + - [Sharing permissions](#Sharing-permissions) + - [Controlling what data is shared](#Controlling-what-data-is-shared) + - [Querying share metadata](#Querying-share-metadata) ## Creating CKShare records @@ -353,7 +363,51 @@ it is also the primary key of the table it enforces that at most one cover image ## Sharing permissions -TODO: finish +CloudKit sharing supports permissions so that you can give read-only or read-write access to the +data you share with other users. These permissions are automatically observed by the library and +enforced when writing to your database. If your application tries to write to a record that it +does not have permission for, a `DatabaseError` will be emitted. + +To check for this error you can catch `DatabaseError` and compare its message to +``SyncEngine/writePermissionError``: + +```swift +do { + try await database.write { db in + Reminder.find(id) + .update { $0.title = "Personal" } + .execute(db) + } +} catch let error as DatabaseError where error.message == SyncEngine.writePermissionError { + // User does not have permission to write to this record. +} +``` + +See for more information on accessing the metadata +associationed with your user's data. + +Ideally your app would not allow the user to write to records that they do not have permissions for. +To check their permissions for a record, you can join the root record table to +``SyncMetadata`` and select the ``SyncMetadata/share`` value: + +```swift +let share = try await database.read { db in + RemindersList + .metadata(for: id) + .select(\.share) + .fetchOne(db) + ?? nil +} +guard + share?.currentUserParticipant?.permission == .readWrite + || share?.permission == .readWrite +else { + // User does not have permissions to write to record. + return +} +``` + +This allows you to determine the sharing permissions for a root record. ## Controlling what data is shared diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingPermissionsTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingPermissionsTests.swift index 3207d88b..9a3afcfd 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingPermissionsTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingPermissionsTests.swift @@ -1,5 +1,6 @@ import CloudKit import CustomDump +import GRDB import Foundation import InlineSnapshotTesting import OrderedCollections diff --git a/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift index d0905fdb..1a6c8cd8 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift @@ -49,9 +49,7 @@ extension BaseCloudKitTests { @Test func foreignKeyActionValidation_NoAction() async throws { let error = try #require( await #expect(throws: (any Error).self) { - var configuration = Configuration() - configuration.foreignKeysEnabled = false - let database = try DatabaseQueue(configuration: configuration) + let database = try DatabaseQueue() try await database.write { db in try #sql( """ @@ -110,9 +108,7 @@ extension BaseCloudKitTests { @Test func foreignKeyActionValidation_Restrict() async throws { let error = try #require( await #expect(throws: (any Error).self) { - var configuration = Configuration() - configuration.foreignKeysEnabled = false - let database = try DatabaseQueue(configuration: configuration) + let database = try DatabaseQueue() try await database.write { db in try #sql( """ @@ -213,5 +209,52 @@ extension BaseCloudKitTests { tables: [] ) } + + @Table struct ModelWithUniqueColumn { + let id: Int + let uniqueValue: Int + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + @Test func uniquenessConstraint() async throws { + let error = try #require( + await #expect(throws: (any Error).self) { + let database = try DatabaseQueue() + try await database.write { db in + try #sql( + """ + CREATE TABLE "modelWithUniqueColumns" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "uniqueValue" INTEGER NOT NULL, + UNIQUE("uniqueValue") + ) STRICT + """ + ) + .execute(db) + } + _ = try await SyncEngine( + container: MockCloudContainer( + containerIdentifier: "deadbeef", + privateCloudDatabase: MockCloudDatabase(databaseScope: .private), + sharedCloudDatabase: MockCloudDatabase(databaseScope: .shared) + ), + userDatabase: UserDatabase(database: database), + tables: [ModelWithUniqueColumn.self] + ) + } + ) + assertInlineSnapshot(of: error.localizedDescription, as: .customDump) { + """ + "Could not synchronize data with iCloud." + """ + } + assertInlineSnapshot(of: error, as: .customDump) { + """ + SyncEngine.SchemaError( + reason: .uniquenessConstraint, + debugDescription: "Uniqueness constraints are not supported for synchronized tables." + ) + """ + } + } } } From 6823911fffa98f3b1197bb8bbddbf73cf7593fc3 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 2 Sep 2025 16:35:11 -0500 Subject: [PATCH 2/3] db error --- Sources/SharingGRDBCore/Internal/Exports.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/SharingGRDBCore/Internal/Exports.swift b/Sources/SharingGRDBCore/Internal/Exports.swift index 591037e3..df2a2de6 100644 --- a/Sources/SharingGRDBCore/Internal/Exports.swift +++ b/Sources/SharingGRDBCore/Internal/Exports.swift @@ -2,11 +2,12 @@ @_exported import Sharing @_exported import StructuredQueriesGRDBCore +@_exported import struct GRDB.Configuration @_exported import class GRDB.Database +@_exported import struct GRDB.DatabaseError +@_exported import struct GRDB.DatabaseMigrator @_exported import class GRDB.DatabasePool @_exported import class GRDB.DatabaseQueue @_exported import protocol GRDB.DatabaseReader @_exported import protocol GRDB.DatabaseWriter @_exported import protocol GRDB.ValueObservationScheduler -@_exported import struct GRDB.Configuration -@_exported import struct GRDB.DatabaseMigrator From 08cfadc518c6195a4254784421fcb3285b25a547 Mon Sep 17 00:00:00 2001 From: Brandon Williams Date: Tue, 2 Sep 2025 17:00:32 -0500 Subject: [PATCH 3/3] fixes --- .../SharingGRDBCore/CloudKit/SyncEngine.swift | 134 +++--------------- .../SyncEngineValidationTests.swift | 12 +- 2 files changed, 25 insertions(+), 121 deletions(-) diff --git a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift index d57e8bd1..f78e26dc 100644 --- a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift +++ b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift @@ -158,21 +158,17 @@ tables: [any PrimaryKeyedTable.Type], privateTables: [any PrimaryKeyedTable.Type] = [] ) throws { - let allTables = try userDatabase.read { db in - try SQLQueryExpression( - """ - SELECT "name" FROM "sqlite_master" WHERE "type" = 'table' - """, - as: String.self - ) - .fetchAll(db) - } + let allTables = Set((tables + privateTables).map(HashablePrimaryKeyedTableType.init)) + .map(\.type) + self.tables = allTables + self.privateTables = privateTables + let foreignKeysByTableName = Dictionary( uniqueKeysWithValues: try userDatabase.read { db in try allTables.map { table -> (String, [ForeignKey]) in ( - table, - try ForeignKey.all(table).fetchAll(db) + table.tableName, + try ForeignKey.all(table.tableName).fetchAll(db) ) } } @@ -189,16 +185,11 @@ containerIdentifier: container.containerIdentifier ) ) - let tables = Set((tables + privateTables).map(HashablePrimaryKeyedTableType.init)) - .map(\.type) - self.tables = tables - self.privateTables = privateTables - self.tablesByName = Dictionary(uniqueKeysWithValues: self.tables.map { ($0.tableName, $0) }) self.foreignKeysByTableName = foreignKeysByTableName tablesByOrder = try SharingGRDBCore.tablesByOrder( userDatabase: userDatabase, - tables: tables, + tables: allTables, tablesByName: tablesByName ) try validateSchema() @@ -1878,114 +1869,27 @@ } for table in tables { - // // TODO: write tests for this - // let columnsWithUniqueConstraints = - // try SQLQueryExpression( - // """ - // SELECT "name" FROM pragma_index_list(\(quote: table.tableName, delimiter: .text)) - // WHERE "unique" = 1 AND "origin" <> 'pk' - // """, - // as: String.self - // ) - // .fetchAll(db) - // if !columnsWithUniqueConstraints.isEmpty { - // throw UniqueConstraintDisallowed(table: table, columns: columnsWithUniqueConstraints) - // } - - // // TODO: write tests for this - // let nonNullColumnsWithNoDefault = - // try SQLQueryExpression( - // """ - // SELECT "name" FROM pragma_table_info(\(quote: table.tableName, delimiter: .text)) - // WHERE "notnull" = 1 AND "dflt_value" IS NULL - // """, - // as: String.self - // ) - // .fetchAll(db) - // if !nonNullColumnsWithNoDefault.isEmpty { - // throw NonNullColumnMustHaveDefault(table: table, columns: nonNullColumnsWithNoDefault) - // } - } - } - } - } - -<<<<<<< HEAD - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - private func validateSchema( - tables: [any PrimaryKeyedTable.Type], - foreignKeysByTableName: [String: [ForeignKey]], - userDatabase: UserDatabase - ) throws { - let tableNames = Set(tables.map { $0.tableName }) - for tableName in tableNames { - if tableName.contains(":") { - throw SyncEngine.SchemaError( - reason: .invalidTableName(tableName), - debugDescription: "Table name contains invalid character ':'" - ) - } - } - try userDatabase.read { db in - for (tableName, foreignKeys) in foreignKeysByTableName { - if foreignKeys.count == 1, - let foreignKey = foreignKeys.first, - [.restrict, .noAction].contains(foreignKey.onDelete) - { - throw SyncEngine.SchemaError( - reason: .invalidForeignKeyAction(foreignKey), - debugDescription: """ - Foreign key \(tableName.debugDescription).\(foreignKey.from.debugDescription) action \ - not supported. Must be 'CASCADE', 'SET DEFAULT' or 'SET NULL'. - """ - ) - } - } - - for table in tables { - let columnsWithUniqueConstraints = - try SQLQueryExpression( + let columnsWithUniqueConstraints = + try SQLQueryExpression( """ SELECT "name" FROM pragma_index_list(\(quote: table.tableName, delimiter: .text)) WHERE "unique" = 1 AND "origin" <> 'pk' """, as: String.self - ) - .fetchAll(db) - if !columnsWithUniqueConstraints.isEmpty { - throw SyncEngine.SchemaError( - reason: .uniquenessConstraint, - debugDescription: """ + ) + .fetchAll(db) + if !columnsWithUniqueConstraints.isEmpty { + throw SyncEngine.SchemaError( + reason: .uniquenessConstraint, + debugDescription: """ Uniqueness constraints are not supported for synchronized tables. """ - ) - } + ) + } + } } } } -======= - // TODO: Private, opaque error - // public struct UniqueConstraintDisallowed: Error { - // let localizedDescription: String - // init(table: any PrimaryKeyedTable.Type, columns: [String]) { - // localizedDescription = """ - // Table '\(table.tableName)' has column\(columns.count == 1 ? "" : "s") with unique \ - // constraints: \(columns.map { "'\($0)'" }.joined(separator: ", ")) - // """ - // } - // } - - // TODO: Private, opaque error - // public struct NonNullColumnMustHaveDefault: Error { - // let localizedDescription: String - // init(table: any PrimaryKeyedTable.Type, columns: [String]) { - // localizedDescription = """ - // Table '\(table.tableName)' has non-null column\(columns.count == 1 ? "" : "s") with no \ - // default: \(columns.map { "'\($0)'" }.joined(separator: ", ")) - // """ - // } - // } ->>>>>>> origin/cloudkit private struct HashablePrimaryKeyedTableType: Hashable { let type: any PrimaryKeyedTable.Type diff --git a/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift index a5af96e2..528ec81f 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SyncEngineValidationTests.swift @@ -61,7 +61,7 @@ extension BaseCloudKitTests { .execute(db) try #sql( """ - CREATE TABLE "children" ( + CREATE TABLE "childs" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "parentID" INTEGER REFERENCES "parents"("id") ON DELETE NO ACTION ) STRICT @@ -76,7 +76,7 @@ extension BaseCloudKitTests { sharedCloudDatabase: MockCloudDatabase(databaseScope: .shared) ), userDatabase: UserDatabase(database: database), - tables: [] + tables: [Child.self, Parent.self] ) } ) @@ -98,7 +98,7 @@ extension BaseCloudKitTests { notnull: false ) ), - debugDescription: #"Foreign key "children"."parentID" action not supported. Must be 'CASCADE', 'SET DEFAULT' or 'SET NULL'."# + debugDescription: #"Foreign key "childs"."parentID" action not supported. Must be 'CASCADE', 'SET DEFAULT' or 'SET NULL'."# ) """ } @@ -120,7 +120,7 @@ extension BaseCloudKitTests { .execute(db) try #sql( """ - CREATE TABLE "children" ( + CREATE TABLE "childs" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "parentID" INTEGER REFERENCES "parents"("id") ON DELETE RESTRICT ) STRICT @@ -135,7 +135,7 @@ extension BaseCloudKitTests { sharedCloudDatabase: MockCloudDatabase(databaseScope: .shared) ), userDatabase: UserDatabase(database: database), - tables: [] + tables: [Parent.self, Child.self] ) } ) @@ -157,7 +157,7 @@ extension BaseCloudKitTests { notnull: false ) ), - debugDescription: #"Foreign key "children"."parentID" action not supported. Must be 'CASCADE', 'SET DEFAULT' or 'SET NULL'."# + debugDescription: #"Foreign key "childs"."parentID" action not supported. Must be 'CASCADE', 'SET DEFAULT' or 'SET NULL'."# ) """ }