2017-08-06 21:37:47 +02:00
|
|
|
|
//
|
2017-08-20 07:07:31 +02:00
|
|
|
|
// DatabaseLookupTable.swift
|
2017-08-06 21:37:47 +02:00
|
|
|
|
// RSDatabase
|
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 8/5/17.
|
|
|
|
|
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
|
|
|
|
|
// Implement a lookup table for a many-to-many relationship.
|
|
|
|
|
// Example: CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID));
|
2017-08-20 07:07:31 +02:00
|
|
|
|
// articleID is objectID; authorID is relatedObjectID.
|
2017-08-07 06:16:13 +02:00
|
|
|
|
|
2017-08-08 07:09:10 +02:00
|
|
|
|
public final class DatabaseLookupTable {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
|
private let name: String
|
2017-08-20 07:07:31 +02:00
|
|
|
|
private let objectIDKey: String
|
|
|
|
|
private let relatedObjectIDKey: String
|
2017-08-14 21:54:57 +02:00
|
|
|
|
private let relationshipName: String
|
2017-09-02 19:11:19 +02:00
|
|
|
|
private let relatedTable: DatabaseRelatedObjectsTable
|
2017-08-20 01:30:55 +02:00
|
|
|
|
private let cache: DatabaseLookupTableCache
|
2017-09-11 15:46:32 +02:00
|
|
|
|
private var objectIDsWithNoRelatedObjects = Set<String>()
|
|
|
|
|
|
2017-09-02 19:11:19 +02:00
|
|
|
|
public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseRelatedObjectsTable, relationshipName: String) {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
|
|
|
|
self.name = name
|
2017-08-20 07:07:31 +02:00
|
|
|
|
self.objectIDKey = objectIDKey
|
|
|
|
|
self.relatedObjectIDKey = relatedObjectIDKey
|
2017-08-14 21:54:57 +02:00
|
|
|
|
self.relatedTable = relatedTable
|
|
|
|
|
self.relationshipName = relationshipName
|
2017-08-20 01:30:55 +02:00
|
|
|
|
self.cache = DatabaseLookupTableCache(relationshipName)
|
2017-08-06 21:37:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-11 15:46:32 +02:00
|
|
|
|
public func fetchRelatedObjects(for objectIDs: Set<String>, in database: FMDatabase) -> RelatedObjectsLookupTable? {
|
|
|
|
|
|
|
|
|
|
let objectIDsThatMayHaveRelatedObjects = objectIDs.subtracting(objectIDsWithNoRelatedObjects)
|
|
|
|
|
if objectIDsThatMayHaveRelatedObjects.isEmpty {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let lookupTable = fetchLookupTable(objectIDsThatMayHaveRelatedObjects, database) else {
|
|
|
|
|
objectIDsWithNoRelatedObjects.formUnion(objectIDsThatMayHaveRelatedObjects)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let relatedObjects = fetchRelatedObjectsReferencedByLookupTable(LookupTable, database) {
|
|
|
|
|
|
|
|
|
|
let relatedObjectsDictionary = relatedObjectsDictionary(lookupTable, relatedObjects)
|
|
|
|
|
|
|
|
|
|
let objectIDsWithNoFetchedRelatedObjects = objectIDsThatMayHaveRelatedObjects.subtracting(Set(relatedObjectsDictionary.keys))
|
|
|
|
|
objectIDsWithNoRelatedObjects.formUnion(objectIDsWithNoFetchedRelatedObjects)
|
|
|
|
|
|
|
|
|
|
return relatedObjectsDictionary
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-21 02:46:15 +02:00
|
|
|
|
public func attachRelatedObjects(to objects: [DatabaseObject], in database: FMDatabase) {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let objectsThatMayHaveRelatedObjects = cache.objectsThatMayHaveRelatedObjects(objects)
|
|
|
|
|
if objectsThatMayHaveRelatedObjects.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
attachRelatedObjectsUsingCache(objectsThatMayHaveRelatedObjects, database)
|
|
|
|
|
|
|
|
|
|
let objectsNeedingFetching = objectsThatMayHaveRelatedObjects.filter { (object) -> Bool in
|
|
|
|
|
return object.relatedObjectsWithName(self.relationshipName) == nil
|
|
|
|
|
}
|
|
|
|
|
if objectsNeedingFetching.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let lookupTable = fetchLookupTable(objectsNeedingFetching.databaseIDs(), database) {
|
|
|
|
|
attachRelatedObjectsUsingLookupTable(objectsNeedingFetching, lookupTable, database)
|
2017-08-07 06:16:13 +02:00
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
|
|
|
|
cache.update(with: objectsNeedingFetching)
|
2017-08-14 21:54:57 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-21 02:46:15 +02:00
|
|
|
|
public func saveRelatedObjects(for objects: [DatabaseObject], in database: FMDatabase) {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
2017-08-19 20:43:52 +02:00
|
|
|
|
var objectsWithNoRelationships = [DatabaseObject]()
|
|
|
|
|
var objectsWithRelationships = [DatabaseObject]()
|
|
|
|
|
|
2017-08-19 21:27:54 +02:00
|
|
|
|
for object in objects {
|
|
|
|
|
if let relatedObjects = object.relatedObjectsWithName(relationshipName), !relatedObjects.isEmpty {
|
|
|
|
|
objectsWithRelationships += [object]
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
objectsWithNoRelationships += [object]
|
|
|
|
|
}
|
2017-08-19 20:43:52 +02:00
|
|
|
|
}
|
2017-08-19 21:27:54 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
removeRelationships(for: objectsWithNoRelationships, database)
|
|
|
|
|
updateRelationships(for: objectsWithRelationships, database)
|
|
|
|
|
|
|
|
|
|
cache.update(with: objects)
|
2017-08-07 06:16:13 +02:00
|
|
|
|
}
|
2017-08-14 21:54:57 +02:00
|
|
|
|
}
|
2017-08-07 06:16:13 +02:00
|
|
|
|
|
2017-08-21 00:03:05 +02:00
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
|
private extension DatabaseLookupTable {
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
// MARK: Removing
|
|
|
|
|
|
|
|
|
|
func removeRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
|
2017-08-19 20:43:52 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let objectIDs = objects.databaseIDs()
|
|
|
|
|
let objectIDsToRemove = objectIDs.subtracting(cache.objectIDsWithNoRelationship)
|
|
|
|
|
if objectIDsToRemove.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
database.rs_deleteRowsWhereKey(objectIDKey, inValues: Array(objectIDsToRemove), tableName: name)
|
2017-08-19 20:43:52 +02:00
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
2017-08-20 21:41:33 +02:00
|
|
|
|
func deleteLookups(for objectID: String, _ relatedObjectIDs: Set<String>, _ 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)
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
// MARK: Saving/Updating
|
|
|
|
|
|
|
|
|
|
func updateRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
|
2017-08-19 20:43:52 +02:00
|
|
|
|
|
2017-08-20 21:41:33 +02:00
|
|
|
|
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.
|
|
|
|
|
|
|
|
|
|
let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objectsNeedingUpdate)
|
|
|
|
|
if relatedObjectsToSave.isEmpty {
|
2017-09-05 02:10:02 +02:00
|
|
|
|
assertionFailure("updateRelationships: expected relatedObjectsToSave would not be empty. This should be unreachable.")
|
2017-08-20 21:41:33 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
relatedTable.save(relatedObjectsToSave, in: database)
|
|
|
|
|
}
|
2017-08-20 01:30:55 +02:00
|
|
|
|
|
2017-08-20 21:41:33 +02:00
|
|
|
|
func uniqueArrayOfRelatedObjects(with objects: [DatabaseObject]) -> [DatabaseObject] {
|
|
|
|
|
|
|
|
|
|
// Can’t create a Set, because we can’t make a Set<DatabaseObject>, 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.
|
2017-08-20 01:30:55 +02:00
|
|
|
|
|
2017-08-20 21:41:33 +02:00
|
|
|
|
var relatedObjectsUniqueArray = [DatabaseObject]()
|
|
|
|
|
for object in objects {
|
|
|
|
|
guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else {
|
|
|
|
|
assertionFailure("uniqueArrayOfRelatedObjects: expected every object to have related objects.")
|
|
|
|
|
continue
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
2017-08-20 21:41:33 +02:00
|
|
|
|
for relatedObject in relatedObjects {
|
|
|
|
|
if !relatedObjectsUniqueArray.includesObjectWithDatabaseID(relatedObject.databaseID) {
|
|
|
|
|
relatedObjectsUniqueArray += [relatedObject]
|
|
|
|
|
}
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-08-20 21:41:33 +02:00
|
|
|
|
return relatedObjectsUniqueArray
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func relatedObjectIDsMatchesCache(_ object: DatabaseObject) -> Bool {
|
|
|
|
|
|
|
|
|
|
let relatedObjects = object.relatedObjectsWithName(relationshipName) ?? [DatabaseObject]()
|
|
|
|
|
let cachedRelationshipIDs = cache[object.databaseID] ?? Set<String>()
|
|
|
|
|
|
|
|
|
|
return relatedObjects.databaseIDs() == cachedRelationshipIDs
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 21:41:33 +02:00
|
|
|
|
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<String>()
|
|
|
|
|
|
|
|
|
|
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<String>, _ database: FMDatabase) {
|
|
|
|
|
|
2017-08-21 00:03:05 +02:00
|
|
|
|
for relatedObjectID in relatedObjectIDs {
|
|
|
|
|
let d: [NSObject: Any] = [(objectIDKey as NSString): objectID, (relatedObjectIDKey as NSString): relatedObjectID]
|
|
|
|
|
let _ = database.rs_insertRow(with: d, insertType: .orIgnore, tableName: name)
|
|
|
|
|
}
|
2017-08-20 21:41:33 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
// MARK: Attaching
|
|
|
|
|
|
|
|
|
|
func attachRelatedObjectsUsingCache(_ objects: [DatabaseObject], _ database: FMDatabase) {
|
2017-08-07 06:46:47 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let lookupTable = cache.lookupTableForObjectIDs(objects.databaseIDs())
|
|
|
|
|
attachRelatedObjectsUsingLookupTable(objects, lookupTable, database)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-11 15:46:32 +02:00
|
|
|
|
func fetchRelatedObjectsReferencedByLookupTable(_ lookupTable: LookupTable, _ database: FMDatabase) -> [DatabaseObject]? {
|
|
|
|
|
|
|
|
|
|
let relatedObjectIDs = lookupTable.relatedObjectIDs()
|
|
|
|
|
if (relatedObjectIDs.isEmpty) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return fetchRelatedObjectsWithIDs(relatedObjectIDs)
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
func attachRelatedObjectsUsingLookupTable(_ objects: [DatabaseObject], _ lookupTable: LookupTable, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
|
|
let relatedObjectIDs = lookupTable.relatedObjectIDs()
|
|
|
|
|
if (relatedObjectIDs.isEmpty) {
|
2017-08-08 04:37:31 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
guard let relatedObjects = fetchRelatedObjectsWithIDs(relatedObjectIDs, database) else {
|
2017-08-08 04:37:31 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
2017-08-08 05:00:46 +02:00
|
|
|
|
let relatedObjectsDictionary = relatedObjects.dictionary()
|
2017-08-07 06:46:47 +02:00
|
|
|
|
|
|
|
|
|
for object in objects {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
attachRelatedObjectsToObjectUsingLookupTable(object, relatedObjectsDictionary, lookupTable)
|
2017-08-07 06:46:47 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
func attachRelatedObjectsToObjectUsingLookupTable(_ object: DatabaseObject, _ relatedObjectsDictionary: [String: DatabaseObject], _ lookupTable: LookupTable) {
|
2017-08-09 05:10:02 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let identifier = object.databaseID
|
|
|
|
|
guard let relatedObjectIDs = lookupTable[identifier], !relatedObjectIDs.isEmpty else {
|
|
|
|
|
return
|
2017-08-09 05:10:02 +02:00
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let relatedObjects = relatedObjectIDs.flatMap { relatedObjectsDictionary[$0] }
|
|
|
|
|
if !relatedObjects.isEmpty {
|
|
|
|
|
object.setRelatedObjects(relatedObjects, name: relationshipName)
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
|
|
|
|
// MARK: Fetching
|
|
|
|
|
|
|
|
|
|
func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set<String>, _ database: FMDatabase) -> [DatabaseObject]? {
|
2017-08-14 22:16:52 +02:00
|
|
|
|
|
2017-08-21 01:03:09 +02:00
|
|
|
|
let relatedObjects = relatedTable.fetchObjectsWithIDs(relatedObjectIDs, in: database)
|
|
|
|
|
if relatedObjects.isEmpty {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
return nil
|
2017-08-07 06:16:13 +02:00
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
return relatedObjects
|
2017-08-07 06:16:13 +02:00
|
|
|
|
}
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
func fetchLookupTable(_ objectIDs: Set<String>, _ database: FMDatabase) -> LookupTable? {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
guard let lookupValues = fetchLookupValues(objectIDs, database) else {
|
|
|
|
|
return nil
|
2017-08-14 21:54:57 +02:00
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
return LookupTable(lookupValues: lookupValues)
|
2017-08-14 21:54:57 +02:00
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
|
|
|
|
func fetchLookupValues(_ objectIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
guard !objectIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(objectIDKey, inValues: Array(objectIDs), tableName: name) else {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return lookupValuesWithResultSet(resultSet)
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-06 21:37:47 +02:00
|
|
|
|
func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set<LookupValue> {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
2017-08-06 21:37:47 +02:00
|
|
|
|
return resultSet.mapToSet(lookupValueWithRow)
|
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
2017-08-08 05:00:46 +02:00
|
|
|
|
func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
|
|
|
|
guard let objectID = row.string(forColumn: objectIDKey) else {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
guard let relatedObjectID = row.string(forColumn: relatedObjectIDKey) else {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
return nil
|
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID)
|
2017-08-06 21:37:47 +02:00
|
|
|
|
}
|
2017-09-11 15:46:32 +02:00
|
|
|
|
|
|
|
|
|
func relatedObjectsDictionary(_ lookupTable: LookupTable, relatedObjects: [DatabaseObject]) -> RelatedObjectsDictionary? {
|
|
|
|
|
|
|
|
|
|
var relatedObjectsDictionary = RelatedObjectsDictionary()
|
|
|
|
|
let d = relatedObjects.dictionary()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return relatedObjectsDictionary.isEmpty ? nil : relatedObjectsDictionary
|
|
|
|
|
}
|
2017-08-06 21:37:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-21 00:03:05 +02:00
|
|
|
|
// MARK: -
|
|
|
|
|
|
2017-08-20 21:41:33 +02:00
|
|
|
|
private struct LookupTable {
|
2017-08-08 07:09:10 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
private let dictionary: [String: Set<String>] // objectID: Set<relatedObjectID>
|
2017-08-08 07:09:10 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
init(dictionary: [String: Set<String>]) {
|
2017-08-08 07:09:10 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
self.dictionary = dictionary
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init(lookupValues: Set<LookupValue>) {
|
|
|
|
|
|
|
|
|
|
var d = [String: Set<String>]()
|
2017-08-08 07:09:10 +02:00
|
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
|
for lookupValue in lookupValues {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let objectID = lookupValue.objectID
|
|
|
|
|
let relatedObjectID: String = lookupValue.relatedObjectID
|
|
|
|
|
if d[objectID] == nil {
|
|
|
|
|
d[objectID] = Set([relatedObjectID])
|
2017-08-08 07:09:10 +02:00
|
|
|
|
}
|
|
|
|
|
else {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
d[objectID]!.insert(relatedObjectID)
|
2017-08-08 07:09:10 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-08-14 22:16:52 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
self.init(dictionary: d)
|
2017-08-08 07:09:10 +02:00
|
|
|
|
}
|
2017-08-14 22:16:52 +02:00
|
|
|
|
|
2017-09-11 15:46:32 +02:00
|
|
|
|
func objectIDs() -> Set<String> {
|
|
|
|
|
|
|
|
|
|
return Set(dictionary.keys)
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
func relatedObjectIDs() -> Set<String> {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
|
var ids = Set<String>()
|
2017-08-20 07:07:31 +02:00
|
|
|
|
for (_, relatedObjectIDs) in dictionary {
|
|
|
|
|
ids.formUnion(relatedObjectIDs)
|
2017-08-14 21:54:57 +02:00
|
|
|
|
}
|
2017-08-14 22:16:52 +02:00
|
|
|
|
return ids
|
2017-08-14 21:54:57 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
subscript(_ objectID: String) -> Set<String>? {
|
2017-08-08 07:09:10 +02:00
|
|
|
|
get {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
return dictionary[objectID]
|
2017-08-08 07:09:10 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 21:41:33 +02:00
|
|
|
|
private struct LookupValue: Hashable {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let objectID: String
|
|
|
|
|
let relatedObjectID: String
|
2017-08-08 07:09:10 +02:00
|
|
|
|
let hashValue: Int
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
init(objectID: String, relatedObjectID: String) {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
self.objectID = objectID
|
|
|
|
|
self.relatedObjectID = relatedObjectID
|
|
|
|
|
self.hashValue = (objectID + relatedObjectID).hashValue
|
2017-08-06 21:37:47 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static public func ==(lhs: LookupValue, rhs: LookupValue) -> Bool {
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
return lhs.objectID == rhs.objectID && lhs.relatedObjectID == rhs.relatedObjectID
|
2017-08-06 21:37:47 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-08-07 06:16:13 +02:00
|
|
|
|
|
2017-08-20 01:30:55 +02:00
|
|
|
|
private final class DatabaseLookupTableCache {
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
var objectIDsWithNoRelationship = Set<String>()
|
2017-08-20 01:30:55 +02:00
|
|
|
|
private let relationshipName: String
|
2017-08-20 07:07:31 +02:00
|
|
|
|
private var cachedLookups = [String: Set<String>]() // objectID: Set<relatedObjectID>
|
2017-08-20 01:30:55 +02:00
|
|
|
|
|
|
|
|
|
init(_ relationshipName: String) {
|
|
|
|
|
|
|
|
|
|
self.relationshipName = relationshipName
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
func update(with objects: [DatabaseObject]) {
|
2017-08-20 01:30:55 +02:00
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
var idsWithRelationships = Set<String>()
|
|
|
|
|
var idsWithNoRelationships = Set<String>()
|
|
|
|
|
|
2017-08-20 01:30:55 +02:00
|
|
|
|
for object in objects {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
let objectID = object.databaseID
|
|
|
|
|
if let relatedObjects = object.relatedObjectsWithName(relationshipName), !relatedObjects.isEmpty {
|
|
|
|
|
idsWithRelationships.insert(objectID)
|
|
|
|
|
self[objectID] = relatedObjects.databaseIDs()
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
|
|
|
|
else {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
idsWithNoRelationships.insert(objectID)
|
|
|
|
|
self[objectID] = nil
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
objectIDsWithNoRelationship.subtract(idsWithRelationships)
|
|
|
|
|
objectIDsWithNoRelationship.formUnion(idsWithNoRelationships)
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
subscript(_ objectID: String) -> Set<String>? {
|
|
|
|
|
get {
|
|
|
|
|
return cachedLookups[objectID]
|
|
|
|
|
}
|
|
|
|
|
set {
|
|
|
|
|
cachedLookups[objectID] = newValue
|
|
|
|
|
}
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
2017-09-11 15:46:32 +02:00
|
|
|
|
func objectIDsThatMayHaveRelatedObjects(_ objectIDs: Set<String>) -> Set<String> {
|
2017-08-20 07:07:31 +02:00
|
|
|
|
|
|
|
|
|
// Filter out objects that are known to have no related objects
|
2017-09-11 15:46:32 +02:00
|
|
|
|
return Set(objectIDs.filter{ !objectIDsWithNoRelationship.contains($0) })
|
2017-08-20 07:07:31 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-11 15:46:32 +02:00
|
|
|
|
// func objectsThatMayHaveRelatedObjects(_ objects: [DatabaseObject]) -> [DatabaseObject] {
|
|
|
|
|
//
|
|
|
|
|
// // Filter out objects that are known to have no related objects
|
|
|
|
|
// return objects.filter{ !objectIDsWithNoRelationship.contains($0.databaseID) }
|
|
|
|
|
// }
|
|
|
|
|
|
2017-08-20 07:07:31 +02:00
|
|
|
|
func lookupTableForObjectIDs(_ objectIDs: Set<String>) -> LookupTable {
|
|
|
|
|
|
|
|
|
|
var d = [String: Set<String>]()
|
|
|
|
|
for objectID in objectIDs {
|
|
|
|
|
if let relatedObjectIDs = self[objectID] {
|
|
|
|
|
d[objectID] = relatedObjectIDs
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return LookupTable(dictionary: d)
|
2017-08-20 01:30:55 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|