Move modules to Modules folder.

This commit is contained in:
Brent Simmons
2025-01-06 21:13:56 -08:00
parent 430871c94a
commit 2933d9aca0
463 changed files with 2 additions and 20 deletions

View File

@@ -0,0 +1,53 @@
//
// Database.swift
// RSDatabase
//
// Created by Brent Simmons on 12/15/19.
// Copyright © 2019 Brent Simmons. All rights reserved.
//
import Foundation
import RSDatabaseObjC
public enum DatabaseError: Error {
case isSuspended // On iOS, to support background refreshing, a database may be suspended.
}
/// Result type that provides an FMDatabase or a DatabaseError.
public typealias DatabaseResult = Result<FMDatabase, DatabaseError>
/// Block that executes database code or handles DatabaseQueueError.
public typealias DatabaseBlock = (DatabaseResult) -> Void
/// Completion block that provides an optional DatabaseError.
public typealias DatabaseCompletionBlock = (DatabaseError?) -> Void
/// Result type for fetching an Int or getting a DatabaseError.
public typealias DatabaseIntResult = Result<Int, DatabaseError>
/// Completion block for DatabaseIntResult.
public typealias DatabaseIntCompletionBlock = (DatabaseIntResult) -> Void
// MARK: - Extensions
public extension DatabaseResult {
/// Convenience for getting the database from a DatabaseResult.
var database: FMDatabase? {
switch self {
case .success(let database):
return database
case .failure:
return nil
}
}
/// Convenience for getting the error from a DatabaseResult.
var error: DatabaseError? {
switch self {
case .success:
return nil
case .failure(let error):
return error
}
}
}

View File

@@ -0,0 +1,61 @@
//
// DatabaseObject.swift
// RSDatabase
//
// Created by Brent Simmons on 8/7/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public typealias DatabaseDictionary = [String: Any]
public protocol DatabaseObject {
var databaseID: String { get }
func databaseDictionary() -> DatabaseDictionary?
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]?
}
public extension DatabaseObject {
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? {
return nil
}
}
extension Array where Element == DatabaseObject {
func dictionary() -> [String: DatabaseObject] {
var d = [String: DatabaseObject]()
for object in self {
d[object.databaseID] = object
}
return d
}
func databaseIDs() -> Set<String> {
return Set(self.map { $0.databaseID })
}
func includesObjectWithDatabaseID(_ databaseID: String) -> Bool {
for object in self {
if object.databaseID == databaseID {
return true
}
}
return false
}
func databaseDictionaries() -> [DatabaseDictionary]? {
let dictionaries = self.compactMap{ $0.databaseDictionary() }
return dictionaries.isEmpty ? nil : dictionaries
}
}

View File

@@ -0,0 +1,33 @@
//
// DatabaseObjectCache.swift
// RSDatabase
//
// Created by Brent Simmons on 9/12/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public final class DatabaseObjectCache {
private var d = [String: DatabaseObject]()
public init() {
//
}
public func add(_ databaseObjects: [DatabaseObject]) {
for databaseObject in databaseObjects {
self[databaseObject.databaseID] = databaseObject
}
}
public subscript(_ databaseID: String) -> DatabaseObject? {
get {
return d[databaseID]
}
set {
d[databaseID] = newValue
}
}
}

View File

@@ -0,0 +1,259 @@
//
// DatabaseQueue.swift
// RSDatabase
//
// Created by Brent Simmons on 11/13/19.
// Copyright © 2019 Brent Simmons. All rights reserved.
//
import Foundation
import SQLite3
import RSDatabaseObjC
/// Manage a serial queue and a SQLite database.
/// It replaces RSDatabaseQueue, which is deprecated.
/// Main-thread only.
/// Important note: on iOS, the queue can be suspended
/// in order to support background refreshing.
public final class DatabaseQueue {
/// Check to see if the queue is suspended. Read-only.
/// Calling suspend() and resume() will change the value of this property.
/// This will return true only on iOS  on macOS its always false.
public var isSuspended: Bool {
#if os(iOS)
precondition(Thread.isMainThread)
return _isSuspended
#else
return false
#endif
}
private var _isSuspended = true
private var isCallingDatabase = false
private let database: FMDatabase
private let databasePath: String
private let serialDispatchQueue: DispatchQueue
private let targetDispatchQueue: DispatchQueue
#if os(iOS)
private let databaseLock = NSLock()
#endif
/// When init returns, the database will not be suspended: it will be ready for database calls.
public init(databasePath: String) {
precondition(Thread.isMainThread)
self.serialDispatchQueue = DispatchQueue(label: "DatabaseQueue (Serial) - \(databasePath)", attributes: .initiallyInactive)
self.targetDispatchQueue = DispatchQueue(label: "DatabaseQueue (Target) - \(databasePath)")
self.serialDispatchQueue.setTarget(queue: self.targetDispatchQueue)
self.serialDispatchQueue.activate()
self.databasePath = databasePath
self.database = FMDatabase(path: databasePath)!
openDatabase()
_isSuspended = false
}
// MARK: - Suspend and Resume
/// Close the SQLite database and dont allow database calls until resumed.
/// This is for iOS, where we need to close the SQLite database in some conditions.
///
/// After calling suspend, if you call into the database before calling resume,
/// your code will not run, and runInDatabaseSync and runInTransactionSync will
/// both throw DatabaseQueueError.isSuspended.
///
/// On Mac, suspend() and resume() are no-ops, since there isnt a need for them.
public func suspend() {
#if os(iOS)
precondition(Thread.isMainThread)
guard !_isSuspended else {
return
}
_isSuspended = true
serialDispatchQueue.suspend()
targetDispatchQueue.async {
self.lockDatabase()
self.database.close()
self.unlockDatabase()
DispatchQueue.main.async {
self.serialDispatchQueue.resume()
}
}
#endif
}
/// Open the SQLite database. Allow database calls again.
/// This is also for iOS only.
public func resume() {
#if os(iOS)
precondition(Thread.isMainThread)
guard _isSuspended else {
return
}
serialDispatchQueue.suspend()
targetDispatchQueue.sync {
if _isSuspended {
lockDatabase()
openDatabase()
unlockDatabase()
_isSuspended = false
}
}
serialDispatchQueue.resume()
#endif
}
// MARK: - Make Database Calls
/// Run a DatabaseBlock synchronously. This call will block the main thread
/// potentially for a while, depending on how long it takes to execute
/// the DatabaseBlock *and* depending on how many other calls have been
/// scheduled on the queue. Use sparingly  prefer async versions.
public func runInDatabaseSync(_ databaseBlock: DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.sync {
self._runInDatabase(self.database, databaseBlock, false)
}
}
/// Run a DatabaseBlock asynchronously.
public func runInDatabase(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.async {
self._runInDatabase(self.database, databaseBlock, false)
}
}
/// Run a DatabaseBlock wrapped in a transaction synchronously.
/// Transactions help performance significantly when updating the database.
/// Nevertheless, its best to avoid this because it will block the main thread
/// prefer the async `runInTransaction` instead.
public func runInTransactionSync(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.sync {
self._runInDatabase(self.database, databaseBlock, true)
}
}
/// Run a DatabaseBlock wrapped in a transaction asynchronously.
/// Transactions help performance significantly when updating the database.
public func runInTransaction(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.async {
self._runInDatabase(self.database, databaseBlock, true)
}
}
/// Run all the lines that start with "create".
/// Use this to create tables, indexes, etc.
public func runCreateStatements(_ statements: String) throws {
precondition(Thread.isMainThread)
var error: DatabaseError? = nil
runInDatabaseSync { result in
switch result {
case .success(let database):
statements.enumerateLines { (line, stop) in
if line.lowercased().hasPrefix("create") {
database.executeStatements(line)
}
stop = false
}
case .failure(let databaseError):
error = databaseError
}
}
if let error = error {
throw(error)
}
}
/// Compact the database. This should be done from time to time
/// weekly-ish? to keep up the performance level of a database.
/// Generally a thing to do at startup, if its been a while
/// since the last vacuum() call. You almost certainly want to call
/// vacuumIfNeeded instead.
public func vacuum() {
precondition(Thread.isMainThread)
runInDatabase { result in
result.database?.executeStatements("vacuum;")
}
}
/// Vacuum the database if its been more than `daysBetweenVacuums` since the last vacuum.
/// Normally you would call this right after initing a DatabaseQueue.
///
/// - Returns: true if database will be vacuumed.
@discardableResult
public func vacuumIfNeeded(daysBetweenVacuums: Int) -> Bool {
precondition(Thread.isMainThread)
let defaultsKey = "DatabaseQueue-LastVacuumDate-\(databasePath)"
let minimumVacuumInterval = TimeInterval(daysBetweenVacuums * (60 * 60 * 24)) // Doesnt have to be precise
let now = Date()
let cutoffDate = now - minimumVacuumInterval
if let lastVacuumDate = UserDefaults.standard.object(forKey: defaultsKey) as? Date {
if lastVacuumDate < cutoffDate {
vacuum()
UserDefaults.standard.set(now, forKey: defaultsKey)
return true
}
return false
}
// Never vacuumed  almost certainly a new database.
// Just set the LastVacuumDate pref to now and skip vacuuming.
UserDefaults.standard.set(now, forKey: defaultsKey)
return false
}
}
private extension DatabaseQueue {
func lockDatabase() {
#if os(iOS)
databaseLock.lock()
#endif
}
func unlockDatabase() {
#if os(iOS)
databaseLock.unlock()
#endif
}
func _runInDatabase(_ database: FMDatabase, _ databaseBlock: DatabaseBlock, _ useTransaction: Bool) {
lockDatabase()
defer {
unlockDatabase()
}
precondition(!isCallingDatabase)
isCallingDatabase = true
autoreleasepool {
if _isSuspended {
databaseBlock(.failure(.isSuspended))
}
else {
if useTransaction {
database.beginTransaction()
}
databaseBlock(.success(database))
if useTransaction {
database.commit()
}
}
}
isCallingDatabase = false
}
func openDatabase() {
database.open()
database.executeStatements("PRAGMA synchronous = 1;")
database.setShouldCacheStatements(true)
}
}

View File

@@ -0,0 +1,139 @@
//
// DatabaseTable.swift
// RSDatabase
//
// Created by Brent Simmons on 7/16/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
public protocol DatabaseTable {
var name: String { get }
}
public extension DatabaseTable {
// MARK: Fetching
func selectRowsWhere(key: String, equals value: Any, in database: FMDatabase) -> FMResultSet? {
return database.rs_selectRowsWhereKey(key, equalsValue: value, tableName: name)
}
func selectSingleRowWhere(key: String, equals value: Any, in database: FMDatabase) -> FMResultSet? {
return database.rs_selectSingleRowWhereKey(key, equalsValue: value, tableName: name)
}
func selectRowsWhere(key: String, inValues values: [Any], in database: FMDatabase) -> FMResultSet? {
if values.isEmpty {
return nil
}
return database.rs_selectRowsWhereKey(key, inValues: values, tableName: name)
}
// MARK: Deleting
func deleteRowsWhere(key: String, equalsAnyValue values: [Any], in database: FMDatabase) {
if values.isEmpty {
return
}
database.rs_deleteRowsWhereKey(key, inValues: values, tableName: name)
}
// MARK: Updating
func updateRowsWithValue(_ value: Any, valueKey: String, whereKey: String, matches: [Any], database: FMDatabase) {
let _ = database.rs_updateRows(withValue: value, valueKey: valueKey, whereKey: whereKey, inValues: matches, tableName: self.name)
}
func updateRowsWithDictionary(_ dictionary: DatabaseDictionary, whereKey: String, matches: Any, database: FMDatabase) {
let _ = database.rs_updateRows(with: dictionary, whereKey: whereKey, equalsValue: matches, tableName: self.name)
}
// MARK: Saving
func insertRows(_ dictionaries: [DatabaseDictionary], insertType: RSDatabaseInsertType, in database: FMDatabase) {
for oneDictionary in dictionaries {
let _ = database.rs_insertRow(with: oneDictionary, insertType: insertType, tableName: self.name)
}
}
func insertRow(_ rowDictionary: DatabaseDictionary, insertType: RSDatabaseInsertType, in database: FMDatabase) {
insertRows([rowDictionary], insertType: insertType, in: database)
}
// MARK: Counting
func numberWithCountResultSet(_ resultSet: FMResultSet) -> Int {
guard resultSet.next() else {
return 0
}
return Int(resultSet.int(forColumnIndex: 0))
}
func numberWithSQLAndParameters(_ sql: String, _ parameters: [Any], in database: FMDatabase) -> Int {
if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) {
return numberWithCountResultSet(resultSet)
}
return 0
}
// MARK: Mapping
func mapResultSet<T>(_ resultSet: FMResultSet, _ completion: (_ resultSet: FMResultSet) -> T?) -> [T] {
var objects = [T]()
while resultSet.next() {
if let obj = completion(resultSet) {
objects += [obj]
}
}
return objects
}
// MARK: Columns
func containsColumn(_ columnName: String, in database: FMDatabase) -> Bool {
if let resultSet = database.executeQuery("select * from \(name) limit 1;", withArgumentsIn: nil) {
if let columnMap = resultSet.columnNameToIndexMap {
if let _ = columnMap[columnName.lowercased()] {
return true
}
}
}
return false
}
}
public extension FMResultSet {
func compactMap<T>(_ completion: (_ row: FMResultSet) -> T?) -> [T] {
var objects = [T]()
while next() {
if let obj = completion(self) {
objects += [obj]
}
}
close()
return objects
}
func mapToSet<T>(_ completion: (_ row: FMResultSet) -> T?) -> Set<T> {
return Set(compactMap(completion))
}
}

View File

@@ -0,0 +1,229 @@
//
// DatabaseLookupTable.swift
// RSDatabase
//
// Created by Brent Simmons on 8/5/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
// 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));
// articleID is objectID; authorID is relatedObjectID.
public final class DatabaseLookupTable {
private let name: String
private let objectIDKey: String
private let relatedObjectIDKey: String
private let relationshipName: String
private let relatedTable: DatabaseRelatedObjectsTable
private var objectIDsWithNoRelatedObjects = Set<String>()
public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseRelatedObjectsTable, relationshipName: String) {
self.name = name
self.objectIDKey = objectIDKey
self.relatedObjectIDKey = relatedObjectIDKey
self.relatedTable = relatedTable
self.relationshipName = relationshipName
}
public func fetchRelatedObjects(for objectIDs: Set<String>, in database: FMDatabase) -> RelatedObjectsMap? {
let objectIDsThatMayHaveRelatedObjects = objectIDs.subtracting(objectIDsWithNoRelatedObjects)
if objectIDsThatMayHaveRelatedObjects.isEmpty {
return nil
}
guard let relatedObjectIDsMap = fetchRelatedObjectIDsMap(objectIDsThatMayHaveRelatedObjects, database) else {
objectIDsWithNoRelatedObjects.formUnion(objectIDsThatMayHaveRelatedObjects)
return nil
}
if let relatedObjects = fetchRelatedObjectsWithIDs(relatedObjectIDsMap.relatedObjectIDs(), database) {
let relatedObjectsMap = RelatedObjectsMap(relatedObjects: relatedObjects, relatedObjectIDsMap: relatedObjectIDsMap)
let objectIDsWithNoFetchedRelatedObjects = objectIDsThatMayHaveRelatedObjects.subtracting(relatedObjectsMap.objectIDs())
objectIDsWithNoRelatedObjects.formUnion(objectIDsWithNoFetchedRelatedObjects)
return relatedObjectsMap
}
return nil
}
public func saveRelatedObjects(for objects: [DatabaseObject], in database: FMDatabase) {
var objectsWithNoRelationships = [DatabaseObject]()
var objectsWithRelationships = [DatabaseObject]()
for object in objects {
if let relatedObjects = object.relatedObjectsWithName(relationshipName), !relatedObjects.isEmpty {
objectsWithRelationships += [object]
}
else {
objectsWithNoRelationships += [object]
}
}
removeRelationships(for: objectsWithNoRelationships, database)
updateRelationships(for: objectsWithRelationships, database)
objectIDsWithNoRelatedObjects.formUnion(objectsWithNoRelationships.databaseIDs())
objectIDsWithNoRelatedObjects.subtract(objectsWithRelationships.databaseIDs())
}
}
// MARK: - Private
private extension DatabaseLookupTable {
// MARK: Removing
func removeRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
let objectIDs = objects.databaseIDs()
let objectIDsToRemove = objectIDs.subtracting(objectIDsWithNoRelatedObjects)
if objectIDsToRemove.isEmpty {
return
}
database.rs_deleteRowsWhereKey(objectIDKey, inValues: Array(objectIDsToRemove), tableName: name)
}
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)
}
// MARK: Saving/Updating
func updateRelationships(for objects: [DatabaseObject], _ database: FMDatabase) {
if objects.isEmpty {
return
}
if let lookupTable = fetchRelatedObjectIDsMap(objects.databaseIDs(), database) {
for object in objects {
syncRelatedObjectsAndLookupTable(object, lookupTable, database)
}
}
// Save the actual related objects.
let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objects)
if relatedObjectsToSave.isEmpty {
assertionFailure("updateRelationships: expected relatedObjectsToSave would not be empty. This should be unreachable.")
return
}
relatedTable.save(relatedObjectsToSave, in: database)
}
func uniqueArrayOfRelatedObjects(with objects: [DatabaseObject]) -> [DatabaseObject] {
// Cant create a Set, because we cant make a Set<DatabaseObject>, because protocol-conforming objects cant 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.
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 syncRelatedObjectsAndLookupTable(_ object: DatabaseObject, _ lookupTable: RelatedObjectIDsMap, _ 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) {
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)
}
}
// MARK: Fetching
func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set<String>, _ database: FMDatabase) -> [DatabaseObject]? {
guard let relatedObjects = relatedTable.fetchObjectsWithIDs(relatedObjectIDs, in: database), !relatedObjects.isEmpty else {
return nil
}
return relatedObjects
}
func fetchRelatedObjectIDsMap(_ objectIDs: Set<String>, _ database: FMDatabase) -> RelatedObjectIDsMap? {
guard let lookupValues = fetchLookupValues(objectIDs, database) else {
return nil
}
return RelatedObjectIDsMap(lookupValues: lookupValues)
}
func fetchLookupValues(_ objectIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? {
guard !objectIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(objectIDKey, inValues: Array(objectIDs), tableName: name) else {
return nil
}
return lookupValuesWithResultSet(resultSet)
}
func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set<LookupValue> {
return resultSet.mapToSet(lookupValueWithRow)
}
func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? {
guard let objectID = row.string(forColumn: objectIDKey) else {
return nil
}
guard let relatedObjectID = row.string(forColumn: relatedObjectIDKey) else {
return nil
}
return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID)
}
}

View File

@@ -0,0 +1,83 @@
//
// DatabaseRelatedObjectsTable.swift
// RSDatabase
//
// Created by Brent Simmons on 9/2/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSDatabaseObjC
// Protocol for a database table for related objects  authors and attachments in NetNewsWire, for instance.
public protocol DatabaseRelatedObjectsTable: DatabaseTable {
var databaseIDKey: String { get}
var cache: DatabaseObjectCache { get }
func fetchObjectsWithIDs(_ databaseIDs: Set<String>, in database: FMDatabase) -> [DatabaseObject]?
func objectsWithResultSet(_ resultSet: FMResultSet) -> [DatabaseObject]
func objectWithRow(_ row: FMResultSet) -> DatabaseObject?
func save(_ objects: [DatabaseObject], in database: FMDatabase)
}
public extension DatabaseRelatedObjectsTable {
// MARK: Default implementations
func fetchObjectsWithIDs(_ databaseIDs: Set<String>, in database: FMDatabase) -> [DatabaseObject]? {
if databaseIDs.isEmpty {
return nil
}
var cachedObjects = [DatabaseObject]()
var databaseIDsToFetch = Set<String>()
for databaseID in databaseIDs {
if let cachedObject = cache[databaseID] {
cachedObjects += [cachedObject]
}
else {
databaseIDsToFetch.insert(databaseID)
}
}
if databaseIDsToFetch.isEmpty {
return cachedObjects
}
guard let resultSet = selectRowsWhere(key: databaseIDKey, inValues: Array(databaseIDsToFetch), in: database) else {
return cachedObjects
}
let fetchedDatabaseObjects = objectsWithResultSet(resultSet)
cache.add(fetchedDatabaseObjects)
return cachedObjects + fetchedDatabaseObjects
}
func objectsWithResultSet(_ resultSet: FMResultSet) -> [DatabaseObject] {
return resultSet.compactMap(objectWithRow)
}
func save(_ objects: [DatabaseObject], in database: FMDatabase) {
// Objects in cache must already exist in database. Filter them out.
let objectsToSave = objects.filter { (object) -> Bool in
if let _ = cache[object.databaseID] {
return false
}
return true
}
cache.add(objectsToSave)
if let databaseDictionaries = objectsToSave.databaseDictionaries() {
insertRows(databaseDictionaries, insertType: .orIgnore, in: database)
}
}
}

View File

@@ -0,0 +1,63 @@
//
// RelatedObjectIDsMap.swift
// RSDatabase
//
// Created by Brent Simmons on 9/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Maps objectIDs to Set<String> where the Strings are relatedObjectIDs.
struct RelatedObjectIDsMap {
private let dictionary: [String: Set<String>] // objectID: Set<relatedObjectID>
init(dictionary: [String: Set<String>]) {
self.dictionary = dictionary
}
init(lookupValues: Set<LookupValue>) {
var d = [String: Set<String>]()
for lookupValue in lookupValues {
let objectID = lookupValue.objectID
let relatedObjectID: String = lookupValue.relatedObjectID
if d[objectID] == nil {
d[objectID] = Set([relatedObjectID])
}
else {
d[objectID]!.insert(relatedObjectID)
}
}
self.init(dictionary: d)
}
func objectIDs() -> Set<String> {
return Set(dictionary.keys)
}
func relatedObjectIDs() -> Set<String> {
var ids = Set<String>()
for (_, relatedObjectIDs) in dictionary {
ids.formUnion(relatedObjectIDs)
}
return ids
}
subscript(_ objectID: String) -> Set<String>? {
return dictionary[objectID]
}
}
struct LookupValue: Hashable {
let objectID: String
let relatedObjectID: String
}

View File

@@ -0,0 +1,44 @@
//
// RelatedObjectsMap.swift
// RSDatabase
//
// Created by Brent Simmons on 9/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Map objectID to [DatabaseObject] (related objects).
// Its used as the return value for DatabaseLookupTable.fetchRelatedObjects.
public struct RelatedObjectsMap {
private let dictionary: [String: [DatabaseObject]] // objectID: relatedObjects
init(relatedObjects: [DatabaseObject], relatedObjectIDsMap: RelatedObjectIDsMap) {
var d = [String: [DatabaseObject]]()
let relatedObjectsDictionary = relatedObjects.dictionary()
for objectID in relatedObjectIDsMap.objectIDs() {
if let relatedObjectIDs = relatedObjectIDsMap[objectID] {
let relatedObjects = relatedObjectIDs.compactMap{ relatedObjectsDictionary[$0] }
if !relatedObjects.isEmpty {
d[objectID] = relatedObjects
}
}
}
self.dictionary = d
}
public func objectIDs() -> Set<String> {
return Set(dictionary.keys)
}
public subscript(_ objectID: String) -> [DatabaseObject]? {
return dictionary[objectID]
}
}