2017-08-06 21:37:47 +02:00
|
|
|
//
|
|
|
|
// LookupTable.swift
|
|
|
|
// 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));
|
|
|
|
// authorID is primaryKey; articleID is foreignKey.
|
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
|
|
|
|
private let primaryKey: String
|
|
|
|
private let foreignKey: String
|
|
|
|
private let relationshipName: String
|
|
|
|
private weak var relatedTable: DatabaseTable?
|
2017-08-20 01:30:55 +02:00
|
|
|
private let cache: DatabaseLookupTableCache
|
2017-08-06 21:37:47 +02:00
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
public init(name: String, primaryKey: String, foreignKey: String, relatedTable: DatabaseTable, relationshipName: String) {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
|
|
self.name = name
|
|
|
|
self.primaryKey = primaryKey
|
|
|
|
self.foreignKey = foreignKey
|
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-08-14 21:54:57 +02:00
|
|
|
public func attachRelationships(to objects: [DatabaseObject], database: FMDatabase) {
|
|
|
|
|
2017-08-14 22:33:50 +02:00
|
|
|
guard let lookupTable = fetchLookupTable(objects.databaseIDs(), database) else {
|
2017-08-14 21:54:57 +02:00
|
|
|
return;
|
2017-08-07 06:16:13 +02:00
|
|
|
}
|
2017-08-14 21:54:57 +02:00
|
|
|
attachRelationshipsUsingLookupTable(to: objects, lookupTable: lookupTable, database: database)
|
|
|
|
}
|
|
|
|
|
2017-08-19 21:27:54 +02:00
|
|
|
public func saveRelationships(for objects: [DatabaseObject], 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
|
|
|
|
|
|
|
removeRelationships(for: objectsWithNoRelationships, database: database)
|
2017-08-20 01:30:55 +02:00
|
|
|
updateRelationships(for: objectsWithRelationships, database: database)
|
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-14 21:54:57 +02:00
|
|
|
private extension DatabaseLookupTable {
|
|
|
|
|
2017-08-19 21:27:54 +02:00
|
|
|
func removeRelationships(for objects: [DatabaseObject], database: FMDatabase) {
|
2017-08-19 20:43:52 +02:00
|
|
|
|
|
|
|
removeLookupsForForeignIDs(objects.databaseIDs(), database)
|
|
|
|
}
|
|
|
|
|
2017-08-20 01:30:55 +02:00
|
|
|
func updateRelationships(for objects: [DatabaseObject], database: FMDatabase) {
|
|
|
|
|
|
|
|
let objectsNeedingUpdate = objects.filter { (object) -> Bool in
|
|
|
|
return !relationshipsMatchCache(object)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func relationshipsMatchCache(_ object: DatabaseObject) -> Bool {
|
|
|
|
|
|
|
|
let relationships = object.relatedObjectsWithName(relationshipName)
|
|
|
|
let cachedRelationshipIDs = cache[object.databaseID]
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
func attachRelationshipsUsingLookupTable(to objects: [DatabaseObject], lookupTable: LookupTable, database: FMDatabase) {
|
2017-08-07 06:46:47 +02:00
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
let primaryIDs = lookupTable.primaryIDs()
|
2017-08-08 04:37:31 +02:00
|
|
|
if (primaryIDs.isEmpty) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
guard let relatedObjects: [DatabaseObject] = relatedTable?.fetchObjectsWithIDs(primaryIDs, database), !relatedObjects.isEmpty 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-08 05:00:46 +02:00
|
|
|
let identifier = object.databaseID
|
2017-08-14 21:54:57 +02:00
|
|
|
if let lookupValues = lookupTable[identifier], !lookupValues.isEmpty {
|
2017-08-08 04:37:31 +02:00
|
|
|
let primaryIDs = lookupValues.primaryIDs()
|
2017-08-08 05:00:46 +02:00
|
|
|
let oneObjectRelatedObjects = primaryIDs.flatMap{ (primaryID) -> DatabaseObject? in
|
2017-08-08 04:37:31 +02:00
|
|
|
return relatedObjectsDictionary[primaryID]
|
2017-08-07 06:46:47 +02:00
|
|
|
}
|
2017-08-14 21:54:57 +02:00
|
|
|
object.setRelatedObjects(oneObjectRelatedObjects, name: relationshipName)
|
2017-08-07 06:46:47 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-08-06 21:37:47 +02:00
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
func fetchLookupTable(_ foreignIDs: Set<String>, _ database: FMDatabase) -> LookupTable? {
|
2017-08-09 05:10:02 +02:00
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
let foreignIDsToLookup = foreignIDs.subtracting(foreignIDsWithNoRelationship)
|
2017-08-14 22:33:50 +02:00
|
|
|
guard let lookupValues = fetchLookupValues(foreignIDsToLookup, database) else {
|
2017-08-09 05:10:02 +02:00
|
|
|
return nil
|
|
|
|
}
|
2017-08-14 22:16:52 +02:00
|
|
|
updateCache(lookupValues, foreignIDsToLookup)
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
|
|
return LookupTable(lookupValues)
|
2017-08-07 06:16:13 +02:00
|
|
|
}
|
|
|
|
|
2017-08-20 01:30:55 +02:00
|
|
|
func cacheForeignIDsWithNoRelationships(_ foreignIDs: Set<String>) {
|
|
|
|
|
|
|
|
foreignIDsWithNoRelationship.formUnion(foreignIDs)
|
|
|
|
for foreignID in foreignIDs {
|
|
|
|
cache[foreignID] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
func updateCache(_ lookupValues: Set<LookupValue>, _ foreignIDs: Set<String>) {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
// Maintain foreignIDsWithNoRelationship.
|
|
|
|
// If a relationship exist, remove the foreignID from foreignIDsWithNoRelationship.
|
|
|
|
// If a relationship does not exist, add the foreignID to foreignIDsWithNoRelationship.
|
|
|
|
|
|
|
|
let foreignIDsWithRelationship = lookupValues.foreignIDs()
|
2017-08-20 01:30:55 +02:00
|
|
|
|
|
|
|
let foreignIDs
|
2017-08-07 06:16:13 +02:00
|
|
|
for foreignID in foreignIDs {
|
2017-08-14 22:16:52 +02:00
|
|
|
if !foreignIDsWithRelationship.contains(foreignID) {
|
2017-08-07 06:16:13 +02:00
|
|
|
foreignIDsWithNoRelationship.insert(foreignID)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2017-08-14 21:54:57 +02:00
|
|
|
|
|
|
|
func removeLookupsForForeignIDs(_ foreignIDs: Set<String>, _ database: FMDatabase) {
|
|
|
|
|
|
|
|
let foreignIDsToRemove = foreignIDs.subtracting(foreignIDsWithNoRelationship)
|
|
|
|
if foreignIDsToRemove.isEmpty {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
foreignIDsWithNoRelationship.formUnion(foreignIDsToRemove)
|
|
|
|
|
|
|
|
database.rs_deleteRowsWhereKey(foreignKey, inValues: Array(foreignIDsToRemove), tableName: name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func fetchLookupValues(_ foreignIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? {
|
|
|
|
|
|
|
|
guard !foreignIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(foreignKey, inValues: Array(foreignIDs), tableName: name) else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return lookupValuesWithResultSet(resultSet)
|
|
|
|
}
|
|
|
|
|
2017-08-06 21:37:47 +02:00
|
|
|
func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set<LookupValue> {
|
|
|
|
|
|
|
|
return resultSet.mapToSet(lookupValueWithRow)
|
|
|
|
}
|
|
|
|
|
2017-08-08 05:00:46 +02:00
|
|
|
func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
2017-08-08 05:00:46 +02:00
|
|
|
guard let primaryID = row.string(forColumn: primaryKey) else {
|
2017-08-06 21:37:47 +02:00
|
|
|
return nil
|
|
|
|
}
|
2017-08-08 05:00:46 +02:00
|
|
|
guard let foreignID = row.string(forColumn: foreignKey) else {
|
2017-08-06 21:37:47 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return LookupValue(primaryID: primaryID, foreignID: foreignID)
|
|
|
|
}
|
2017-08-08 05:00:46 +02:00
|
|
|
|
2017-08-06 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
struct LookupTable {
|
2017-08-08 07:09:10 +02:00
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
private let dictionary: [String: Set<LookupValue>]
|
2017-08-08 07:09:10 +02:00
|
|
|
|
2017-08-14 21:54:57 +02:00
|
|
|
init(_ lookupValues: Set<LookupValue>) {
|
2017-08-08 07:09:10 +02:00
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
var d = [String: Set<LookupValue>]()
|
2017-08-08 07:09:10 +02:00
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
for lookupValue in lookupValues {
|
2017-08-08 07:09:10 +02:00
|
|
|
let foreignID = lookupValue.foreignID
|
2017-08-14 22:16:52 +02:00
|
|
|
if d[foreignID] == nil {
|
|
|
|
d[foreignID] = Set([lookupValue])
|
2017-08-08 07:09:10 +02:00
|
|
|
}
|
|
|
|
else {
|
2017-08-14 22:16:52 +02:00
|
|
|
d[foreignID]!.insert(lookupValue)
|
2017-08-08 07:09:10 +02:00
|
|
|
}
|
|
|
|
}
|
2017-08-14 22:16:52 +02:00
|
|
|
|
|
|
|
self.dictionary = d
|
2017-08-08 07:09:10 +02:00
|
|
|
}
|
2017-08-14 22:16:52 +02:00
|
|
|
|
|
|
|
func primaryIDs() -> Set<String> {
|
2017-08-14 21:54:57 +02:00
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
var ids = Set<String>()
|
|
|
|
for (_, lookupValues) in dictionary {
|
|
|
|
ids.formUnion(lookupValues.primaryIDs())
|
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-08 07:09:10 +02:00
|
|
|
subscript(_ foreignID: String) -> Set<LookupValue>? {
|
|
|
|
get {
|
|
|
|
return dictionary[foreignID]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-14 22:16:52 +02:00
|
|
|
struct LookupValue: Hashable {
|
2017-08-06 21:37:47 +02:00
|
|
|
|
2017-08-08 07:09:10 +02:00
|
|
|
let primaryID: String
|
|
|
|
let foreignID: String
|
|
|
|
let hashValue: Int
|
2017-08-06 21:37:47 +02:00
|
|
|
|
|
|
|
init(primaryID: String, foreignID: String) {
|
|
|
|
|
|
|
|
self.primaryID = primaryID
|
|
|
|
self.foreignID = foreignID
|
2017-08-19 21:27:54 +02:00
|
|
|
self.hashValue = (primaryID + foreignID).hashValue
|
2017-08-06 21:37:47 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static public func ==(lhs: LookupValue, rhs: LookupValue) -> Bool {
|
|
|
|
|
|
|
|
return lhs.primaryID == rhs.primaryID && lhs.foreignID == rhs.foreignID
|
|
|
|
}
|
|
|
|
}
|
2017-08-07 06:16:13 +02:00
|
|
|
|
2017-08-20 01:30:55 +02:00
|
|
|
private final class DatabaseLookupTableCache {
|
|
|
|
|
|
|
|
private let relationshipName: String
|
|
|
|
private var foreignIDsWithNoRelationship = Set<String>()
|
|
|
|
private var cachedLookups = [String: Set<String>]() // foreignID: Set<primaryID>
|
|
|
|
|
|
|
|
init(_ relationshipName: String) {
|
|
|
|
|
|
|
|
self.relationshipName = relationshipName
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateCacheWithIDsWithNoRelationship(_ foreignIDs: Set<String>) {
|
|
|
|
|
|
|
|
foreignIDsWithNoRelationship.formUnion(foreignIDs)
|
|
|
|
for foreignID in foreignIDs {
|
|
|
|
cachedLookups[foreignID] = nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateCacheWithObjects(_ object: [DatabaseObject]) {
|
|
|
|
|
|
|
|
var foreignIDsWithRelationship = Set<String>()
|
|
|
|
|
|
|
|
for object in objects {
|
|
|
|
|
|
|
|
if let relatedObjects = object.relatedObjectsWithName, !relatedObjects.isEmpty {
|
|
|
|
foreignIDsWithRelationship.insert(object.databaseID)
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
updateCacheWithIDsWithNoRelationship(objects.databaseIDs())
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
foreignIDsWithNoRelationship.subtract(foreignIDsWithRelationships)
|
|
|
|
}
|
|
|
|
|
|
|
|
func foreignIDHasNoRelationship(_ foreignID: String) -> Bool {
|
|
|
|
|
|
|
|
return foreignIDsWithNoRelationship.contains(foreignID)
|
|
|
|
}
|
|
|
|
|
|
|
|
func relationshipIDsForForeignID(_ foreignID: String) -> Set<String>? {
|
|
|
|
|
|
|
|
return cachedLookups[foreignID]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-08-07 06:46:47 +02:00
|
|
|
private extension Set where Element == LookupValue {
|
2017-08-08 07:09:10 +02:00
|
|
|
|
2017-08-07 06:46:47 +02:00
|
|
|
func primaryIDs() -> Set<String> {
|
2017-08-08 07:09:10 +02:00
|
|
|
|
|
|
|
return Set<String>(self.map { $0.primaryID })
|
|
|
|
}
|
|
|
|
|
|
|
|
func foreignIDs() -> Set<String> {
|
2017-08-07 06:46:47 +02:00
|
|
|
|
2017-08-08 07:09:10 +02:00
|
|
|
return Set<String>(self.map { $0.foreignID })
|
2017-08-07 06:46:47 +02:00
|
|
|
}
|
|
|
|
}
|
2017-08-08 07:09:10 +02:00
|
|
|
|