From dccc44c9204bbc5e5ad88cc790a27e445f7b4950 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 20 Aug 2017 12:41:33 -0700 Subject: [PATCH] Make progress saving relationships in DatabaseLookupTable. --- Frameworks/RSDatabase/DatabaseTable.swift | 3 +- .../RSDatabase.xcodeproj/project.pbxproj | 4 +- .../RSDatabase/DatabaseLookupTable.swift | 126 +++++++++++++----- .../RSDatabase/DatabaseObject.swift | 10 ++ ToDo.ooutline | Bin 2581 -> 2523 bytes 5 files changed, 106 insertions(+), 37 deletions(-) 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 ee8e338d0e03ee439244c308370baa658e81eb14..d2847926a886c83ace93b8cbc9adb2279aaf8747 100644 GIT binary patch delta 2483 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 delta 2541 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 D$u`a=