From 2ce577e9d4efeef703b74450734482a43dcebfdb Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 20 Aug 2017 15:56:58 -0700 Subject: [PATCH] Make progress on Database surgery. --- Frameworks/Data/Author.swift | 2 +- Frameworks/Database/AttachmentsTable.swift | 4 +- Frameworks/Database/AuthorsTable.swift | 46 ++-- Frameworks/Database/Constants.swift | 5 + Frameworks/Database/Database.swift | 23 +- .../Database.xcodeproj/project.pbxproj | 4 + .../Database/Extensions/Author+Database.swift | 13 +- .../Database/Extensions/String+Database.swift | 22 ++ Frameworks/Database/StatusesTable.swift | 4 +- Frameworks/Database/TagsTable.swift | 224 ++---------------- Frameworks/RSDatabase/DatabaseTable.swift | 4 +- .../RSDatabase/DatabaseLookupTable.swift | 7 +- .../RSDatabase/DatabaseObject.swift | 12 + ToDo.ooutline | Bin 2523 -> 2454 bytes 14 files changed, 104 insertions(+), 266 deletions(-) create mode 100644 Frameworks/Database/Extensions/String+Database.swift diff --git a/Frameworks/Data/Author.swift b/Frameworks/Data/Author.swift index b24099363..7f354b06f 100644 --- a/Frameworks/Data/Author.swift +++ b/Frameworks/Data/Author.swift @@ -11,7 +11,7 @@ import RSCore public struct Author: Hashable { - public let databaseID: String // calculated + public let authorID: String // calculated public let name: String? public let url: String? public let avatarURL: String? diff --git a/Frameworks/Database/AttachmentsTable.swift b/Frameworks/Database/AttachmentsTable.swift index df94e41a0..f7a39b4d3 100644 --- a/Frameworks/Database/AttachmentsTable.swift +++ b/Frameworks/Database/AttachmentsTable.swift @@ -34,14 +34,12 @@ import Data final class AttachmentsTable: DatabaseTable { let name: String - let queue: RSDatabaseQueue private let cacheByArticleID = ObjectCache(keyPathForID: \Attachment.articleID) private let cacheByDatabaseID = ObjectCache(keyPathForID: \Attachment.databaseID) - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue } private var cachedAttachments = [String: Attachment]() // Attachment.databaseID key diff --git a/Frameworks/Database/AuthorsTable.swift b/Frameworks/Database/AuthorsTable.swift index 4a9c35cb4..cfa4a6b10 100644 --- a/Frameworks/Database/AuthorsTable.swift +++ b/Frameworks/Database/AuthorsTable.swift @@ -17,45 +17,27 @@ import Data // CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID)); -final class AuthorsTable: DatabaseTable { - +struct AuthorsTable: DatabaseTable { + let name: String - let queue: RSDatabaseQueue private let cache = ObjectCache(keyPathForID: \Author.databaseID) - private var articleIDToAuthorsCache = [String: Set]() - private let authorsLookupTable = LookupTable(name: DatabaseTableName.authorsLookup, primaryKey: DatabaseKey.authorID, foreignKey: DatabaseKey.articleID) - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue + } + + // MARK: DatabaseTable Methods + + func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject] { + + + } + + func save(_ objects: [DatabaseObject], in database: FMDatabase) { + <#code#> } - func attachAuthors(_ articles: Set
, _ database: FMDatabase) { - - attachCachedAuthors(articles) - - let articlesMissingAuthors = articlesNeedingAuthors(articles) - if articlesMissingAuthors.isEmpty { - return - } - - let articleIDs = Set(articlesMissingAuthors.map { $0.databaseID }) - let authorTable = fetchAuthorsForArticleIDs(articleIDs, database) - - for article in articlesMissingAuthors { - - let articleID = article.databaseID - - if let authors = authorTable?[articleID] { - articleIDsWithNoAuthors.remove(articleID) - article.authors = Array(authors) - } - else { - articleIDsWithNoAuthors.insert(articleID) - } - } - } } private extension AuthorsTable { diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index dc5baeb58..1a643b679 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -63,3 +63,8 @@ public struct DatabaseKey { static let emailAddress = "emailAddress" } +public struct RelationshipName { + + static let authors = "authors" + static let tags = "tags" +} diff --git a/Frameworks/Database/Database.swift b/Frameworks/Database/Database.swift index 2cf56b796..30e396c6b 100644 --- a/Frameworks/Database/Database.swift +++ b/Frameworks/Database/Database.swift @@ -24,16 +24,17 @@ typealias ArticleResultBlock = (Set
) -> Void final class Database { - fileprivate let queue: RSDatabaseQueue + private let queue: RSDatabaseQueue private let databaseFile: String private let articlesTable: ArticlesTable private let authorsTable: AuthorsTable + private let authorsLookupTable: DatabaseLookupTable private let attachmentsTable: AttachmentsTable private let statusesTable: StatusesTable - private let tagsTable: TagsTable - fileprivate var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! - fileprivate let minimumNumberOfArticles = 10 - fileprivate weak var delegate: AccountDelegate? + private let tagsLookupTable: DatabaseLookupTable + private var articleArrivalCutoffDate = NSDate.rs_dateWithNumberOfDays(inThePast: 3 * 31)! + private let minimumNumberOfArticles = 10 + private weak var delegate: AccountDelegate? init(databaseFile: String, delegate: AccountDelegate) { @@ -42,11 +43,15 @@ final class Database { self.queue = RSDatabaseQueue(filepath: databaseFile, excludeFromBackup: false) self.articlesTable = ArticlesTable(name: DatabaseTableName.articles, queue: queue) - self.authorsTable = AuthorsTable(name: DatabaseTableName.authors, queue: queue) - self.attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments, queue: queue) - self.statusesTable = StatusesTable(name: DatabaseTableName.statuses, queue: queue) - self.tagsTable = TagsTable(name: DatabaseTableName.tags, queue: queue) + self.attachmentsTable = AttachmentsTable(name: DatabaseTableName.attachments) + self.statusesTable = StatusesTable(name: DatabaseTableName.statuses) + + self.authorsTable = AuthorsTable(name: DatabaseTableName.authors) + self.authorsLookupTable = DatabaseLookupTable(name: DatabaseTableName.authorsLookup, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.authorID, relatedTable: authorsTable, relationshipName: RelationshipName.authors) + let tagsTable = TagsTable(name: DatabaseTableName.tags) + self.tagsLookupTable = DatabaseLookupTable(name: DatabaseTableName.tags, objectIDKey: DatabaseKey.articleID, relatedObjectIDKey: DatabaseKey.tagName, relatedTable: tagsTable, relationshipName: RelationshipName.tags) + let createStatementsPath = Bundle(for: type(of: self)).path(forResource: "CreateStatements", ofType: "sql")! let createStatements = try! NSString(contentsOfFile: createStatementsPath, encoding: String.Encoding.utf8.rawValue) queue.createTables(usingStatements: createStatements as String) diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index 2f2177e2f..c583966b9 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -20,6 +20,7 @@ 846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846146241F0ABC7400870CB3 /* RSParser.framework */; }; 84BB4BA21F119C5400858766 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 84BB4B981F119C4900858766 /* RSCore.framework */; }; 84BB4BA91F11A32800858766 /* TagsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB4BA81F11A32800858766 /* TagsTable.swift */; }; + 84D0DEA11F4A429800073503 /* String+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84D0DEA01F4A429800073503 /* String+Database.swift */; }; 84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156E91F0AB80500F8CC05 /* Database.swift */; }; 84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */; }; 84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E156ED1F0AB81400F8CC05 /* StatusesTable.swift */; }; @@ -124,6 +125,7 @@ 8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 84BB4B8F1F119C4900858766 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = ""; }; 84BB4BA81F11A32800858766 /* TagsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagsTable.swift; sourceTree = ""; }; + 84D0DEA01F4A429800073503 /* String+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "String+Database.swift"; path = "Extensions/String+Database.swift"; sourceTree = ""; }; 84E156E81F0AB75600F8CC05 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 84E156E91F0AB80500F8CC05 /* Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Database.swift; sourceTree = ""; }; 84E156EB1F0AB80E00F8CC05 /* ArticlesTable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticlesTable.swift; sourceTree = ""; }; @@ -212,6 +214,7 @@ 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */, 84F20F901F1810DD00D8E682 /* Author+Database.swift */, 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */, + 84D0DEA01F4A429800073503 /* String+Database.swift */, 845580711F0AEE49003CCFA1 /* AccountInfo.swift */, ); name = Extensions; @@ -468,6 +471,7 @@ 84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */, 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, 840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */, + 84D0DEA11F4A429800073503 /* String+Database.swift in Sources */, 843CB9961F34174100EE6581 /* Author+Database.swift in Sources */, 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, diff --git a/Frameworks/Database/Extensions/Author+Database.swift b/Frameworks/Database/Extensions/Author+Database.swift index 5ba5805d9..86d2db4d5 100644 --- a/Frameworks/Database/Extensions/Author+Database.swift +++ b/Frameworks/Database/Extensions/Author+Database.swift @@ -12,13 +12,22 @@ import RSDatabase extension Author { - init?(databaseID: String, row: FMResultSet) { + init?(authorID: String, row: FMResultSet) { let name = row.string(forColumn: DatabaseKey.name) let url = row.string(forColumn: DatabaseKey.url) let avatarURL = row.string(forColumn: DatabaseKey.avatarURL) let emailAddress = row.string(forColumn: DatabaseKey.emailAddress) - self.init(databaseID: databaseID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress) + self.init(authorID: authorID, name: name, url: url, avatarURL: avatarURL, emailAddress: emailAddress) + } +} + +extension Author: DatabaseObject { + + var databaseID: String { + get { + return authorID + } } } diff --git a/Frameworks/Database/Extensions/String+Database.swift b/Frameworks/Database/Extensions/String+Database.swift new file mode 100644 index 000000000..13804d2ee --- /dev/null +++ b/Frameworks/Database/Extensions/String+Database.swift @@ -0,0 +1,22 @@ +// +// String+Database.swift +// Database +// +// Created by Brent Simmons on 8/20/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSDatabase + +// A tag is a String. +// Extending tag to conform to DatabaseObject means extending String to conform to DatabaseObject. + +extension String: DatabaseObject { + + var databaseID: String { + get { + return self + } + } +} diff --git a/Frameworks/Database/StatusesTable.swift b/Frameworks/Database/StatusesTable.swift index e76b80cbf..5055ad852 100644 --- a/Frameworks/Database/StatusesTable.swift +++ b/Frameworks/Database/StatusesTable.swift @@ -18,13 +18,11 @@ import Data final class StatusesTable: DatabaseTable { let name: String - let queue: RSDatabaseQueue private let cache = ObjectCache(keyPathForID: \ArticleStatus.articleID) - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue } func markArticles(_ articles: Set
, statusKey: String, flag: Bool) { diff --git a/Frameworks/Database/TagsTable.swift b/Frameworks/Database/TagsTable.swift index 329f8936f..8ddcd636a 100644 --- a/Frameworks/Database/TagsTable.swift +++ b/Frameworks/Database/TagsTable.swift @@ -11,225 +11,33 @@ import RSDatabase import Data // Article->tags is a many-to-many relationship. -// Since a tag is just a simple string, the tags table and the lookup table are the same table. +// Since a tag is just a String, the tags table and the lookup table are the same table. +// All the heavy lifting is done in DatabaseLookupTable. // // CREATE TABLE if not EXISTS tags(tagName TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(tagName, articleID)); // CREATE INDEX if not EXISTS tags_tagName_index on tags (tagName COLLATE NOCASE); -typealias TagNameSet = Set - -final class TagsTable: DatabaseTable { - +struct TagsTable: DatabaseTable { + let name: String - let queue: RSDatabaseQueue - let lookupTable: LookupTable - init(name: String, queue: RSDatabaseQueue) { + init(name: String) { self.name = name - self.queue = queue - self.lookupTable = LookupTable(name: DatabaseTableName.tags, primaryKey: DatabaseKey.tagName, foreignKey: DatabaseKey.articleID) } - func attachTags(_ articles: Set
, _ database: FMDatabase) { - - guard let lookupTableDictionary = lookupTable.fetchLookupTableDictionary(articleIDs, database) else { - return - } - - for article in articles { - if let lookupValues = lookupTableDictionary[article.databaseID] { - article.tags = lookupValues.tags() - } - } + // MARK: DatabaseTable Methods + + func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject] { + + // A tag is a string, and it is its own databaseID. + return databaseIDs.map{ $0 as DatabaseObject } + } + + func save(_ objects: [DatabaseObject], in database: FMDatabase) { + + // Nothing to do, since tags are saved in the lookup table, not in a separate table. } -// func saveTagsForArticles(_ articles: Set
) { -// -// var articlesToSaveTags = Set
() -// var articlesToRemoveTags = Set
() -// -// articles.forEach { (oneArticle) in -// -// if articleTagsMatchCache(oneArticle) { -// return -// } -// if let tags = oneArticle.tags { -// articlesToSaveTags.insert(oneArticle) -// } -// else { -// articlesToRemoveTags.insert(oneArticle) -// } -// } -// -// if !articlesToSaveTags.isEmpty { -// updateTagsForArticles(articlesToSaveTags) -// } -// -// if !articlesToRemoveTags.isEmpty { -// removeArticleFromTags(articlesToRemoveTags) -// } -// } -} - -private extension TagsTable { - -// func cacheTagsForArticle(_ article: Article, tags: TagNameSet) { -// -// articleIDsWithNoTags.remove(article.articleID) -// articleIDCache[article.articleID] = tags -// } -// -// func cachedTagsForArticleID(_ articleID: String) -> TagNameSet? { -// -// return articleIDsCache[articleID] -// } -// -// func articleTagsMatchCache(_ article: Article) -> Bool { -// -// if let tags = article.tags { -// return tags == articleIDCache[article.articleID] -// } -// return articleIDIsKnowToHaveNoTags(article.articleID) -// } -// -// func articleIDIsKnownToHaveNoTags(_ articleID: String) -> Bool { -// -// return articleIDsWithNoTags.contains(articleID) -// } -// -// func removeTagsFromCacheForArticleID(_ articleID: String) { -// -// articleIDsCache[oneArticleID] = nil -// articleIDsWithNoTags.insert(oneArticleID) -// } -// -// func removeArticleFromTags(_ articles: Set
) { -// -// var articleIDsToRemove = [String]() -// -// articles.forEach { (oneArticle) in -// let oneArticleID = oneArticle.articleID -// if articleIDIsKnownToHaveNoTags(oneArticle) { -// return -// } -// articleIDsToRemove += oneArticleID -// removeTagsFromCacheForArticleID(oneArticleID) -// } -// -// if !articleIDsToRemove.isEmpty { -// queue.update { (database) in -// database.rs_deleteRowsWhereKey(DatabaseKey.articleID, inValues: articleIDsToRemove, tableName: DatabaseTableName.tags) -// } -// } -// } -// -// typealias TagsTable = [String: TagNameSet] // [articleID: Set] -// -// func updateTagsForArticles(_ articles: Set
) { -// -// var tagsForArticleIDs = TagsTable() -// articles.forEach { (oneArticle) -// if let tags = oneArticle.tags { -// cacheTagsForArticle(oneArticle, tags) -// tagsForArticleIDs[oneArticle.articleID] = oneArticle.tags -// } -// else { -// assertionFailure("article must have tags") -// } -// } -// -// if tagsForArticleIDs.isEmpty { // Shouldn’t be empty -// return -// } -// let articleIDs = tagsForArticleIDs.keys -// -// queue.update { (database) in -// -// let existingTags = self.fetchTagsForArticleIDs(articleIDs, database: database) -// self.syncIncomingAndExistingTags(incomingTags: tagsForArticleIDs, existingTags: existingTags, database: database) -// } -// } -// -// func syncIncomingAndExistingTags(incomingTags: TagsTable, existingTags: TagsTable, database: database) { -// -// for (oneArticleID, oneTagNames) in incomingTags { -// if let existingTagNames = existingTags[oneArticleID] { -// syncIncomingAndExistingTagsForArticleID(oneArticleID, incomingTagNames: oneTagNames, existingTagNames: existingTagNames, database: database) -// } -// else { -// saveIncomingTagsForArticleID(oneArticleID, tagNames: oneTagNames, database: database) -// } -// } -// } -// -// func saveIncomingTagsForArticleID(_ articleID: String, tagNames: TagNameSet, database: FMDatabase) { -// -// // No existing tags in database. Simple save. -// -// for oneTagName in tagNames { -// let oneDictionary = [DatabaseTableName.articleID: articleID, DatabaseTableName.tagName: oneTagName] -// database.rs_insertRow(with: oneDictionary, insertType: .OrIgnore, tableName: DatabaseTableName.tags) -// } -// } -// -// func syncingIncomingAndExistingTagsForArticleID(_ articleID: String, incomingTagNames: TagNameSet, existingTagNames: TagNameSet, database: FMDatabase) { -// -// if incomingTagNames == existingTagNames { -// return -// } -// -// var tagsToRemove = TagNameSet() -// for oneExistingTagName in existingTagNames { -// if !incomingTagNames.contains(oneExistingTagName) { -// tagsToRemove.insert(oneExistingTagName) -// } -// } -// -// var tagsToAdd = TagNameSet() -// for oneIncomingTagName in incomingTagNames { -// if !existingTagNames.contains(oneIncomingTagName) { -// tagsToAdd.insert(oneIncomingTagName) -// } -// } -// -// if !tagsToRemove.isEmpty { -// let placeholders = NSString.rs_SQLValueListWithPlaceholders -// let sql = "delete from \(DatabaseTableName.tags) where \(DatabaseKey.articleID) = ? and \(DatabaseKey.tagName) in " -// database.executeUpdate(sql, withArgumentsIn: [articleID, ]) -// } -// } -// -// func fetchTagsForArticleIDs(_ articleIDs: Set, database: FMDatabase) -> TagsTable { -// -// var tagSpecifiers = TagsTable() -// -// guard let rs = database.rs_selectRowsWhereKey(DatabaseKey.articleID, inValues: Array(articleIDs), tableName: DatabaseTableName.tags) else { -// return tagSpecifiers -// } -// -// while rs.next() { -// -// guard let oneTagName = rs.string(forColumn: DatabaseKey.tagName), let oneArticleID = rs.string(forColumn: DatabaseKey.articleID) else { -// continue -// } -// if tagSpecifiers[oneArticleID] == nil { -// tagSpecifiers[oneArticleID] = Set([oneTagName]) -// } -// else { -// tagSpecifiers[oneArticleID]!.insert(oneTagName) -// } -// } -// -// return tagSpecifiers -// } -} - -private extension Set where Element == LookupValue { - - func tags() -> Set { - - return Set(flatMap{ $0.primaryID }) - } } diff --git a/Frameworks/RSDatabase/DatabaseTable.swift b/Frameworks/RSDatabase/DatabaseTable.swift index 5e0946032..6303e45fa 100644 --- a/Frameworks/RSDatabase/DatabaseTable.swift +++ b/Frameworks/RSDatabase/DatabaseTable.swift @@ -8,9 +8,9 @@ import Foundation -public protocol DatabaseTable: class { +public protocol DatabaseTable { - var name: String {get} + var name: String { get } func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject] func save(_ objects: [DatabaseObject], in database: FMDatabase) diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift b/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift index 0dec93042..b107bf00d 100644 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift +++ b/Frameworks/RSDatabase/RSDatabase/DatabaseLookupTable.swift @@ -18,7 +18,7 @@ public final class DatabaseLookupTable { private let objectIDKey: String private let relatedObjectIDKey: String private let relationshipName: String - private weak var relatedTable: DatabaseTable? + private let relatedTable: DatabaseTable private let cache: DatabaseLookupTableCache public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseTable, relationshipName: String) { @@ -124,11 +124,6 @@ private extension DatabaseLookupTable { // 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.") diff --git a/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift b/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift index 66e460766..b7f51d95d 100644 --- a/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift +++ b/Frameworks/RSDatabase/RSDatabase/DatabaseObject.swift @@ -16,6 +16,18 @@ public protocol DatabaseObject { func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? } +public extension DatabaseObject { + + func setRelatedObjects(_ objects: [DatabaseObject], name: String) { + // Do nothing + } + + func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? { + + return nil + } +} + extension Array where Element == DatabaseObject { func dictionary() -> [String: DatabaseObject] { diff --git a/ToDo.ooutline b/ToDo.ooutline index d2847926a886c83ace93b8cbc9adb2279aaf8747..fab1f61b1cd9f4959b931c8d464316595c95cf62 100644 GIT binary patch delta 2414 zcmV-!36b{O6P6QyP)h>@6aWAK2ms}K6ie|jQK`@d003_x000aC003ieZggdCbaO6v zZEV$9TT|OO6n^io@c3!>LGdkd!cLOTc3BFgTWEon_JvVw#SxJ&$Z{_I^()zyuK^MQ z%+Qy{l8*G9Pv1F562tc^pC}7NV;lw})#&J|f&wRWao~-A)T_%QFi_u*-wnku;D;_E z%8a7vNS!ej{iSKk<+2m{0rqH^L>(vewE+4kj-Z3IGv0odFoFX_HEthfzrmu{p@F9= zY#pJI3ZsZ%2Qt3C7^Et(GebTUok3izLlJ?i!0Uw6(}Gxw8BH9PP^59h0$)D2!e^*h zZH;cx%iw^2a0;fF@c#qg;&?t%rvx%}{7zAZFz`Pg^k+6n2^oO z@(#rbNl1&Ov#)G=zCik^EQKdpzUHn_Y8=G)H`)k)CF{sR1c^UE91{*0@+c(fBtFjk z^5JTv+P$7-t7F6556%){Z^kwPl3n*gG8&MKt~%~L84hO12P_0Z$mNx)^bfAtih`?} z{j)$!!=(faC*cAq!d{7>D}s0n@VW%A>$*DDpA0{O6!Mv)8GrT*qjrc6qB8$&}H(o_aJ zSenw(;fPiw*)V26>o0C@DS(?Xy&)LHfLHBP> zT0^tZN;&$`jJ$6Kk(El+yg6bsj?SBbyeQ>ZYkfs{y;9v`XvMhBow)!LxD9BcS0XA0 zZCz9qxDQ0h9bXaAbhqo^iuUK^tkqaM4GqOYE=Y&O`7D(t1-m@duj1>^k$%1~JPGW7 z7(r376z&l}dqo7UK}b1x0j2<$&QJ-wG{BnYoU*@gDJ`d5Mbw+9ikPxv_H5m@IZyJV z&r|QD_^5@qmM-(avgy%fZ0V1W%+C~Kt1&dERFYod>mCN(E|}5}KbLFsy2+r~V$gUa z3>t%dX_6FlF!=elNlULqOZTnNV(guUjFZ#er=w5c=u49pyG2XyjnHzicMM(i&*ADI zK3@6-D?B_&da`O4ID0EzHlISa;K<0y>G!*R)6U4rjvbt$2l(09ZQpio{xhe4m-hO+ z?RL8=^nm>^zK3KJ@?%_=;zYtSqFBn{~{#7B)t38TYW!8AKN6fYtZ1ON$B?V z-awMjw+FTU#oN36N`cml}Y!^#IR!BvQ+Nl=TKh6o=Ll-b(0`?s!q-5O_qFF+e?YulDL z5zNLz2N9wyAjRS#K+q@|Ji-JCYQN`wCoXRkPekH@X{ZU~O8iz_#3mnR?)Bku>?IDr@yD64v#0ryx<;|8Nc+rcq~0Fg9bbKPZw@_PZej*m5|@3~ z*2U-bcW`De)bx3>=y5N9qc7H{<piM{1h-Xc)3Q=g9C`KHgE?(&DK@Un_T z`QPj?*P*FyL*dihyI{#AR;E1Mqhvy`qx^k-@~o~-Z|47olRKaF&$e*YctKYW(NsMD zzmT6H0Qf4QW_0a8=I5(x_yRE{iInWmOyT5~$8fy2^Vu(do?(ik*7)83P)h>@6aWAK z2ms}K6iaL?(F^JU007Mc000XB003oVX>@OLb1ryoY>iS|Z__Xoe($fa{EVAkwyHu+ zC#Go>gs27^3|=PJJ|td!!*-e$#DC|cO*+N|+FNwa`L6NoY-=>xK#0oOoDTZ~O27&y zl`V35bA8Ewru1yyn>A<5I0>4R0lww5Ov%5@vbwJO&RA6hSNXnhCbQf?^jtu;#QCyH zT3HAg;@#n!fmF$b$Tq`%umvS)=OL%ud#wad=%YENB#IIYZyhw#yctZH9E-Bc!Dx!r26WHnFIFpwpj|>F5QSOiz9_Cbk;!H&rJ!p-Q#Tu(;U^y;j zPInL~X@Gli)^&K0IjkF}=rt+{D;`L#WGZty8jNYyu3A%#GQ`$mWD4n5=cT;8Sj3_d zEze(n=Ut%Y`2KeM@!jqdTTV8VluAly9dw%G5yq>G;bE84J-oBOmK<$j| z7yH&0YxVWb=*mP?hOOsTcK_-q{)A-~L4!y+AX&5e7d2eHy8AF#i<{90G4T{)Bpeg literal 2523 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