diff --git a/Frameworks/RSDatabase/DatabaseTable.swift b/Frameworks/RSDatabase/DatabaseTable.swift index cf128a913..5e0946032 100644 --- a/Frameworks/RSDatabase/DatabaseTable.swift +++ b/Frameworks/RSDatabase/DatabaseTable.swift @@ -12,7 +12,8 @@ public protocol DatabaseTable: class { var name: String {get} - func fetchObjectsWithIDs(_ databaseIDs: Set, _ database: FMDatabase) -> [DatabaseObject] + func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject] + func save(_ objects: [DatabaseObject], in database: FMDatabase) } public extension DatabaseTable { diff --git a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj index c00a8c503..e966cfd2c 100755 --- a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj +++ b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj @@ -565,7 +565,7 @@ INFOPLIST_FILE = RSDatabase/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=75"; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=125"; PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.RSDatabase; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -586,7 +586,7 @@ INFOPLIST_FILE = RSDatabase/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; - OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=75"; + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-long-function-bodies=125"; PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.RSDatabase; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift b/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift index b125142f1..eefb968f1 100644 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift +++ b/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift @@ -90,34 +90,105 @@ private extension DatabaseLookupTable { database.rs_deleteRowsWhereKey(objectIDKey, inValues: Array(objectIDsToRemove), tableName: name) } + func deleteLookups(for objectID: String, _ relatedObjectIDs: Set, _ database: FMDatabase) { + + guard !relatedObjectIDs.isEmpty else { + assertionFailure("deleteLookups: expected non-empty relatedObjectIDs") + return + } + + // delete from authorLookup where articleID=? and authorID in (?,?,?) + let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(relatedObjectIDs.count))! + let sql = "delete from \(name) where \(objectIDKey)=? and \(relatedObjectIDKey) in \(placeholders)" + + let parameters: [Any] = [objectID] + Array(relatedObjectIDs) + let _ = database.executeUpdate(sql, withArgumentsIn: parameters) + } + // MARK: Saving/Updating func updateRelationships(for objects: [DatabaseObject], _ database: FMDatabase) { -// let objectsNeedingUpdate = objects.filter { (object) -> Bool in -// return !relationshipsMatchCache(object) -// } + let objectsNeedingUpdate = objects.filter { !relatedObjectIDsMatchesCache($0) } + if objectsNeedingUpdate.isEmpty { + return + } + + if let lookupTable = fetchLookupTable(objectsNeedingUpdate.databaseIDs(), database) { + for object in objectsNeedingUpdate { + syncRelatedObjectsAndLookupTable(object, lookupTable, database) + } + } + + // Save the actual related objects. + + guard let relatedTable = relatedTable else { + assertionFailure("updateRelationships: relatedTable unexpectedly disappeared.") + return + } + + let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objectsNeedingUpdate) + if relatedObjectsToSave.isEmpty { + assertionFailure("updateRelationships: expected related objects to save. This should be unreachable.") + return + } + + relatedTable.save(relatedObjectsToSave, in: database) } - func relationshipsMatchCache(_ object: DatabaseObject) -> Bool { + func uniqueArrayOfRelatedObjects(with objects: [DatabaseObject]) -> [DatabaseObject] { + + // Can’t create a Set, because we can’t make a Set, because protocol-conforming objects can’t be made Hashable or even Equatable. + // We still want the array to include only one copy of each object, but we have to do it the slow way. Instruments will tell us if this is a performance problem. - let relationships = object.relatedObjectsWithName(relationshipName) - let cachedRelationshipIDs = cache[object.databaseID] + var relatedObjectsUniqueArray = [DatabaseObject]() + for object in objects { + guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else { + assertionFailure("uniqueArrayOfRelatedObjects: expected every object to have related objects.") + continue + } + for relatedObject in relatedObjects { + if !relatedObjectsUniqueArray.includesObjectWithDatabaseID(relatedObject.databaseID) { + relatedObjectsUniqueArray += [relatedObject] + } + } + } + return relatedObjectsUniqueArray + } + + func relatedObjectIDsMatchesCache(_ object: DatabaseObject) -> Bool { - if let relationships = relationships { - if let cachedRelationshipIDs = cachedRelationshipIDs { - return relationships.databaseIDs() == cachedRelationshipIDs - } - return false // cachedRelationshipIDs == nil, relationships != nil - } - else { // relationships == nil - if let cachedRelationshipIDs = cachedRelationshipIDs { - return !cachedRelationshipIDs.isEmpty - } - return true // both nil - } + let relatedObjects = object.relatedObjectsWithName(relationshipName) ?? [DatabaseObject]() + let cachedRelationshipIDs = cache[object.databaseID] ?? Set() + + return relatedObjects.databaseIDs() == cachedRelationshipIDs } + func syncRelatedObjectsAndLookupTable(_ object: DatabaseObject, _ lookupTable: LookupTable, _ database: FMDatabase) { + + guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else { + assertionFailure("syncRelatedObjectsAndLookupTable should be called only on objects with related objects.") + return + } + + let relatedObjectIDs = relatedObjects.databaseIDs() + let lookupTableRelatedObjectIDs = lookupTable[object.databaseID] ?? Set() + + let relatedObjectIDsToDelete = lookupTableRelatedObjectIDs.subtracting(relatedObjectIDs) + if !relatedObjectIDsToDelete.isEmpty { + deleteLookups(for: object.databaseID, relatedObjectIDsToDelete, database) + } + + let relatedObjectIDsToSave = relatedObjectIDs.subtracting(lookupTableRelatedObjectIDs) + if !relatedObjectIDsToSave.isEmpty { + saveLookups(for: object.databaseID, relatedObjectIDsToSave, database) + } + } + + func saveLookups(for objectID: String, _ relatedObjectIDs: Set, _ database: FMDatabase) { + + } + // MARK: Attaching func attachRelatedObjectsUsingCache(_ objects: [DatabaseObject], _ database: FMDatabase) { @@ -159,7 +230,7 @@ private extension DatabaseLookupTable { func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set, _ database: FMDatabase) -> [DatabaseObject]? { - guard let relatedObjects = relatedTable?.fetchObjectsWithIDs(relatedObjectIDs, database), !relatedObjects.isEmpty else { + guard let relatedObjects = relatedTable?.fetchObjectsWithIDs(relatedObjectIDs, in: database), !relatedObjects.isEmpty else { return nil } return relatedObjects @@ -198,7 +269,7 @@ private extension DatabaseLookupTable { } } -struct LookupTable { +private struct LookupTable { private let dictionary: [String: Set] // objectID: Set @@ -241,7 +312,7 @@ struct LookupTable { } } -struct LookupValue: Hashable { +private struct LookupValue: Hashable { let objectID: String let relatedObjectID: String @@ -319,16 +390,3 @@ private final class DatabaseLookupTableCache { } } -private extension Set where Element == LookupValue { - - func objectIDs() -> Set { - - return Set(self.map { $0.objectID }) - } - - func relatedObjectIDs() -> Set { - - return Set(self.map { $0.relatedObjectID }) - } -} - diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift b/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift index 45d338286..66e460766 100644 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift +++ b/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift @@ -31,4 +31,14 @@ extension Array where Element == DatabaseObject { return Set(self.map { $0.databaseID }) } + + func includesObjectWithDatabaseID(_ databaseID: String) -> Bool { + + for object in self { + if object.databaseID == databaseID { + return true + } + } + return false + } } diff --git a/ToDo.ooutline b/ToDo.ooutline index ee8e338d0..d2847926a 100644 Binary files a/ToDo.ooutline and b/ToDo.ooutline differ