Create Articles with attached objects.

This commit is contained in:
Brent Simmons 2017-09-13 21:41:01 -07:00
parent 57cf5a25d7
commit 7563906f9b
10 changed files with 172 additions and 73 deletions

View File

@ -175,14 +175,56 @@ private extension ArticlesTable {
// Note: the row is a result of a JOIN query with the statuses table,
// so we can get the status at the same time and avoid additional database lookups.
article.status = statusesTable.statusWithRow(row)
// article.status = statusesTable.statusWithRow(row)
return article
}
func articlesWithResultSet(_ resultSet: FMResultSet, _ database: FMDatabase) -> Set<Article> {
let articles = resultSet.mapToSet(articleWithRow)
attachRelatedObjects(articles, database)
// Create set of stub Articles without related objects.
// Then fetch the related objects, given the set of articleIDs.
// Then create set of Articles *with* related objects and return it.
let stubArticles = resultSet.mapToSet(articleWithRow)
if stubArticles.isEmpty {
return stubArticles
}
// Fetch related objects.
let articleIDs = stubArticles.articleIDs()
let authorsMap = authorsLookupTable.fetchRelatedObjects(for: articleIDs, in: database)
let attachmentsMap = attachmentsLookupTable.fetchRelatedObjects(for: articleIDs, in: database)
let tagsMap = tagsLookupTable.fetchRelatedObjects(for: articleIDs, in: database)
if authorsMap == nil && attachmentsMap == nil && tagsMap == nil {
return stubArticles
}
// Create articles with related objects.
var articles = Set<Article>()
for stubArticle in articles {
var authors: Set<Author>? = nil
var attachments: Set<Attachment>? = nil
var tags: Set<String>? = nil
let articleID = stubArticle.articleID
if let authorsMap = authorsMap {
authors = authorsMap.authors(for: articleID)
}
if let attachmentsMap = attachmentsMap {
attachments = attachmentsMap.attachments(for: articleID)
}
if let tagsMap = tagsMap {
tags = tagsMap.tags(for: articleID)
}
let realArticle = stubArticle.articleByAttaching(authors, attachments, tags)
articles.insert(realArticle)
}
return articles
}
@ -378,13 +420,13 @@ private extension ArticlesTable {
// Only update exactly what has changed in the Article (if anything).
// Untested theory: this gets us better performance and less database fragmentation.
guard let fetchedArticle = fetchedArticle[updatedArticle.articleID] else {
guard let fetchedArticle = fetchedArticles[updatedArticle.articleID] else {
assertionFailure("Expected to find matching fetched article.");
saveNewArticles(Set([updatedArticle]), database)
return
}
guard let changesDictionary = updatedArticle.changesFrom(fetchedArticle), !changesDictionary.isEmpty else {
guard let changesDictionary = updatedArticle.changesFrom(fetchedArticle), changesDictionary.count > 0 else {
// Not unexpected. There may be no changes.
return
}

View File

@ -8,12 +8,13 @@
/* Begin PBXBuildFile section */
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsTable.swift */; };
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */; };
84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */; };
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F20F901F1810DD00D8E682 /* Author+Database.swift */; };
844BEE411F0AB3AB004AB7CD /* Database.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE371F0AB3AA004AB7CD /* Database.framework */; };
844BEE461F0AB3AB004AB7CD /* DatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */; };
845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580661F0AEBCD003CCFA1 /* Constants.swift */; };
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580751F0AF670003CCFA1 /* Article+Database.swift */; };
845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580771F0AF678003CCFA1 /* Folder+Database.swift */; };
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */; };
8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */; };
846146271F0ABC7B00870CB3 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 846146241F0ABC7400870CB3 /* RSParser.framework */; };
@ -113,13 +114,14 @@
/* Begin PBXFileReference section */
840405CE1F1A963700DF0296 /* AttachmentsTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsTable.swift; sourceTree = "<group>"; };
842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseObject+Database.swift"; sourceTree = "<group>"; };
84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RelatedObjectsMap+Database.swift"; sourceTree = "<group>"; };
844BEE371F0AB3AA004AB7CD /* Database.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Database.framework; sourceTree = BUILT_PRODUCTS_DIR; };
844BEE401F0AB3AB004AB7CD /* DatabaseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DatabaseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
844BEE451F0AB3AB004AB7CD /* DatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTests.swift; sourceTree = "<group>"; };
844BEE471F0AB3AB004AB7CD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
845580661F0AEBCD003CCFA1 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
845580751F0AF670003CCFA1 /* Article+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Article+Database.swift"; path = "Extensions/Article+Database.swift"; sourceTree = "<group>"; };
845580771F0AF678003CCFA1 /* Folder+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Folder+Database.swift"; path = "Extensions/Folder+Database.swift"; sourceTree = "<group>"; };
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "ArticleStatus+Database.swift"; path = "Extensions/ArticleStatus+Database.swift"; sourceTree = "<group>"; };
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "Attachment+Database.swift"; path = "Extensions/Attachment+Database.swift"; sourceTree = "<group>"; };
8461461E1F0ABC7300870CB3 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
@ -212,13 +214,14 @@
8461462A1F0AC44100870CB3 /* Extensions */ = {
isa = PBXGroup;
children = (
845580771F0AF678003CCFA1 /* Folder+Database.swift */,
846FB36A1F4A937B00EAB81D /* Feed+Database.swift */,
845580751F0AF670003CCFA1 /* Article+Database.swift */,
845580791F0AF67D003CCFA1 /* ArticleStatus+Database.swift */,
84F20F901F1810DD00D8E682 /* Author+Database.swift */,
8455807B1F0C0DBD003CCFA1 /* Attachment+Database.swift */,
84D0DEA01F4A429800073503 /* String+Database.swift */,
842889FF1F6A3C4400395871 /* DatabaseObject+Database.swift */,
84288A011F6A3D8000395871 /* RelatedObjectsMap+Database.swift */,
);
name = Extensions;
sourceTree = "<group>";
@ -472,13 +475,14 @@
84D0DEA11F4A429800073503 /* String+Database.swift in Sources */,
843CB9961F34174100EE6581 /* Author+Database.swift in Sources */,
848AD2961F58A91E004FB0EC /* UnreadCountDictionary.swift in Sources */,
845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */,
845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */,
8455807A1F0AF67D003CCFA1 /* ArticleStatus+Database.swift in Sources */,
8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */,
84288A021F6A3D8000395871 /* RelatedObjectsMap+Database.swift in Sources */,
84BB4BA91F11A32800858766 /* TagsTable.swift in Sources */,
840405CF1F1A963700DF0296 /* AttachmentsTable.swift in Sources */,
84F20F8F1F180D8700D8E682 /* AuthorsTable.swift in Sources */,
84288A001F6A3C4400395871 /* DatabaseObject+Database.swift in Sources */,
84E156EC1F0AB80E00F8CC05 /* ArticlesTable.swift in Sources */,
84E156EE1F0AB81400F8CC05 /* StatusesTable.swift in Sources */,
84E156EA1F0AB80500F8CC05 /* Database.swift in Sources */,

View File

@ -0,0 +1,32 @@
//
// DatabaseObject+Database.swift
// Database
//
// Created by Brent Simmons on 9/13/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSDatabase
import Data
extension Array where Element == DatabaseObject {
func asTags() -> Set<String>? {
let tags = Set(self.map { $0 as! String })
return tags.isEmpty ? nil : tags
}
func asAuthors() -> Set<Author>? {
let authors = Set(self.map { $0 as! Author })
return authors.isEmpty ? nil : authors
}
func asAttachments() -> Set<Attachment>? {
let attachments = Set(self.map { $0 as! Attachment })
return attachments.isEmpty ? nil : attachments
}
}

View File

@ -13,7 +13,7 @@ import RSParser
extension Article {
init?(row: FMResultSet, authors: Set<Author>, attachments: Set<Attachment>, tags: Set<String>, accountID: String) {
init?(row: FMResultSet, accountID: String, authors: Set<Author>? = nil, attachments: Set<Attachment>? = nil, tags: Set<String>? = nil) {
guard let feedID = row.string(forColumn: DatabaseKey.feedID) else {
return nil
@ -40,12 +40,21 @@ extension Article {
init(parsedItem: ParsedItem, accountID: String, feedID: String) {
let authors = parsedItem.authors?.authors()
let authors = Author.authorsWithParsedAuthors(parsedItem.authors)
let attachments = Attachment.attachmentsWithParsedAttachments(parsedItem.attachments)
self.init(accountID: accountID, articleID: parsedItem.syncServiceID, feedID: feedID, uniqueID: parsedItem.uniqueID, title: parsedItem.title, contentHTML: parsedItem.contentHTML, contentText: parsedItem.contentText, url: parsedItem.url, externalURL: parsedItem.externalURL, summary: parsedItem.summary, imageURL: parsedItem.imageURL, bannerImageURL: parsedItem.bannerImageURL, datePublished: parsedItem.datePublished, dateModified: parsedItem.dateModified, authors: authors, tags: parsedItem.tags, attachments: attachments, accountInfo: nil)
}
func articleByAttaching(_ authors: Set<Author>?, _ attachments: Set<Attachment>?, _ tags: Set<String>?) -> Article {
if authors == nil && attachments == nil && tags == nil {
return self
}
return Article(accountID: accountID, articleID: articleID, feedID: feedID, uniqueID: uniqueID, title: title, contentHTML: contentHTML, contentText: contentText, url: url, externalURL: externalURL, summary: summary, imageURL: imageURL, bannerImageURL: bannerImageURL, datePublished: datePublished, dateModified: dateModified, authors: authors, tags: tags, attachments: attachments, accountInfo: accountInfo)
}
private func addPossibleStringChangeWithKeyPath(_ comparisonKeyPath: KeyPath<Article,String?>, _ otherArticle: Article, _ key: String, _ dictionary: NSMutableDictionary) {
if self[keyPath: comparisonKeyPath] != otherArticle[keyPath: comparisonKeyPath] {

View File

@ -30,6 +30,16 @@ extension Author {
self.init(authorID: nil, name: parsedAuthor.name, url: parsedAuthor.url, avatarURL: parsedAuthor.avatarURL, emailAddress: parsedAuthor.emailAddress)
}
static func authorsWithParsedAuthors(_ parsedAuthors: Set<ParsedAuthor>?) -> Set<Author>? {
guard let parsedAuthors = parsedAuthors else {
return nil
}
let authors = Set(parsedAuthors.flatMap { Author(parsedAuthor: $0) })
return authors.isEmpty ? nil: authors
}
}
extension Author: DatabaseObject {
@ -42,25 +52,24 @@ extension Author: DatabaseObject {
public func databaseDictionary() -> NSDictionary? {
var d = NSMutableDictionary()
let d = NSMutableDictionary()
// TODO
if d.count < 1 {
return nil
d[DatabaseKey.authorID] = authorID
if let name = name {
d[DatabaseKey.name] = name
}
if let url = url {
d[DatabaseKey.url] = url
}
if let avatarURL = avatarURL {
d[DatabaseKey.avatarURL] = avatarURL
}
if let emailAddress = emailAddress {
d[DatabaseKey.emailAddress] = emailAddress
}
return (d.copy() as! NSDictionary)
}
}
// MARK: Author creation from Set<ParsedAuthor>
extension Set where Element == ParsedAuthor {
func authors() -> Set<Author>? {
let createdAuthors = Set(self.flatMap { Author(parsedAuthor: $0) })
return createdAuthors.isEmpty ? nil: createdAuthors
}
}

View File

@ -1,18 +0,0 @@
//
// Folder+Database.swift
// Database
//
// Created by Brent Simmons on 7/3/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import Data
extension Folder {
func flattenedFeedIDs() -> [String] {
return flattenedFeeds().map { $0.feedID }
}
}

View File

@ -17,10 +17,8 @@ extension String: DatabaseObject {
public func databaseDictionary() -> NSDictionary? {
preconditionFailure("databaseDictionary() called for a tag: this should never happen.")
return nil // unused
}
public var databaseID: String {
get {
return self

View File

@ -0,0 +1,38 @@
//
// RelatedObjectsMap+Database.swift
// Database
//
// Created by Brent Simmons on 9/13/17.
// Copyright © 2017 Ranchero Software. All rights reserved.
//
import Foundation
import RSDatabase
import Data
extension RelatedObjectsMap {
func attachments(for articleID: String) -> Set<Attachment>? {
if let objects = self[articleID] {
return objects.asAttachments()
}
return nil
}
func authors(for articleID: String) -> Set<Author>? {
if let objects = self[articleID] {
return objects.asAuthors()
}
return nil
}
func tags(for articleID: String) -> Set<String>? {
if let objects = self[articleID] {
return objects.asTags()
}
return nil
}
}

View File

@ -54,13 +54,12 @@ final class StatusesTable: DatabaseTable {
fetchAndCacheStatusesForArticleIDs(articleIDsMissingCachedStatus) {
let articleIDsNeedingStatus = self.articleIDsWithNoCachedStatus(articleIDs)
if articleIDsNeedingStatus.isEmpty {
completion(self.statusesDictionary(articleIDs))
return
if !articleIDsNeedingStatus.isEmpty {
// Create new statuses.
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus)
}
// Create new statuses.
self.createAndSaveStatusesForArticleIDs(articleIDsNeedingStatus, completion)
completion(self.statusesDictionary(articleIDs))
}
}
@ -121,12 +120,12 @@ private extension StatusesTable {
func saveStatuses(_ statuses: Set<ArticleStatus>) {
queue.update { (database) in
let statusArray = statuses.map { $0.databaseDictionary() }
let statusArray = statuses.map { $0.databaseDictionary()! }
self.insertRows(statusArray, insertType: .orIgnore, in: database)
}
}
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>, _ completion: @escaping RSVoidCompletionBlock) {
func createAndSaveStatusesForArticleIDs(_ articleIDs: Set<String>) {
assert(Thread.isMainThread)
@ -134,10 +133,6 @@ private extension StatusesTable {
let statuses = Set(articleIDs.map { ArticleStatus(articleID: $0, dateArrived: now) })
cache.addIfNotCached(statuses)
// No need to wait for database to return before calling completion,
// since the new statuses have been cached at this point.
completion()
saveStatuses(statuses)
}

View File

@ -6,12 +6,12 @@
</editor> -->
<title>ToDo</title>
<dateCreated>Tue, 12 Sep 2017 20:15:17 GMT</dateCreated>
<expansionState>11,16,17,20,24,29,31,34,37,39,41,43,47,52,55,57,59,61,63,72,77,79,85,90</expansionState>
<vertScrollState>0</vertScrollState>
<windowTop>209</windowTop>
<expansionState>11,16,17,20,24,29,31,34,37,39,40,44,49,52,54,56,58,67,72,78,83</expansionState>
<vertScrollState>59</vertScrollState>
<windowTop>248</windowTop>
<windowLeft>30</windowLeft>
<windowRight>762</windowRight>
<windowBottom>968</windowBottom>
<windowBottom>1007</windowBottom>
</head>
<body>
<outline text="App">
@ -63,20 +63,16 @@
<outline text="Frameworks">
<outline text="Account"/>
<outline text="Database">
<outline text="Get it building again"/>
<outline text="Authors">
<outline text="databaseDictionary()"/>
</outline>
<outline text="Database.swift">
<outline text="updateFeedWithParsedFeed"/>
<outline text="Handle accountInfo in statuses"/>
<outline text="Send notification on saving/updating"/>
</outline>
<outline text="ArticlesTable">
<outline text="Fetch related objects for articles in time for Article.init, so Article can be immutable"/>
<outline text="mark articles"/>
<outline text="Delete old articles"/>
<outline text="Update cutoff date periodically"/>
<outline text="Do something with statuses on fetching articles"/>
</outline>
<outline text="StatusesTable">
<outline text="Update cached statuses on marking"/>
@ -91,9 +87,6 @@
<outline text="Tags table">
<outline text="Search on tags using COLLATE NOCASE"/>
</outline>
<outline text="DatabaseLookupTable">
<outline text="Fetch relationships using unique IDs instead of objects"/>
</outline>
<outline text="Unit tests">
<outline text="fetching articles"/>
<outline text="fetching unread articles"/>
@ -111,9 +104,6 @@
</outline>
</outline>
</outline>
<outline text="RSDatabase">
<outline text="DatabaseRelatedObjectsTable.save implementation should use cache"/>
</outline>
<outline text="Data">
<outline text="Make model classes quicklookable"/>
</outline>