diff --git a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 587d0c8d..a0d54872 100644 --- a/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Examples/Examples.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "41e7781e6c506773b6af84af513bcd6d3b1be59d635e6c4c4bd89638368e4629", + "originHash" : "c133bf7d10c8ce1e5d6506c3d2f080eac8b4c8c2827044d53a9b925e903564fd", "pins" : [ { "identity" : "combine-schedulers", @@ -73,24 +73,6 @@ "version" : "1.9.4" } }, - { - "identity" : "swift-docc-plugin", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-docc-plugin", - "state" : { - "revision" : "3e4f133a77e644a5812911a0513aeb7288b07d06", - "version" : "1.4.5" - } - }, - { - "identity" : "swift-docc-symbolkit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/swiftlang/swift-docc-symbolkit", - "state" : { - "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", - "version" : "1.0.0" - } - }, { "identity" : "swift-identified-collections", "kind" : "remoteSourceControl", diff --git a/Examples/Reminders/RemindersLists.swift b/Examples/Reminders/RemindersLists.swift index 8f3f807d..710aa677 100644 --- a/Examples/Reminders/RemindersLists.swift +++ b/Examples/Reminders/RemindersLists.swift @@ -13,7 +13,7 @@ class RemindersListsModel { .group(by: \.id) .order(by: \.position) .leftJoin(Reminder.all) { $0.id.eq($1.remindersListID) && !$1.isCompleted } - .leftJoin(SyncMetadata.all) { $0.hasMetadata(in: $2) } + .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($2.id) } .select { ReminderListState.Columns( remindersCount: $1.id.count(), diff --git a/Sources/SQLiteData/CloudKit/SyncMetadata.swift b/Sources/SQLiteData/CloudKit/SyncMetadata.swift index 44672565..1f62fc57 100644 --- a/Sources/SQLiteData/CloudKit/SyncMetadata.swift +++ b/Sources/SQLiteData/CloudKit/SyncMetadata.swift @@ -11,12 +11,25 @@ /// See for more info. @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) @Table("sqlitedata_icloud_metadata") - public struct SyncMetadata: Hashable, Sendable { + public struct SyncMetadata: Hashable, Identifiable, Sendable { + /// A selection of columns representing a synchronized record's unique identifier and type. + @Selection + public struct ID: Hashable, Sendable { + /// The unique identifier of the record synchronized. + public var recordPrimaryKey: String + + /// The type of the record synchronized, _i.e._ its table name. + public var recordType: String + } + + /// The unique identifier and type of the record synchronized. + public let id: ID + /// The unique identifier of the record synchronized. - public var recordPrimaryKey: String + public var recordPrimaryKey: String { id.recordPrimaryKey } /// The type of the record synchronized, _i.e._ its table name. - public var recordType: String + public var recordType: String { id.recordType } /// The record zone name. public var zoneName: String @@ -35,11 +48,25 @@ @Column(generated: .virtual) public let recordName: String + /// A selection of columns representing a synchronized parent record's unique identifier and + /// type. + @Selection + public struct ParentID: Hashable, Sendable { + /// The unique identifier of the parent record synchronized. + public var parentRecordPrimaryKey: String + + /// The type of the parent record synchronized, _i.e._ its table name. + public var parentRecordType: String + } + + /// The identifier and type of this record's parent, if any. + public var parentRecordID: ParentID? + /// The unique identifier of this record's parent, if any. - public var parentRecordPrimaryKey: String? + public var parentRecordPrimaryKey: String? { parentRecordID?.parentRecordPrimaryKey } /// The type of this record's parent, _i.e._ its table name, if any. - public var parentRecordType: String? + public var parentRecordType: String? { parentRecordID?.parentRecordType } /// The name of this record's parent, if any. /// @@ -85,6 +112,25 @@ public var userModificationTime: Int64 } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension SyncMetadata.TableColumns { + public var recordPrimaryKey: TableColumn { + id.recordPrimaryKey + } + + public var recordType: TableColumn { + id.recordType + } + + public var parentRecordPrimaryKey: TableColumn { + parentRecordID.parentRecordPrimaryKey + } + + public var parentRecordType: TableColumn { + parentRecordID.parentRecordType + } + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SyncMetadata { package init( @@ -99,16 +145,18 @@ share: CKShare? = nil, userModificationTime: Int64 ) { - self.recordPrimaryKey = recordPrimaryKey - self.recordType = recordType + self.id = ID(recordPrimaryKey: recordPrimaryKey, recordType: recordType) self.recordName = "\(recordPrimaryKey):\(recordType)" self.zoneName = zoneName self.ownerName = ownerName - self.parentRecordPrimaryKey = parentRecordPrimaryKey - self.parentRecordType = parentRecordType if let parentRecordPrimaryKey, let parentRecordType { + self.parentRecordID = ParentID( + parentRecordPrimaryKey: parentRecordPrimaryKey, + parentRecordType: parentRecordType + ) self.parentRecordName = "\(parentRecordPrimaryKey):\(parentRecordType)" } else { + self.parentRecordID = nil self.parentRecordName = nil } self.lastKnownServerRecord = lastKnownServerRecord @@ -139,10 +187,11 @@ } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - extension PrimaryKeyedTable where PrimaryKey: IdentifierStringConvertible { + extension PrimaryKeyedTable where PrimaryKey.QueryOutput: IdentifierStringConvertible { /// A query for finding the metadata associated with a record. /// /// - Parameter primaryKey: The primary key of the record whose metadata to look up. + @available(*, deprecated, message: "Use 'SyncMetadata.find(record.syncMetadataID)', instead") public static func metadata(for primaryKey: PrimaryKey.QueryOutput) -> Where { SyncMetadata.where { #sql( @@ -153,13 +202,15 @@ ) } } - } - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - extension PrimaryKeyedTable where PrimaryKey.QueryOutput: IdentifierStringConvertible { - /// Constructs a ``SyncMetadata/RecordName-swift.struct`` for a primary keyed table give an ID. - /// - /// - Parameter id: The ID of the record. + /// An identifier representing any associated synchronization metadata. + public var syncMetadataID: SyncMetadata.ID { + SyncMetadata.ID( + recordPrimaryKey: primaryKey.rawIdentifier, + recordType: Self.tableName + ) + } + package static func recordName(for id: PrimaryKey.QueryOutput) -> String { "\(id.rawIdentifier):\(tableName)" } @@ -179,10 +230,29 @@ /// RemindersList /// .leftJoin(SyncMetadata.all) { $0.hasMetadata.in($1) } /// ``` + @available( + *, + deprecated, + message: """ + Join the 'SyncMetadata' table using 'SyncMetadata.id' and 'Table.syncMetadataID', instead. + """ + ) public func hasMetadata(in metadata: SyncMetadata.TableColumns) -> some QueryExpression { metadata.recordType.eq(QueryValue.tableName) && #sql("\(primaryKey)").eq(metadata.recordPrimaryKey) } + + /// An identifier representing any associated synchronization metadata. + /// + /// This helper can be useful when joining your tables to the ``SyncMetadata`` table: + /// + /// ```swift + /// RemindersList + /// .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($1.id) } + /// ``` + public var syncMetadataID: some QueryExpression { + #sql("\(primaryKey), \(bind: QueryValue.tableName)") + } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) diff --git a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md index 6983cc68..e7836d10 100644 --- a/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md +++ b/Sources/SQLiteData/Documentation.docc/Articles/CloudKit.md @@ -541,8 +541,8 @@ to attach the metadatabase to your database connection. This can be done with th ``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. +With that done you can use the ``StructuredQueriesCore/PrimaryKeyedTable/syncMetadataID`` property +to construct a SQL query for fetching the metadata associated with one of your records. For example, if you want to retrieve the `CKRecord` that is associated with a particular row in one of your tables, say a reminder, then you can use ``SyncMetadata/lastKnownServerRecord`` to @@ -550,8 +550,8 @@ retrieve the `CKRecord` and then invoke a CloudKit database function to retrieve ```swift let lastKnownServerRecord = try database.read { db in - try RemindersList - .metadata(for: remindersListID) + try SyncMetadata + .find(remindersList.syncMetadataID) .select(\.lastKnownServerRecord) .fetchOne(db) ?? nil @@ -578,7 +578,7 @@ will give you access to the most current list of participants and permissions fo ```swift let share = try database.read { db in try RemindersList - .metadata(for: remindersListID) + .find(remindersList.syncMetadataID) .select(\.share) .fetchOne(db) } @@ -606,7 +606,7 @@ following: @FetchAll( RemindersList - .leftJoin(SyncMetadata.all) { $0.hasMetadata(in: $1) } + .leftJoin(SyncMetadata.all) { $0.syncMetadataID.eq($1.id) } .select { Row.Columns( remindersList: $0, @@ -617,7 +617,7 @@ following: var rows ``` -Here we have used the ``StructuredQueriesCore/PrimaryKeyedTableDefinition/hasMetadata(in:)`` helper +Here we have used the ``StructuredQueriesCore/PrimaryKeyedTableDefinition/syncMetadataID`` helper that is defined on all primary key tables so that we can join ``SyncMetadata`` to `RemindersList`.