diff --git a/Examples/Reminders/ReminderForm.swift b/Examples/Reminders/ReminderForm.swift index 4fc55ec0..4f37515b 100644 --- a/Examples/Reminders/ReminderForm.swift +++ b/Examples/Reminders/ReminderForm.swift @@ -184,8 +184,8 @@ struct ReminderFormView: View { } .execute(db) } + dismiss() } - dismiss() } } diff --git a/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift b/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift index cb8cad7d..fc4e2e90 100644 --- a/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift +++ b/Sources/SharingGRDBCore/CloudKit/Metadatabase.swift @@ -63,6 +63,7 @@ func defaultMetadatabase( "share" BLOB, "isShared" INTEGER NOT NULL AS ("share" IS NOT NULL), "userModificationDate" TEXT NOT NULL DEFAULT (\(.datetime())), + "_isDeleted" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY ("recordPrimaryKey", "recordType"), UNIQUE ("recordName") diff --git a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift index 4ae48eab..005b4794 100644 --- a/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift +++ b/Sources/SharingGRDBCore/CloudKit/SyncEngine.swift @@ -24,8 +24,8 @@ @Sendable (any DatabaseReader, SyncEngine) -> (private: any SyncEngineProtocol, shared: any SyncEngineProtocol) package let container: any CloudContainer - let dataManager = Dependency(\.dataManager) + public static let writePermissionError = "co.pointfree.sqlitedata-icloud.write-permission-error" public convenience init( for database: any DatabaseWriter, @@ -33,13 +33,15 @@ privateTables: repeat (each T2).Type, containerIdentifier: String? = nil, defaultZone: CKRecordZone = CKRecordZone(zoneName: "co.pointfree.SQLiteData.defaultZone"), - logger: Logger = isTesting ? Logger(.disabled) : Logger(subsystem: "SQLiteData", category: "CloudKit") + logger: Logger = isTesting + ? Logger(.disabled) : Logger(subsystem: "SQLiteData", category: "CloudKit") ) throws where repeat (each T1).PrimaryKey.QueryOutput: IdentifierStringConvertible, repeat (each T2).PrimaryKey.QueryOutput: IdentifierStringConvertible { - let containerIdentifier = containerIdentifier + let containerIdentifier = + containerIdentifier ?? ModelConfiguration(groupContainer: .automatic).cloudKitContainerIdentifier var allTables: [any PrimaryKeyedTable.Type] = [] @@ -253,6 +255,7 @@ db.add(function: .syncEngineIsSynchronizingChanges) db.add(function: .didUpdate(syncEngine: self)) db.add(function: .didDelete(syncEngine: self)) + db.add(function: .hasPermission) for trigger in SyncMetadata.callbackTriggers { try trigger.execute(db) @@ -407,6 +410,7 @@ for trigger in SyncMetadata.callbackTriggers.reversed() { try trigger.drop().execute(db) } + db.remove(function: .hasPermission) db.remove(function: .didDelete(syncEngine: self)) db.remove(function: .didUpdate(syncEngine: self)) db.remove(function: .syncEngineIsSynchronizingChanges) @@ -455,21 +459,23 @@ ) } - func didDelete(recordName: String, zoneID: CKRecordZone.ID?) { + func didDelete(recordName: String, zoneID: CKRecordZone.ID?, share: CKShare?) { let zoneID = zoneID ?? defaultZone.zoneID let syncEngine = self.syncEngines.withValue { zoneID.ownerName == CKCurrentUserDefaultName ? $0.private : $0.shared } - syncEngine?.state.add( - pendingRecordZoneChanges: [ - .deleteRecord( - CKRecord.ID( - recordName: recordName, - zoneID: zoneID - ) + var changes: [CKSyncEngine.PendingRecordZoneChange] = [ + .deleteRecord( + CKRecord.ID( + recordName: recordName, + zoneID: zoneID ) - ] - ) + ) + ] + if let share { + changes.append(.deleteRecord(share.recordID)) + } + syncEngine?.state.add(pendingRecordZoneChanges: changes) } // TODO: Possible to get test coverage on this? @@ -594,11 +600,11 @@ options: CKSyncEngine.SendChangesOptions = CKSyncEngine.SendChangesOptions(scope: .all), syncEngine: any SyncEngineProtocol ) async -> CKSyncEngine.RecordZoneChangeBatch? { - let allChanges = syncEngine.state.pendingRecordZoneChanges.filter(options.scope.contains) - guard !allChanges.isEmpty + var changes = await pendingRecordZoneChanges(options: options, syncEngine: syncEngine) + guard !changes.isEmpty else { return nil } - let changes = allChanges.sorted { lhs, rhs in + changes.sort { lhs, rhs in switch (lhs, rhs) { case (.saveRecord(let lhs), .saveRecord(let rhs)): guard @@ -753,6 +759,101 @@ return batch } + private func pendingRecordZoneChanges( + options: CKSyncEngine.SendChangesOptions, + syncEngine: any SyncEngineProtocol + ) async -> [CKSyncEngine.PendingRecordZoneChange] { + var changes = syncEngine.state.pendingRecordZoneChanges.filter(options.scope.contains) + guard !changes.isEmpty + else { return [] } + + let deletedRecordIDs: [CKRecord.ID] = changes.compactMap { + switch $0 { + case .saveRecord(_): + return nil + case .deleteRecord(let recordID): + return recordID + @unknown default: + return nil + } + } + let deletedRecordNames = deletedRecordIDs.map(\.recordName) + + let (metadataOfDeletions, recordsWithRoot): ([SyncMetadata], [RecordWithRoot]) = + await withErrorReporting { + try await userDatabase.read { db in + let metadataOfDeletions = try SyncMetadata.where { + $0.recordName.in(deletedRecordNames) + } + .fetchAll(db) + + let recordsWithRoot = + try With { + SyncMetadata + .where { $0.parentRecordName.is(nil) && $0.recordName.in(deletedRecordNames) } + .select { + RecordWithRoot.Columns( + parentRecordName: $0.parentRecordName, + recordName: $0.recordName, + lastKnownServerRecord: $0.lastKnownServerRecord, + rootRecordName: $0.recordName, + rootLastKnownServerRecord: $0.lastKnownServerRecord + ) + } + .union( + all: true, + SyncMetadata + .join(RecordWithRoot.all) { $1.recordName.is($0.parentRecordName) } + .select { metadata, tree in + RecordWithRoot.Columns( + parentRecordName: metadata.parentRecordName, + recordName: metadata.recordName, + lastKnownServerRecord: metadata.lastKnownServerRecord, + rootRecordName: tree.rootRecordName, + rootLastKnownServerRecord: tree.lastKnownServerRecord + ) + } + ) + } query: { + RecordWithRoot + .where { $0.recordName.in(deletedRecordNames) } + } + .fetchAll(db) + + return (metadataOfDeletions, recordsWithRoot) + } + } + ?? ([], []) + + let shareRecordIDsToDelete = metadataOfDeletions.compactMap(\.share?.recordID) + + for recordWithRoot in recordsWithRoot { + guard + let lastKnownServerRecord = recordWithRoot.lastKnownServerRecord, + let rootLastKnownServerRecord = recordWithRoot.rootLastKnownServerRecord + else { continue } + guard let rootShareRecordID = rootLastKnownServerRecord.share?.recordID + else { continue } + guard shareRecordIDsToDelete.contains(rootShareRecordID) + else { continue } + changes.removeAll(where: { $0 == .deleteRecord(lastKnownServerRecord.recordID) }) + syncEngine.state.remove( + pendingRecordZoneChanges: [.deleteRecord(lastKnownServerRecord.recordID)] + ) + } + + await withErrorReporting { + try await userDatabase.write { db in + try SyncMetadata + .where { $0.recordName.in(deletedRecordNames) } + .delete() + .execute(db) + } + } + + return changes + } + package func handleAccountChange( changeType: CKSyncEngine.Event.AccountChange.ChangeType, syncEngine: any SyncEngineProtocol @@ -1152,6 +1253,7 @@ } private func cacheShare(_ share: CKShare) async throws { + // TODO: Instead of getting URL here we can make `shareMetadata(…)` take a share instead of a URL guard let url = share.url else { return } @@ -1416,7 +1518,7 @@ @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) extension DatabaseFunction { fileprivate static func didUpdate(syncEngine: SyncEngine) -> Self { - Self("didUpdate") { recordName, zoneID in + Self("didUpdate") { recordName, zoneID, _ in syncEngine.didUpdate( recordName: recordName, zoneID: zoneID @@ -1425,8 +1527,13 @@ } fileprivate static func didDelete(syncEngine: SyncEngine) -> Self { - return Self("didDelete") { recordName, zoneID in - syncEngine.didDelete(recordName: recordName, zoneID: zoneID) + return Self("didDelete") { recordName, zoneID, share in + syncEngine + .didDelete( + recordName: recordName, + zoneID: zoneID, + share: share + ) } } @@ -1442,6 +1549,22 @@ } } + fileprivate static var hasPermission: Self { + Self(.sqliteDataCloudKitSchemaName + "_hasPermission", argumentCount: 1) { arguments in + let share = try Data.fromDatabaseValue(arguments[0]).flatMap { + let coder = try NSKeyedUnarchiver(forReadingFrom: $0) + coder.requiresSecureCoding = true + return CKShare(coder: coder) + } + guard let share + else { return true } + let hasPermission = + share.publicPermission == .readWrite + || share.currentUserParticipant?.permission == .readWrite + return hasPermission + } + } + fileprivate static var syncEngineIsSynchronizingChanges: Self { Self( .sqliteDataCloudKitSchemaName + "_" + "syncEngineIsSynchronizingChanges", @@ -1454,9 +1577,9 @@ private convenience init( _ name: String, - function: @escaping @Sendable (String, CKRecordZone.ID?) -> Void + function: @escaping @Sendable (String, CKRecordZone.ID?, CKShare?) -> Void ) { - self.init(.sqliteDataCloudKitSchemaName + "_" + name, argumentCount: 2) { arguments in + self.init(.sqliteDataCloudKitSchemaName + "_" + name, argumentCount: 3) { arguments in guard let recordName = String.fromDatabaseValue(arguments[0]) else { @@ -1467,7 +1590,14 @@ coder.requiresSecureCoding = true return CKRecord(coder: coder)?.recordID.zoneID } - function(recordName, zoneID) + + let share = try Data.fromDatabaseValue(arguments[2]).flatMap { + let coder = try NSKeyedUnarchiver(forReadingFrom: $0) + coder.requiresSecureCoding = true + return CKShare(coder: coder) + } + + function(recordName, zoneID, share) return nil } } @@ -1493,7 +1623,8 @@ else { return URL(string: "file:\(String.sqliteDataCloudKitSchemaName)?mode=memory&cache=shared")! } - return databaseURL + return + databaseURL .deletingLastPathComponent() .appending(component: ".\(databaseURL.deletingPathExtension().lastPathComponent)") .appendingPathExtension("metadata\(containerIdentifier.map { "-\($0)" } ?? "").sqlite") @@ -1561,7 +1692,8 @@ /// - Parameter containerIdentifier: The identifier of the CloudKit container used to synchronize /// data. public func attachMetadatabase(containerIdentifier: String? = nil) throws { - let containerIdentifier = containerIdentifier + let containerIdentifier = + containerIdentifier ?? ModelConfiguration(groupContainer: .automatic).cloudKitContainerIdentifier guard let containerIdentifier else { diff --git a/Sources/SharingGRDBCore/CloudKit/SyncMetadata+MacroExpansion.swift b/Sources/SharingGRDBCore/CloudKit/SyncMetadata+MacroExpansion.swift index d6928a25..52ae476f 100644 --- a/Sources/SharingGRDBCore/CloudKit/SyncMetadata+MacroExpansion.swift +++ b/Sources/SharingGRDBCore/CloudKit/SyncMetadata+MacroExpansion.swift @@ -48,6 +48,12 @@ keyPath: \QueryValue.isShared ) } + public var _isDeleted: StructuredQueriesCore.TableColumn { + StructuredQueriesCore.TableColumn( + "_isDeleted", + keyPath: \QueryValue._isDeleted + ) + } public let userModificationDate = StructuredQueriesCore.TableColumn( "userModificationDate", keyPath: \QueryValue.userModificationDate @@ -59,7 +65,8 @@ QueryValue.columns.parentRecordType, QueryValue.columns.parentRecordName, QueryValue.columns.lastKnownServerRecord, QueryValue.columns._lastKnownServerRecordAllFields, QueryValue.columns.share, - QueryValue.columns.isShared, QueryValue.columns.userModificationDate, + QueryValue.columns.isShared, QueryValue.columns._isDeleted, + QueryValue.columns.userModificationDate, ] } public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { @@ -68,11 +75,12 @@ QueryValue.columns.parentRecordPrimaryKey, QueryValue.columns.parentRecordType, QueryValue.columns.lastKnownServerRecord, QueryValue.columns._lastKnownServerRecordAllFields, QueryValue.columns.share, + QueryValue.columns._isDeleted, QueryValue.columns.userModificationDate, ] } public var queryFragment: QueryFragment { - "\(self.recordPrimaryKey), \(self.recordType), \(self.recordName), \(self.parentRecordPrimaryKey), \(self.parentRecordType), \(self.parentRecordName), \(self.lastKnownServerRecord), \(self._lastKnownServerRecordAllFields), \(self.share), \(self.isShared), \(self.userModificationDate)" + "\(self.recordPrimaryKey), \(self.recordType), \(self.recordName), \(self.parentRecordPrimaryKey), \(self.parentRecordType), \(self.parentRecordName), \(self.lastKnownServerRecord), \(self._lastKnownServerRecordAllFields), \(self.share), \(self.isShared), \(self._isDeleted), \(self.userModificationDate)" } } } @@ -98,6 +106,7 @@ ) let share = try decoder.decode(CKShare?.SystemFieldsRepresentation.self) let isShared = try decoder.decode(Bool.self) + let _isDeleted = try decoder.decode(Bool.self) let userModificationDate = try decoder.decode(Date.self) guard let recordPrimaryKey else { throw QueryDecodingError.missingRequiredColumn @@ -120,6 +129,9 @@ guard let isShared else { throw QueryDecodingError.missingRequiredColumn } + guard let _isDeleted else { + throw QueryDecodingError.missingRequiredColumn + } guard let userModificationDate else { throw QueryDecodingError.missingRequiredColumn } @@ -130,6 +142,7 @@ self._lastKnownServerRecordAllFields = _lastKnownServerRecordAllFields self.share = share self.isShared = isShared + self._isDeleted = _isDeleted self.userModificationDate = userModificationDate } } @@ -214,4 +227,153 @@ self.lastKnownServerRecord = lastKnownServerRecord } } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension RecordWithRoot { + public struct Columns: StructuredQueriesCore.QueryExpression { + public typealias QueryValue = RecordWithRoot + public let queryFragment: StructuredQueriesCore.QueryFragment + public init( + parentRecordName: some StructuredQueriesCore.QueryExpression, + recordName: some StructuredQueriesCore.QueryExpression, + lastKnownServerRecord: some StructuredQueriesCore.QueryExpression< + CKRecord?.SystemFieldsRepresentation + >, + rootRecordName: some StructuredQueriesCore.QueryExpression, + rootLastKnownServerRecord: some StructuredQueriesCore.QueryExpression< + CKRecord?.SystemFieldsRepresentation + > + ) { + self.queryFragment = """ + \(parentRecordName.queryFragment) AS "parentRecordName", \(recordName.queryFragment) AS "recordName", \(lastKnownServerRecord.queryFragment) AS "lastKnownServerRecord", \(rootRecordName.queryFragment) AS "rootRecordName", \(rootLastKnownServerRecord.queryFragment) AS "rootLastKnownServerRecord" + """ + } + } + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = RecordWithRoot + public let parentRecordName = StructuredQueriesCore.TableColumn( + "parentRecordName", + keyPath: \QueryValue.parentRecordName + ) + public let recordName = StructuredQueriesCore.TableColumn( + "recordName", + keyPath: \QueryValue.recordName + ) + public let lastKnownServerRecord = StructuredQueriesCore.TableColumn< + QueryValue, CKRecord?.SystemFieldsRepresentation + >("lastKnownServerRecord", keyPath: \QueryValue.lastKnownServerRecord) + public let rootRecordName = StructuredQueriesCore.TableColumn( + "rootRecordName", + keyPath: \QueryValue.rootRecordName + ) + public let rootLastKnownServerRecord = StructuredQueriesCore.TableColumn< + QueryValue, CKRecord?.SystemFieldsRepresentation + >("rootLastKnownServerRecord", keyPath: \QueryValue.rootLastKnownServerRecord) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + [ + QueryValue.columns.parentRecordName, QueryValue.columns.recordName, + QueryValue.columns.lastKnownServerRecord, QueryValue.columns.rootRecordName, + QueryValue.columns.rootLastKnownServerRecord, + ] + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + [ + QueryValue.columns.parentRecordName, QueryValue.columns.recordName, + QueryValue.columns.lastKnownServerRecord, QueryValue.columns.rootRecordName, + QueryValue.columns.rootLastKnownServerRecord, + ] + } + public var queryFragment: QueryFragment { + "\(self.parentRecordName), \(self.recordName), \(self.lastKnownServerRecord), \(self.rootRecordName), \(self.rootLastKnownServerRecord)" + } + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + nonisolated extension RecordWithRoot: StructuredQueriesCore.Table { + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var tableName: String { + "recordWithRoots" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + self.parentRecordName = try decoder.decode(String.self) + let recordName = try decoder.decode(String.self) + let lastKnownServerRecord = try decoder.decode(CKRecord?.SystemFieldsRepresentation.self) + let rootRecordName = try decoder.decode(String.self) + let rootLastKnownServerRecord = try decoder.decode(CKRecord?.SystemFieldsRepresentation.self) + guard let recordName else { + throw QueryDecodingError.missingRequiredColumn + } + guard let lastKnownServerRecord else { + throw QueryDecodingError.missingRequiredColumn + } + guard let rootRecordName else { + throw QueryDecodingError.missingRequiredColumn + } + guard let rootLastKnownServerRecord else { + throw QueryDecodingError.missingRequiredColumn + } + self.recordName = recordName + self.lastKnownServerRecord = lastKnownServerRecord + self.rootRecordName = rootRecordName + self.rootLastKnownServerRecord = rootLastKnownServerRecord + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension RootShare { + public struct Columns: StructuredQueriesCore.QueryExpression { + public typealias QueryValue = RootShare + public let queryFragment: StructuredQueriesCore.QueryFragment + public init( + parentRecordName: some StructuredQueriesCore.QueryExpression, + share: some StructuredQueriesCore.QueryExpression + ) { + self.queryFragment = """ + \(parentRecordName.queryFragment) AS "parentRecordName", \(share.queryFragment) AS "share" + """ + } + } + + public nonisolated struct TableColumns: StructuredQueriesCore.TableDefinition { + public typealias QueryValue = RootShare + public let parentRecordName = StructuredQueriesCore.TableColumn( + "parentRecordName", + keyPath: \QueryValue.parentRecordName + ) + public let share = StructuredQueriesCore.TableColumn< + QueryValue, CKShare?.SystemFieldsRepresentation + >("share", keyPath: \QueryValue.share) + public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] { + [QueryValue.columns.parentRecordName, QueryValue.columns.share] + } + public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] { + [QueryValue.columns.parentRecordName, QueryValue.columns.share] + } + public var queryFragment: QueryFragment { + "\(self.parentRecordName), \(self.share)" + } + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + nonisolated extension RootShare: StructuredQueriesCore.Table { + public nonisolated static var columns: TableColumns { + TableColumns() + } + public nonisolated static var tableName: String { + "rootShares" + } + public nonisolated init(decoder: inout some StructuredQueriesCore.QueryDecoder) throws { + self.parentRecordName = try decoder.decode(String.self) + let share = try decoder.decode(CKShare?.SystemFieldsRepresentation.self) + guard let share else { + throw QueryDecodingError.missingRequiredColumn + } + self.share = share + } + } + #endif diff --git a/Sources/SharingGRDBCore/CloudKit/SyncMetadata.swift b/Sources/SharingGRDBCore/CloudKit/SyncMetadata.swift index 85676907..3a4833a8 100644 --- a/Sources/SharingGRDBCore/CloudKit/SyncMetadata.swift +++ b/Sources/SharingGRDBCore/CloudKit/SyncMetadata.swift @@ -60,6 +60,10 @@ // @Column(as: CKShare?.SystemFieldsRepresentation.self) public var share: CKShare? + /// Determines if the metadata has been "soft" deleted. It will be fully deleted once the + /// next batch of pending changes is processed. + public var _isDeleted = false + // @Column(generated: .virtual) public let isShared: Bool @@ -76,6 +80,26 @@ let lastKnownServerRecord: CKRecord? } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + // @Table @Selection + struct RecordWithRoot { + let parentRecordName: String? + let recordName: String + // @Column(as: CKRecord?.SystemFieldsRepresentation.self) + let lastKnownServerRecord: CKRecord? + let rootRecordName: String + // @Column(as: CKRecord?.SystemFieldsRepresentation.self) + let rootLastKnownServerRecord: CKRecord? + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + // @Table @Selection + struct RootShare { + let parentRecordName: String? + // @Column(as: CKShare?.SystemFieldsRepresentation.self) + let share: CKShare? + } + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) extension SyncMetadata { package init( diff --git a/Sources/SharingGRDBCore/CloudKit/Triggers.swift b/Sources/SharingGRDBCore/CloudKit/Triggers.swift index e2dacc02..65f11912 100644 --- a/Sources/SharingGRDBCore/CloudKit/Triggers.swift +++ b/Sources/SharingGRDBCore/CloudKit/Triggers.swift @@ -1,190 +1,280 @@ #if canImport(CloudKit) -import CloudKit -import Foundation - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -extension PrimaryKeyedTable { - static func metadataTriggers(parentForeignKey: ForeignKey?) -> [TemporaryTrigger] { - [ - afterInsert(parentForeignKey: parentForeignKey), - afterUpdate(parentForeignKey: parentForeignKey), - afterDelete, - ] + import CloudKit + import Foundation + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension PrimaryKeyedTable { + static func metadataTriggers(parentForeignKey: ForeignKey?) -> [TemporaryTrigger] { + [ + afterInsert(parentForeignKey: parentForeignKey), + afterUpdate(parentForeignKey: parentForeignKey), + afterDeleteFromUser(parentForeignKey: parentForeignKey), + afterDeleteFromSyncEngine, + ] + } + + fileprivate static func afterInsert(parentForeignKey: ForeignKey?) -> TemporaryTrigger { + createTemporaryTrigger( + "\(String.sqliteDataCloudKitSchemaName)_after_insert_on_\(tableName)", + ifNotExists: true, + after: .insert { new in + checkWritePermissions(alias: new, parentForeignKey: parentForeignKey) + SyncMetadata.upsert(new: new, parentForeignKey: parentForeignKey) + } + ) + } + + fileprivate static func afterUpdate(parentForeignKey: ForeignKey?) -> TemporaryTrigger { + createTemporaryTrigger( + "\(String.sqliteDataCloudKitSchemaName)_after_update_on_\(tableName)", + ifNotExists: true, + after: .update { _, new in + checkWritePermissions(alias: new, parentForeignKey: parentForeignKey) + SyncMetadata.upsert(new: new, parentForeignKey: parentForeignKey) + } + ) + } + + fileprivate static func afterDeleteFromUser(parentForeignKey: ForeignKey?) -> TemporaryTrigger< + Self + > { + createTemporaryTrigger( + "\(String.sqliteDataCloudKitSchemaName)_after_delete_on_\(tableName)_from_user", + ifNotExists: true, + after: .delete { old in + checkWritePermissions(alias: old, parentForeignKey: parentForeignKey) + SyncMetadata + .where { + $0.recordPrimaryKey.eq(SQLQueryExpression("\(old.primaryKey)")) + && $0.recordType.eq(tableName) + } + .update { $0._isDeleted = true } + } when: { _ in + !SyncEngine.isSynchronizingChanges() + } + ) + } + + fileprivate static var afterDeleteFromSyncEngine: TemporaryTrigger { + createTemporaryTrigger( + "\(String.sqliteDataCloudKitSchemaName)_after_delete_on_\(tableName)_from_sync_engine", + ifNotExists: true, + after: .delete { old in + SyncMetadata + .where { + $0.recordPrimaryKey.eq(SQLQueryExpression("\(old.primaryKey)")) + && $0.recordType.eq(tableName) + } + .delete() + } when: { _ in + SyncEngine.isSynchronizingChanges() + } + ) + } + } + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension SyncMetadata { + fileprivate static func upsert( + new: StructuredQueriesCore.TableAlias.TableColumns, + parentForeignKey: ForeignKey?, + ) -> some StructuredQueriesCore.Statement { + let (parentRecordPrimaryKey, parentRecordType) = parentFields( + alias: new, + parentForeignKey: parentForeignKey + ) + return insert { + ($0.recordPrimaryKey, $0.recordType, $0.parentRecordPrimaryKey, $0.parentRecordType) + } select: { + Values( + SQLQueryExpression("\(new.primaryKey)"), + T.tableName, + SQLQueryExpression(parentRecordPrimaryKey), + SQLQueryExpression(parentRecordType) + ) + } onConflict: { + ($0.recordPrimaryKey, $0.recordType) + } doUpdate: { + $0.parentRecordPrimaryKey = $1.parentRecordPrimaryKey + $0.parentRecordType = $1.parentRecordType + $0.userModificationDate = $1.userModificationDate + } + } } - fileprivate static func afterInsert(parentForeignKey: ForeignKey?) -> TemporaryTrigger { - createTemporaryTrigger( - "\(String.sqliteDataCloudKitSchemaName)_after_insert_on_\(tableName)", + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + extension SyncMetadata { + static var callbackTriggers: [TemporaryTrigger] { + [ + afterInsertTrigger, + afterUpdateTrigger, + afterSoftDeleteTrigger, + ] + } + + private enum ParentSyncMetadata: AliasName {} + + fileprivate static let afterInsertTrigger = createTemporaryTrigger( + "after_insert_on_sqlitedata_icloud_metadata", ifNotExists: true, - after: .insert { new in SyncMetadata.upsert(new: new, parentForeignKey: parentForeignKey) } + after: .insert { new in + Values(.didUpdate(new)) + } when: { _ in + !SyncEngine.isSynchronizingChanges() + } ) - } - fileprivate static func afterUpdate(parentForeignKey: ForeignKey?) -> TemporaryTrigger { - createTemporaryTrigger( - "\(String.sqliteDataCloudKitSchemaName)_after_update_on_\(tableName)", + fileprivate static let afterUpdateTrigger = createTemporaryTrigger( + "after_update_on_sqlitedata_icloud_metadata", ifNotExists: true, - after: .update { _, new in SyncMetadata.upsert(new: new, parentForeignKey: parentForeignKey) } + after: .update { _, new in + Values(.didUpdate(new)) + } when: { old, new in + old._isDeleted.eq(new._isDeleted) && !SyncEngine.isSynchronizingChanges() + } ) - } - fileprivate static var afterDelete: TemporaryTrigger { - createTemporaryTrigger( - "\(String.sqliteDataCloudKitSchemaName)_after_delete_on_\(tableName)", + fileprivate static let afterSoftDeleteTrigger = createTemporaryTrigger( + "after_delete_on_sqlitedata_icloud_metadata", ifNotExists: true, - after: .delete { old in - SyncMetadata - .where { - $0.recordPrimaryKey.eq(SQLQueryExpression("\(old.primaryKey)")) - && $0.recordType.eq(tableName) - } - .delete() + after: .update(of: \._isDeleted) { _, new in + Values( + .didDelete( + recordName: new.recordName, + lastKnownServerRecord: new.lastKnownServerRecord + ?? rootServerRecord(recordName: new.recordName), + share: new.share + ) + ) + } when: { old, new in + !old._isDeleted && new._isDeleted && !SyncEngine.isSynchronizingChanges() } ) } -} - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -extension SyncMetadata { - fileprivate static func upsert( - new: TemporaryTrigger.Operation.New, - parentForeignKey: ForeignKey?, - ) -> some StructuredQueriesCore.Statement { - let (parentRecordPrimaryKey, parentRecordType): (QueryFragment, QueryFragment) = - parentForeignKey - .map { (#""new".\#(quote: $0.from)"#, "\(bind: $0.table)") } - ?? ("NULL", "NULL") - return insert { - ($0.recordPrimaryKey, $0.recordType, $0.parentRecordPrimaryKey, $0.parentRecordType) - } select: { - Values( - SQLQueryExpression("\(new.primaryKey)"), - T.tableName, - SQLQueryExpression(parentRecordPrimaryKey), - SQLQueryExpression(parentRecordType) + + extension QueryExpression where Self == SQLQueryExpression<()> { + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + fileprivate static func didUpdate( + _ new: StructuredQueriesCore.TableAlias< + SyncMetadata, TemporaryTrigger.Operation._New + > + .TableColumns + ) -> Self { + .didUpdate( + recordName: new.recordName, + lastKnownServerRecord: new.lastKnownServerRecord + ?? rootServerRecord(recordName: new.recordName), + share: new.share ) - } onConflict: { - ($0.recordPrimaryKey, $0.recordType) - } doUpdate: { - $0.parentRecordPrimaryKey = $1.parentRecordPrimaryKey - $0.parentRecordType = $1.parentRecordType - $0.userModificationDate = $1.userModificationDate } - } -} - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -extension SyncMetadata { - static var callbackTriggers: [TemporaryTrigger] { - [ - afterInsertTrigger, - afterUpdateTrigger, - afterDeleteTrigger, - ] - } - - private enum ParentSyncMetadata: AliasName {} - fileprivate static let afterInsertTrigger = createTemporaryTrigger( - "after_insert_on_sqlitedata_icloud_metadata", - ifNotExists: true, - after: .insert { new in - Values(.didUpdate(new)) - } when: { _ in - !SyncEngine.isSynchronizingChanges() - } - ) - - fileprivate static let afterUpdateTrigger = createTemporaryTrigger( - "after_update_on_sqlitedata_icloud_metadata", - ifNotExists: true, - after: .update { _, new in - Values(.didUpdate(new)) - } when: { _, _ in - !SyncEngine.isSynchronizingChanges() + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + private static func didUpdate( + recordName: some QueryExpression, + lastKnownServerRecord: some QueryExpression, + share: some QueryExpression + ) -> Self { + Self( + "\(raw: .sqliteDataCloudKitSchemaName)_didUpdate(\(recordName), \(lastKnownServerRecord), \(share))" + ) } - ) - - fileprivate static let afterDeleteTrigger = createTemporaryTrigger( - "after_delete_on_sqlitedata_icloud_metadata", - ifNotExists: true, - after: .delete { old in - Values(.didDelete( - recordName: old.recordName, - lastKnownServerRecord: old.lastKnownServerRecord - ?? rootServerRecord(recordName: old.recordName) - )) - } when: { _ in - !SyncEngine.isSynchronizingChanges() + + @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) + fileprivate static func didDelete( + recordName: some QueryExpression, + lastKnownServerRecord: some QueryExpression, + share: some QueryExpression + ) -> Self { + Self( + "\(raw: .sqliteDataCloudKitSchemaName)_didDelete(\(recordName), \(lastKnownServerRecord), \(share))" + ) } - ) -} + } + + private func isUpdatingWithServerRecord() -> SQLQueryExpression { + SQLQueryExpression("\(raw: .sqliteDataCloudKitSchemaName)_isUpdatingWithServerRecord()") + } + + private func parentFields( + alias: StructuredQueriesCore.TableAlias.TableColumns, + parentForeignKey: ForeignKey? + ) -> (parentRecordPrimaryKey: QueryFragment, parentRecordType: QueryFragment) { + parentForeignKey + .map { (#"\#(type(of: alias).QueryValue.self).\#(quote: $0.from)"#, "\(bind: $0.table)") } + ?? ("NULL", "NULL") + } -extension QueryExpression where Self == SQLQueryExpression<()> { @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - fileprivate static func didUpdate( - _ new: StructuredQueriesCore.TableAlias< - SyncMetadata, TemporaryTrigger.Operation._New - > - .TableColumns - ) -> Self { - .didUpdate( - recordName: new.recordName, - lastKnownServerRecord: new.lastKnownServerRecord - ?? rootServerRecord(recordName: new.recordName) + private func checkWritePermissions( + alias: StructuredQueriesCore.TableAlias.TableColumns, + parentForeignKey: ForeignKey? + ) -> some StructuredQueriesCore.Statement { + let (parentRecordPrimaryKey, parentRecordType) = parentFields( + alias: alias, + parentForeignKey: parentForeignKey ) + + return With { + SyncMetadata + .where { + $0.recordPrimaryKey.is(SQLQueryExpression(parentRecordPrimaryKey)) + && $0.recordType.is(SQLQueryExpression(parentRecordType)) + } + .select { RootShare.Columns(parentRecordName: $0.parentRecordName, share: $0.share) } + .union( + all: true, + SyncMetadata + .select { + RootShare.Columns(parentRecordName: $0.parentRecordName, share: $0.share) + } + .join(RootShare.all) { $0.recordName.is($1.parentRecordName) } + ) + } query: { + RootShare + .select { _ in + SQLQueryExpression( + "RAISE(ABORT, \(quote: SyncEngine.writePermissionError, delimiter: .text))", + as: Never.self + ) + } + .where { + $0.parentRecordName.is(nil) + && !SQLQueryExpression( + "\(raw: String.sqliteDataCloudKitSchemaName)_hasPermission(\($0.share))" + ) + } + } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - private static func didUpdate( - recordName: some QueryExpression, - lastKnownServerRecord: some QueryExpression - ) -> Self { - Self("\(raw: .sqliteDataCloudKitSchemaName)_didUpdate(\(recordName), \(lastKnownServerRecord))") + private func rootServerRecord( + recordName: some QueryExpression + ) -> some QueryExpression { + With { + SyncMetadata + .where { $0.recordName.eq(recordName) } + .select { AncestorMetadata.Columns($0) } + .union( + all: true, + SyncMetadata + .select { AncestorMetadata.Columns($0) } + .join(AncestorMetadata.all) { $0.recordName.is($1.parentRecordName) } + ) + } query: { + AncestorMetadata + .select(\.lastKnownServerRecord) + .where { $0.parentRecordName.is(nil) } + } } @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) - fileprivate static func didDelete( - recordName: some QueryExpression, - lastKnownServerRecord: some QueryExpression - ) -> Self { - Self("\(raw: .sqliteDataCloudKitSchemaName)_didDelete(\(recordName), \(lastKnownServerRecord))") - } -} - -private func isUpdatingWithServerRecord() -> SQLQueryExpression { - SQLQueryExpression("\(raw: .sqliteDataCloudKitSchemaName)_isUpdatingWithServerRecord()") -} - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -private func rootServerRecord( - recordName: some QueryExpression -) -> some QueryExpression { - With { - SyncMetadata - .where { $0.recordName.eq(recordName) } - .select { AncestorMetadata.Columns($0) } - .union( - all: true, - SyncMetadata - .select { AncestorMetadata.Columns($0) } - .join(AncestorMetadata.all) { $0.recordName.is($1.parentRecordName) } + extension AncestorMetadata.Columns { + init(_ metadata: SyncMetadata.TableColumns) { + self.init( + recordName: metadata.recordName, + parentRecordName: metadata.parentRecordName, + lastKnownServerRecord: metadata.lastKnownServerRecord ) - } query: { - AncestorMetadata - .select(\.lastKnownServerRecord) - .where { $0.parentRecordName.is(nil) } - } -} - -@available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) -extension AncestorMetadata.Columns { - fileprivate init(_ metadata: SyncMetadata.TableColumns) { - self.init( - recordName: metadata.recordName, - parentRecordName: metadata.parentRecordName, - lastKnownServerRecord: metadata.lastKnownServerRecord - ) + } } -} #endif diff --git a/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md b/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md index 0c9c0e7c..a492d29e 100644 --- a/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md +++ b/Sources/SharingGRDBCore/Documentation.docc/Articles/CloudKitSharing.md @@ -15,15 +15,7 @@ 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 -- [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) -- [Controlling what data is shared](#Controlling-what-data-is-shared) +TODO: ToC ## Creating CKShare records @@ -344,6 +336,10 @@ graph BT Here the `CoverImage` table has a foreign key pointing to the root table `RemindersList`, but since it is also the primary key of the table it enforces that at most one cover image belongs to a list. +## Sharing permissions + +TODO: finish + ## Controlling what data is shared It is possible to specify that certain associations that are shareable not be shared. For example, diff --git a/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift b/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift index 84099044..c68dc153 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/CloudKitTests.swift @@ -557,7 +557,8 @@ extension BaseCloudKitTests { [0]: "sqlitedata_icloud_datetime", [1]: "sqlitedata_icloud_diddelete", [2]: "sqlitedata_icloud_didupdate", - [3]: "sqlitedata_icloud_syncengineissynchronizingchanges" + [3]: "sqlitedata_icloud_haspermission", + [4]: "sqlitedata_icloud_syncengineissynchronizingchanges" ] """ } diff --git a/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift b/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift index 692c1d7b..2d679e4c 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/ForeignKeyConstraintTests.swift @@ -758,5 +758,60 @@ extension BaseCloudKitTests { #expect(reminder == Reminder(id: 1, title: "Get milk", remindersListID: 3)) } } + + @Test func cascadingDeletes() async throws { + try await userDatabase.userWrite { db in + try db.seed { + RemindersList(id: 1, title: "Personal") + Reminder(id: 1, title: "Get milk", remindersListID: 1) + RemindersList(id: 2, title: "Work") + Reminder(id: 2, title: "Call accountant", remindersListID: 2) + RemindersList(id: 3, title: "Secret") + Reminder(id: 3, title: "Schedule secret meeting", remindersListID: 3) + } + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + try await userDatabase.userWrite { db in + try RemindersList.where { $0.id <= 2 }.delete().execute(db) + } + + try await syncEngine.processPendingRecordZoneChanges(scope: .private) + + assertInlineSnapshot(of: syncEngine.container, as: .customDump) { + """ + MockCloudContainer( + privateCloudDatabase: MockCloudDatabase( + databaseScope: .private, + storage: [ + [0]: CKRecord( + recordID: CKRecord.ID(3:reminders/co.pointfree.SQLiteData.defaultZone/__defaultOwner__), + recordType: "reminders", + parent: CKReference(recordID: CKRecord.ID(3:remindersLists/co.pointfree.SQLiteData.defaultZone/__defaultOwner__)), + share: nil, + id: 3, + isCompleted: 0, + remindersListID: 3, + title: "Schedule secret meeting" + ), + [1]: CKRecord( + recordID: CKRecord.ID(3:remindersLists/co.pointfree.SQLiteData.defaultZone/__defaultOwner__), + recordType: "remindersLists", + parent: nil, + share: nil, + id: 3, + title: "Secret" + ) + ] + ), + sharedCloudDatabase: MockCloudDatabase( + databaseScope: .shared, + storage: [] + ) + ) + """ + } + } } } diff --git a/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift b/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift index 6da480a7..b580e573 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/NewTableSyncTests.swift @@ -89,6 +89,7 @@ extension BaseCloudKitTests { title: "Write blog post" ), share: nil, + _isDeleted: false, isShared: false, userModificationDate: Date(1970-01-01T00:00:00.000Z) ), @@ -114,6 +115,7 @@ extension BaseCloudKitTests { title: "Personal" ), share: nil, + _isDeleted: false, isShared: false, userModificationDate: Date(1970-01-01T00:00:00.000Z) ) diff --git a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift index aa6d0880..775e075b 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/SharingTests.swift @@ -294,6 +294,7 @@ extension BaseCloudKitTests { title: "Personal" ), share: nil, + _isDeleted: false, isShared: false, userModificationDate: Date(1970-01-01T00:00:00.000Z) ) diff --git a/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift b/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift index 08aafd75..2118cd01 100644 --- a/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift +++ b/Tests/SharingGRDBTests/CloudKitTests/TriggerTests.swift @@ -19,13 +19,13 @@ extension BaseCloudKitTests { [ [0]: """ CREATE TRIGGER "after_delete_on_sqlitedata_icloud_metadata" - AFTER DELETE ON "sqlitedata_icloud_metadata" - FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN - SELECT sqlitedata_icloud_didDelete("old"."recordName", coalesce("old"."lastKnownServerRecord", ( + AFTER UPDATE OF "_isDeleted" ON "sqlitedata_icloud_metadata" + FOR EACH ROW WHEN ((NOT ("old"."_isDeleted") AND "new"."_isDeleted") AND NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges())) BEGIN + SELECT sqlitedata_icloud_didDelete("new"."recordName", coalesce("new"."lastKnownServerRecord", ( WITH "ancestorMetadatas" AS ( SELECT "sqlitedata_icloud_metadata"."recordName" AS "recordName", "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."lastKnownServerRecord" AS "lastKnownServerRecord" FROM "sqlitedata_icloud_metadata" - WHERE ("sqlitedata_icloud_metadata"."recordName" = "old"."recordName") + WHERE ("sqlitedata_icloud_metadata"."recordName" = "new"."recordName") UNION ALL SELECT "sqlitedata_icloud_metadata"."recordName" AS "recordName", "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."lastKnownServerRecord" AS "lastKnownServerRecord" FROM "sqlitedata_icloud_metadata" @@ -34,7 +34,7 @@ extension BaseCloudKitTests { SELECT "ancestorMetadatas"."lastKnownServerRecord" FROM "ancestorMetadatas" WHERE ("ancestorMetadatas"."parentRecordName" IS NULL) - ))); + )), "new"."share"); END """, [1]: """ @@ -54,13 +54,13 @@ extension BaseCloudKitTests { SELECT "ancestorMetadatas"."lastKnownServerRecord" FROM "ancestorMetadatas" WHERE ("ancestorMetadatas"."parentRecordName" IS NULL) - ))); + )), "new"."share"); END """, [2]: """ CREATE TRIGGER "after_update_on_sqlitedata_icloud_metadata" AFTER UPDATE ON "sqlitedata_icloud_metadata" - FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + FOR EACH ROW WHEN (("old"."_isDeleted" = "new"."_isDeleted") AND NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges())) BEGIN SELECT sqlitedata_icloud_didUpdate("new"."recordName", coalesce("new"."lastKnownServerRecord", ( WITH "ancestorMetadatas" AS ( SELECT "sqlitedata_icloud_metadata"."recordName" AS "recordName", "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."lastKnownServerRecord" AS "lastKnownServerRecord" @@ -74,109 +74,373 @@ extension BaseCloudKitTests { SELECT "ancestorMetadatas"."lastKnownServerRecord" FROM "ancestorMetadatas" WHERE ("ancestorMetadatas"."parentRecordName" IS NULL) - ))); + )), "new"."share"); END """, [3]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_childWithOnDeleteSetDefaults" + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_childWithOnDeleteSetDefaults_from_sync_engine" AFTER DELETE ON "childWithOnDeleteSetDefaults" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'childWithOnDeleteSetDefaults')); END """, [4]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_childWithOnDeleteSetNulls" + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_childWithOnDeleteSetDefaults_from_user" + AFTER DELETE ON "childWithOnDeleteSetDefaults" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "old"."parentID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'parents')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'childWithOnDeleteSetDefaults')); + END + """, + [5]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_childWithOnDeleteSetNulls_from_sync_engine" AFTER DELETE ON "childWithOnDeleteSetNulls" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'childWithOnDeleteSetNulls')); END """, - [5]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelAs" + [6]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_childWithOnDeleteSetNulls_from_user" + AFTER DELETE ON "childWithOnDeleteSetNulls" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "old"."parentID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'parents')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'childWithOnDeleteSetNulls')); + END + """, + [7]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelAs_from_sync_engine" AFTER DELETE ON "modelAs" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelAs')); END """, - [6]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelBs" + [8]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelAs_from_user" + AFTER DELETE ON "modelAs" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelAs')); + END + """, + [9]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelBs_from_sync_engine" AFTER DELETE ON "modelBs" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelBs')); END """, - [7]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelCs" + [10]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelBs_from_user" + AFTER DELETE ON "modelBs" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "old"."modelAID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'modelAs')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelBs')); + END + """, + [11]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelCs_from_sync_engine" AFTER DELETE ON "modelCs" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelCs')); END """, - [8]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_parents" + [12]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_modelCs_from_user" + AFTER DELETE ON "modelCs" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "old"."modelBID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'modelBs')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'modelCs')); + END + """, + [13]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_parents_from_sync_engine" AFTER DELETE ON "parents" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'parents')); END """, - [9]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_reminderTags" + [14]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_parents_from_user" + AFTER DELETE ON "parents" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'parents')); + END + """, + [15]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_reminderTags_from_sync_engine" AFTER DELETE ON "reminderTags" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'reminderTags')); END """, - [10]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_reminders" - AFTER DELETE ON "reminders" - FOR EACH ROW BEGIN - DELETE FROM "sqlitedata_icloud_metadata" - WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'reminders')); + [16]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_reminderTags_from_user" + AFTER DELETE ON "reminderTags" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'reminderTags')); END """, - [11]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersListAssets" + [17]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersListAssets_from_sync_engine" AFTER DELETE ON "remindersListAssets" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersListAssets')); END """, - [12]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersListPrivates" + [18]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersListAssets_from_user" + AFTER DELETE ON "remindersListAssets" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "old"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersListAssets')); + END + """, + [19]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersListPrivates_from_sync_engine" AFTER DELETE ON "remindersListPrivates" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersListPrivates')); END """, - [13]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersLists" + [20]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersListPrivates_from_user" + AFTER DELETE ON "remindersListPrivates" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "old"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersListPrivates')); + END + """, + [21]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersLists_from_sync_engine" AFTER DELETE ON "remindersLists" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersLists')); END """, - [14]: """ - CREATE TRIGGER "sqlitedata_icloud_after_delete_on_tags" + [22]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_remindersLists_from_user" + AFTER DELETE ON "remindersLists" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'remindersLists')); + END + """, + [23]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_reminders_from_sync_engine" + AFTER DELETE ON "reminders" + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN + DELETE FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'reminders')); + END + """, + [24]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_reminders_from_user" + AFTER DELETE ON "reminders" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "old"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."id") AND ("sqlitedata_icloud_metadata"."recordType" = 'reminders')); + END + """, + [25]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_tags_from_sync_engine" AFTER DELETE ON "tags" - FOR EACH ROW BEGIN + FOR EACH ROW WHEN sqlitedata_icloud_syncEngineIsSynchronizingChanges() BEGIN DELETE FROM "sqlitedata_icloud_metadata" WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."title") AND ("sqlitedata_icloud_metadata"."recordType" = 'tags')); END """, - [15]: """ + [26]: """ + CREATE TRIGGER "sqlitedata_icloud_after_delete_on_tags_from_user" + AFTER DELETE ON "tags" + FOR EACH ROW WHEN NOT (sqlitedata_icloud_syncEngineIsSynchronizingChanges()) BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); + UPDATE "sqlitedata_icloud_metadata" + SET "_isDeleted" = 1 + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" = "old"."title") AND ("sqlitedata_icloud_metadata"."recordType" = 'tags')); + END + """, + [27]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_childWithOnDeleteSetDefaults" AFTER INSERT ON "childWithOnDeleteSetDefaults" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."parentID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'parents')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetDefaults', "new"."parentID", 'parents' @@ -184,10 +448,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [16]: """ + [28]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_childWithOnDeleteSetNulls" AFTER INSERT ON "childWithOnDeleteSetNulls" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."parentID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'parents')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetNulls', "new"."parentID", 'parents' @@ -195,10 +471,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [17]: """ + [29]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_modelAs" AFTER INSERT ON "modelAs" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelAs', NULL, NULL @@ -206,10 +494,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [18]: """ + [30]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_modelBs" AFTER INSERT ON "modelBs" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."modelAID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'modelAs')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelBs', "new"."modelAID", 'modelAs' @@ -217,10 +517,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [19]: """ + [31]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_modelCs" AFTER INSERT ON "modelCs" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."modelBID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'modelBs')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelCs', "new"."modelBID", 'modelBs' @@ -228,10 +540,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [20]: """ + [32]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_parents" AFTER INSERT ON "parents" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'parents', NULL, NULL @@ -239,10 +563,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [21]: """ + [33]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_reminderTags" AFTER INSERT ON "reminderTags" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminderTags', NULL, NULL @@ -250,10 +586,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [22]: """ + [34]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_reminders" AFTER INSERT ON "reminders" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminders', "new"."remindersListID", 'remindersLists' @@ -261,10 +609,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [23]: """ + [35]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_remindersListAssets" AFTER INSERT ON "remindersListAssets" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListAssets', "new"."remindersListID", 'remindersLists' @@ -272,10 +632,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [24]: """ + [36]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_remindersListPrivates" AFTER INSERT ON "remindersListPrivates" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListPrivates', "new"."remindersListID", 'remindersLists' @@ -283,10 +655,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [25]: """ + [37]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_remindersLists" AFTER INSERT ON "remindersLists" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersLists', NULL, NULL @@ -294,10 +678,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [26]: """ + [38]: """ CREATE TRIGGER "sqlitedata_icloud_after_insert_on_tags" AFTER INSERT ON "tags" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."title", 'tags', NULL, NULL @@ -305,10 +701,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [27]: """ + [39]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_childWithOnDeleteSetDefaults" AFTER UPDATE ON "childWithOnDeleteSetDefaults" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."parentID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'parents')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetDefaults', "new"."parentID", 'parents' @@ -316,10 +724,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [28]: """ + [40]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_childWithOnDeleteSetNulls" AFTER UPDATE ON "childWithOnDeleteSetNulls" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."parentID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'parents')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'childWithOnDeleteSetNulls', "new"."parentID", 'parents' @@ -327,10 +747,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [29]: """ + [41]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_modelAs" AFTER UPDATE ON "modelAs" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelAs', NULL, NULL @@ -338,10 +770,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [30]: """ + [42]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_modelBs" AFTER UPDATE ON "modelBs" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."modelAID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'modelAs')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelBs', "new"."modelAID", 'modelAs' @@ -349,10 +793,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [31]: """ + [43]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_modelCs" AFTER UPDATE ON "modelCs" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."modelBID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'modelBs')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'modelCs', "new"."modelBID", 'modelBs' @@ -360,10 +816,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [32]: """ + [44]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_parents" AFTER UPDATE ON "parents" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'parents', NULL, NULL @@ -371,10 +839,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [33]: """ + [45]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_reminderTags" AFTER UPDATE ON "reminderTags" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminderTags', NULL, NULL @@ -382,10 +862,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [34]: """ + [46]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_reminders" AFTER UPDATE ON "reminders" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'reminders', "new"."remindersListID", 'remindersLists' @@ -393,10 +885,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [35]: """ + [47]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_remindersListAssets" AFTER UPDATE ON "remindersListAssets" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListAssets', "new"."remindersListID", 'remindersLists' @@ -404,10 +908,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [36]: """ + [48]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_remindersListPrivates" AFTER UPDATE ON "remindersListPrivates" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS "new"."remindersListID") AND ("sqlitedata_icloud_metadata"."recordType" IS 'remindersLists')) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersListPrivates', "new"."remindersListID", 'remindersLists' @@ -415,10 +931,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [37]: """ + [49]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_remindersLists" AFTER UPDATE ON "remindersLists" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."id", 'remindersLists', NULL, NULL @@ -426,10 +954,22 @@ extension BaseCloudKitTests { DO UPDATE SET "parentRecordPrimaryKey" = "excluded"."parentRecordPrimaryKey", "parentRecordType" = "excluded"."parentRecordType", "userModificationDate" = "excluded"."userModificationDate"; END """, - [38]: """ + [50]: """ CREATE TRIGGER "sqlitedata_icloud_after_update_on_tags" AFTER UPDATE ON "tags" FOR EACH ROW BEGIN + WITH "rootShares" AS ( + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + WHERE (("sqlitedata_icloud_metadata"."recordPrimaryKey" IS NULL) AND ("sqlitedata_icloud_metadata"."recordType" IS NULL)) + UNION ALL + SELECT "sqlitedata_icloud_metadata"."parentRecordName" AS "parentRecordName", "sqlitedata_icloud_metadata"."share" AS "share" + FROM "sqlitedata_icloud_metadata" + JOIN "rootShares" ON ("sqlitedata_icloud_metadata"."recordName" IS "rootShares"."parentRecordName") + ) + SELECT RAISE(ABORT, 'co.pointfree.sqlitedata-icloud.write-permission-error') + FROM "rootShares" + WHERE (("rootShares"."parentRecordName" IS NULL) AND NOT (sqlitedata_icloud_hasPermission("rootShares"."share"))); INSERT INTO "sqlitedata_icloud_metadata" ("recordPrimaryKey", "recordType", "parentRecordPrimaryKey", "parentRecordType") SELECT "new"."title", 'tags', NULL, NULL diff --git a/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift b/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift index 76c74479..00b5419b 100644 --- a/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift +++ b/Tests/SharingGRDBTests/Internal/BaseCloudKitTests.swift @@ -25,8 +25,6 @@ class BaseCloudKitTests: @unchecked Sendable { _syncEngine as! SyncEngine } - typealias SendablePrimaryKeyedTable = PrimaryKeyedTable & Sendable - @available(iOS 17, macOS 14, tvOS 17, watchOS 10, *) init( accountStatus: CKAccountStatus = _AccountStatusScope.accountStatus,