diff --git a/Frameworks/Data/Article.swift b/Frameworks/Data/Article.swift index 35c899c6e..022109532 100644 --- a/Frameworks/Data/Article.swift +++ b/Frameworks/Data/Article.swift @@ -39,10 +39,9 @@ public final class Article: Hashable { } } - init(account: Account, databaseID: String, feedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: [Author]?, tags: Set?, attachments: [Attachment]?, accountInfo: AccountInfo?) { + init(account: Account, databaseID: String?, feedID: String, uniqueID: String, title: String?, contentHTML: String?, contentText: String?, url: String?, externalURL: String?, summary: String?, imageURL: String?, bannerImageURL: String?, datePublished: Date?, dateModified: Date?, authors: [Author]?, tags: Set?, attachments: [Attachment]?, accountInfo: AccountInfo?) { self.account = account - self.databaseID = databaseID self.feedID = feedID self.uniqueID = uniqueID self.title = title @@ -59,8 +58,17 @@ public final class Article: Hashable { self.tags = tags self.attachments = attachments self.accountInfo = accountInfo - - self.hashValue = account.hashValue ^ self.articleID.hashValue + + let _databaseID: String + if let databaseID = databaseID { + _databaseID = databaseID + } + else { + _databaseID = databaseIDWithString("\(feedID) \(uniqueID)") + } + self.databaseID = _databaseID + + self.hashValue = account.hashValue ^ _databaseID.hashValue } public class func ==(lhs: Article, rhs: Article) -> Bool { diff --git a/Frameworks/Data/Attachment.swift b/Frameworks/Data/Attachment.swift index 73fdbd5fa..40ed4cb75 100644 --- a/Frameworks/Data/Attachment.swift +++ b/Frameworks/Data/Attachment.swift @@ -8,21 +8,43 @@ import Foundation -public struct Attachment: Equatable { +public struct Attachment: Hashable { + public let databaseID: String // Calculated + public let articleID: String // Article.databaseID public let url: String public let mimeType: String? public let title: String? public let sizeInBytes: Int? public let durationInSeconds: Int? + public let hashValue: Int - public init(url: String, mimeType: String?, title: String?, sizeInBytes: Int?, durationInSeconds: Int?) { + public init(databaseID: String?, articleID: String, url: String, mimeType: String?, title: String?, sizeInBytes: Int?, durationInSeconds: Int?) { + self.articleID = articleID self.url = url self.mimeType = mimeType self.title = title self.sizeInBytes = sizeInBytes self.durationInSeconds = durationInSeconds + + var s = articleID + url + s += mimeType ?? "" + s += title ?? "" + if let sizeInBytes = sizeInBytes { + s += "\(sizeInBytes)" + } + if let durationInSeconds = durationInSeconds { + s += "\(durationInSeconds)" + } + self.hashValue = s.hashValue + + if let databaseID = databaseID { + self.databaseID = databaseID + } + else { + self.databaseID = databaseIDWithString(s) + } } public static func ==(lhs: Attachment, rhs: Attachment) -> Bool { diff --git a/Frameworks/Data/Author.swift b/Frameworks/Data/Author.swift index 2a670abc9..b24099363 100644 --- a/Frameworks/Data/Author.swift +++ b/Frameworks/Data/Author.swift @@ -33,9 +33,12 @@ public struct Author: Hashable { s += avatarURL ?? "" s += emailAddress ?? "" self.hashValue = s.hashValue - - if databaseID == nil { - self.databaseID = (s as NSString).rs_md5Hash() + + if let databaseID = databaseID { + self.databaseID = databaseID + } + else { + self.databaseID = databaseIDWithString(s) } } diff --git a/Frameworks/Data/Data.xcodeproj/project.pbxproj b/Frameworks/Data/Data.xcodeproj/project.pbxproj index d152730ac..ab852b981 100644 --- a/Frameworks/Data/Data.xcodeproj/project.pbxproj +++ b/Frameworks/Data/Data.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 840405CA1F1A8E4300DF0296 /* DatabaseID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405C91F1A8E4300DF0296 /* DatabaseID.swift */; }; 843079FA1F0AB57F00B4B7F7 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEEA31F0AB512004AB7CD /* RSCore.framework */; }; 844BEE651F0AB3C9004AB7CD /* Data.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 844BEE5B1F0AB3C8004AB7CD /* Data.framework */; }; 844BEE6A1F0AB3C9004AB7CD /* DataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844BEE691F0AB3C9004AB7CD /* DataTests.swift */; }; @@ -61,6 +62,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 840405C91F1A8E4300DF0296 /* DatabaseID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseID.swift; sourceTree = ""; }; 844BEE5B1F0AB3C8004AB7CD /* Data.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Data.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 844BEE641F0AB3C9004AB7CD /* DataTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = DataTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 844BEE691F0AB3C9004AB7CD /* DataTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataTests.swift; sourceTree = ""; }; @@ -116,6 +118,7 @@ 844BEE801F0AB4D0004AB7CD /* Author.swift */, 844BEE821F0AB4D6004AB7CD /* Attachment.swift */, 844BEE841F0AB4DB004AB7CD /* ArticleStatus.swift */, + 840405C91F1A8E4300DF0296 /* DatabaseID.swift */, 844BEE861F0AB4E3004AB7CD /* BatchUpdates.swift */, 844BEE881F0AB4E7004AB7CD /* Notifications.swift */, 844BEE8A1F0AB4EF004AB7CD /* OPML */, @@ -342,6 +345,7 @@ 844BEE871F0AB4E3004AB7CD /* BatchUpdates.swift in Sources */, 844BEE811F0AB4D0004AB7CD /* Author.swift in Sources */, 844BEE921F0AB4EF004AB7CD /* OPMLRepresentable.swift in Sources */, + 840405CA1F1A8E4300DF0296 /* DatabaseID.swift in Sources */, 844BEE9B1F0AB4F8004AB7CD /* UnreadCountProvider.swift in Sources */, 844BEE851F0AB4DB004AB7CD /* ArticleStatus.swift in Sources */, ); diff --git a/Frameworks/Data/DatabaseID.swift b/Frameworks/Data/DatabaseID.swift new file mode 100644 index 000000000..83249e74d --- /dev/null +++ b/Frameworks/Data/DatabaseID.swift @@ -0,0 +1,18 @@ +// +// DatabaseID.swift +// Data +// +// Created by Brent Simmons on 7/15/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation + +// MD5 works because: +// * It’s fast +// * Collisions aren’t going to happen with feed data + +public func databaseIDWithString(_ s: String) -> String { + + return (s as NSString).rs_md5Hash() +} diff --git a/Frameworks/Database/AttachmentsManager.swift b/Frameworks/Database/AttachmentsManager.swift new file mode 100644 index 000000000..61ff52772 --- /dev/null +++ b/Frameworks/Database/AttachmentsManager.swift @@ -0,0 +1,233 @@ +// +// AttachmentsManager.swift +// Database +// +// Created by Brent Simmons on 7/15/17. +// Copyright © 2017 Ranchero Software. All rights reserved. +// + +import Foundation +import RSDatabase +import Data + +// Attachments are treated as atomic. +// If an attachment in a feed changes any of its values, +// it’s actually saved as a new attachment and the old one is deleted. +// (This is rare compared to an article in a feed changing its text, for instance.) +// +// Article -> Attachment is one-to-many. +// Attachment -> Article is one-to-one. +// A given attachment can be owned by one and only one Article. +// An attachment with the same exact values (except for articleID) might exist. +// (That would be quite rare. But it’s by design.) +// +// All the functions here must be called only from inside the database serial queue. +// (The serial queue makes locking unnecessary.) +// +// Attachments are cached, for the lifetime of the app run, once fetched or saved. +// Because: +// * They don’t take up much space. +// * It seriously cuts down on the number of database reads and writes. + +final class AttachmentsManager { + + private var cachedAttachments = [String: Attachment]() // Attachment.databaseID key + private var cachedAttachmentsByArticle = [String: Set]() // Article.databaseID key + private var articlesWithNoAttachments = Set() // Article.databaseID + private let table = DatabaseTable(name: DatabaseTableName.attachments) + + func fetchAttachmentsForArticles(_ articles: Set
, database: FMDatabase) { + + } + + func saveAttachmentsForArticles(_ articles: Set
, database: FMDatabase) { + + // This is complex and overly long because it’s optimized for fewest database hits. + + var articlesWithPossiblyAllAttachmentsDeleted = Set
() + var attachmentsToSave = Set() + var attachmentsToDelete = Set() + + func reconcileAttachments(incomingAttachments: Set, existingAttachments: Set) { + + for oneIncomingAttachment in incomingAttachments { // Add some. + if !existingAttachments.contains(oneIncomingAttachment) { + attachmentsToSave.insert(oneIncomingAttachment) + } + } + for oneExistingAttachment in existingAttachments { // Delete some. + if !incomingAttachments.contains(oneExistingAttachment) { + attachmentsToDelete.insert(oneExistingAttachment) + } + } + } + + for oneArticle in articles { + + if let oneAttachments = oneArticle.attachments, !oneAttachments.isEmpty { + + // If it matches the cache, then do nothing. + if let oneCachedAttachments = cachedAttachmentsByArticle(oneArticle.databaseID) { + if oneCachedAttachments == oneAttachments { + continue + } + + // There is a cache and it doesn’t match. + reconcileAttachments(incomingAttachments: oneAttachments, existingAttachments: oneCachedAttachments) + } + + else { // no cache, but article has attachments + + if let resultSet = table.selectRowsWhere(key: DatabaseKey.articleID, equals: oneArticle.databaseID, in: database) { + let existingAttachments = attachmentsWithResultSet(resultSet) + if existingAttachments != oneAttachments { // Don’t match? + reconcileAttachments(incomingAttachments: oneAttachments, existingAttachments: existingAttachments) + } + } + else { + // Nothing in database. Just save. + attachmentsToSave.formUnion(oneAttachments) + } + } + + cacheAttachmentsForArticle(oneArticle) + } + else { + // No attachments: might need to delete them all from database + if !articlesWithNoAttachments.contains(oneArticle.databaseID) { + articlesWithPossiblyAllAttachmentsDeleted.insert(oneArticle) + uncacheAttachmentsForArticle(oneArticle) + } + } + } + + if !articlesWithPossiblyAllAttachmentsDeleted.isEmpty { + deleteAttachmentsForArticles(articlesWithPossiblyAllAttachmentsDeleted) + } + } +} + +private extension AttachmentsManager { + + func deleteAttachmentsForArticles(_ articles: Set
, database: FMDatabase) { + + let articleIDs = articles.map { $0.databaseID } + articlesWithNoAttachments.formUnion(Set(articleIDs)) + articleIDs.forEach { cachedAttachmentsByArticle[$0] = nil } + + let _ = database.rs_deleteRowsWhereKey(DatabaseKey.articleID, inValues: articleIDs, tableName: DatabaseTableName.attachments) + } + + func addCachedAttachmentsToArticle(_ article: Article) { + + if let _ = article.attachments { + return + } + + if let attachments = cachedAttachmentsByArticle[article.databaseID] { + article.attachments = attachments + } + } + + func fetchAttachmentsForArticle(_ article: Article, database: FMDatabase) { + + if articlesWithNoAttachments.contains(article.databaseID) { + return + } + addCachedAttachmentsToArticle(article) + if let _ = article.attachments { + return + } + + + + + } + + func uncacheAttachmentsForArticle(_ article: Article) { + + assert(article.attachments == nil || article.attachments.isEmpty) + articlesWithNoAttachments.insert(article.databaseID) + cachedAttachmentsByArticle[article.databaseID] = nil + + var attachmentDatabaseIDsToUncache = Set() + for (databaseID, attachment) in cachedAttachments { + if attachment.articleID == article.databaseID { + attachmentDatabaseIDsToUncache.insert(databaseID) + } + } + attachmentDatabaseIDsToUncache.forEach { uncacheAttachmentWithDatabaseID($0) } + } + + func cacheAttachmentsForArticle(_ article: Article) { + + guard let attachments = article.attachments, !attachments.isEmpty else { + assertionFailure("article.attachments must not be empty") + } + + articlesWithNoAttachments.remove(article.databaseID) + cachedAttachmentsByArticle[article.databaseID] = attachments + cacheAttachment(attachments) + } + + func cachedAttachmentForDatabaseID(_ databaseID: String) -> Attachment? { + + return cachedAttachments[databaseID] + } + + func cacheAttachments(_ attachments: Set) { + + attachments.forEach { cacheAttachment($) } + } + + func cacheAttachment(_ attachment: Attachment) { + + cachedAttachments[attachment.databaseID] = attachment + } + + func uncacheAttachmentWithDatabaseID(_ databaseID: String) { + + cachedAttachments[databaseID] = nil + } + + func saveAttachmentsForArticle(_ article: Article, database: FMDatabase) { + + if let attachments = article.attachments { + + } + else { + if articlesWithNoAttachments.contains(article.databaseID) { + return + } + + articlesWithNoAttachments.insert(article.databaseID) + cachedAttachmentsByArticle[article.databaseID] = nil + + deleteAttachmentsForArticleID(article.databaseID) + } + + } + + func attachmentsWithResultSet(_ resultSet: FMResultSet) -> Set { + + var attachments = Set() + + while (resultSet.next()) { + if let oneAttachment = attachmentWithRow(resultSet) { + attachments.insert(oneAttachment) + } + } + + return attachments + } + + func attachmentWithRow(_ row: FMResultSet) -> Attachment? { + + let databaseID = row.string(forColumn: DatabaseKey.databaseID) + if let cachedAttachment = cachedAttachmentForDatabaseID(databaseID) { + return cachedAttachment + } + + return Attachment(databaseID: databaseID, row: row) + } +} diff --git a/Frameworks/Database/Constants.swift b/Frameworks/Database/Constants.swift index f65d1580a..84b1c9277 100644 --- a/Frameworks/Database/Constants.swift +++ b/Frameworks/Database/Constants.swift @@ -13,11 +13,13 @@ public struct DatabaseTableName { static let articles = "articles" static let statuses = "statuses" static let tags = "tags" + static let attachments = "attachments" } public struct DatabaseKey { // Shared + static let databaseID = "databaseID" static let articleID = "articleID" static let accountInfo = "accountInfo" static let url = "url" diff --git a/Frameworks/Database/CreateStatements.sql b/Frameworks/Database/CreateStatements.sql index 0ed7b64c8..5881078c4 100644 --- a/Frameworks/Database/CreateStatements.sql +++ b/Frameworks/Database/CreateStatements.sql @@ -7,9 +7,11 @@ CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT CREATE TABLE if not EXISTS tags(tagName TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(tagName, articleID)); -CREATE TABLE if not EXISTS attachments(articleID TEXT NOT NULL, url TEXT NOT NULL, mimeType TEXT, title TEXT, sizeInBytes INTEGER, durationInSeconds INTEGER, PRIMARY KEY(articleID, url)); +CREATE TABLE if not EXISTS attachments(databaseID TEXT NOT NULL PRIMARY KEY, articleID TEXT NOT NULL, url TEXT NOT NULL, mimeType TEXT, title TEXT, sizeInBytes INTEGER, durationInSeconds INTEGER); CREATE INDEX if not EXISTS articles_feedID_index on articles (feedID); -CREATE INDEX if not EXISTS tags_tagName_index on tags(tagName COLLATE NOCASE); +CREATE INDEX if not EXISTS tags_tagName_index on tags (tagName COLLATE NOCASE); + +CREATE INDEX if not EXISTS attachments_articleID_index on attachments (articleID); diff --git a/Frameworks/Database/Database.xcodeproj/project.pbxproj b/Frameworks/Database/Database.xcodeproj/project.pbxproj index e36fcdcec..a1034a294 100644 --- a/Frameworks/Database/Database.xcodeproj/project.pbxproj +++ b/Frameworks/Database/Database.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 840405CF1F1A963700DF0296 /* AttachmentsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405CE1F1A963700DF0296 /* AttachmentsManager.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 */; }; @@ -108,6 +109,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 840405CE1F1A963700DF0296 /* AttachmentsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsManager.swift; sourceTree = ""; }; 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 = ""; }; @@ -163,6 +165,7 @@ 84E156EB1F0AB80E00F8CC05 /* ArticlesManager.swift */, 84E156ED1F0AB81400F8CC05 /* StatusesManager.swift */, 84F20F8E1F180D8700D8E682 /* AuthorsManager.swift */, + 840405CE1F1A963700DF0296 /* AttachmentsManager.swift */, 84BB4BA81F11A32800858766 /* TagsManager.swift */, 8461462A1F0AC44100870CB3 /* Extensions */, 84E156EF1F0AB81F00F8CC05 /* CreateStatements.sql */, @@ -463,6 +466,7 @@ 8455807C1F0C0DBD003CCFA1 /* Attachment+Database.swift in Sources */, 84F20F8F1F180D8700D8E682 /* AuthorsManager.swift in Sources */, 845580671F0AEBCD003CCFA1 /* Constants.swift in Sources */, + 840405CF1F1A963700DF0296 /* AttachmentsManager.swift in Sources */, 845580781F0AF678003CCFA1 /* Folder+Database.swift in Sources */, 845580761F0AF670003CCFA1 /* Article+Database.swift in Sources */, 845580721F0AEE49003CCFA1 /* AccountInfo.swift in Sources */, diff --git a/Frameworks/Database/Extensions/Attachment+Database.swift b/Frameworks/Database/Extensions/Attachment+Database.swift index c8dca6bf3..d4b578dab 100644 --- a/Frameworks/Database/Extensions/Attachment+Database.swift +++ b/Frameworks/Database/Extensions/Attachment+Database.swift @@ -8,29 +8,28 @@ import Foundation import Data +import RSDatabase extension Attachment { - - init?(databaseDictionary d: [String: Any]) { - - guard let url = d[DatabaseKey.url] as? String else { - return nil - } - let mimeType = d[DatabaseKey.mimeType] as? String - let title = d[DatabaseKey.title] as? String - let sizeInBytes = d[DatabaseKey.sizeInBytes] as? Int - let durationInSeconds = d[DatabaseKey.durationInSeconds] as? Int - - self.init(url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds) + + init?(databaseID: String, row: FMResultSet) { + + let articleID = row.string(forColumn: DatabaseKey.articleID) + let url = row.string(forColumn: DatabaseKey.url) + let mimeType = row.string(forColumn: DatabaseKey.mimeType) + let title = row.string(forColumn: DatabaseKey.title) + let sizeInBytes = optionalIntForColumn(row, DatabaseKey.sizeInBytes) + let durationInSeconds = optionalIntForColumn(row, DatabaseKey.durationInSeconds) + + init(databaseID: databaseID, articleID: articleID, url: url, mimeType: mimeType, title: title, sizeInBytes: sizeInBytes, durationInSeconds: durationInSeconds) } - - static func attachments(with plist: [Any]) -> [Attachment]? { - - return plist.flatMap{ (oneDictionary) -> Attachment? in - if let d = oneDictionary as? [String: Any] { - return Attachment(databaseDictionary: d) - } + + private func optionalIntForColumn(_ row: FMResultSet, _ columnName: String) -> Int? { + + let intValue = row.long(forColumn: columnName) + if intValue < 1 { return nil } + return intValue } } diff --git a/Frameworks/RSDatabase/DatabaseTable.swift b/Frameworks/RSDatabase/DatabaseTable.swift new file mode 100644 index 000000000..9ff3767c6 --- /dev/null +++ b/Frameworks/RSDatabase/DatabaseTable.swift @@ -0,0 +1,25 @@ +// +// DatabaseTable.swift +// RSDatabase +// +// Created by Brent Simmons on 7/16/17. +// Copyright © 2017 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +public struct DatabaseTable { + + public let name: String + + public init(name: String) { + + self.name = name + } + + public func selectRowsWhere(key: String, equals value: Any, in database: FMDatabase) -> FMResultSet? { + + return database.rs_selectRowsWhereKey(key, equalsValue: value, tableName: self.name) + } + +} diff --git a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj index d59abce83..4f6f2a5df 100755 --- a/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj +++ b/Frameworks/RSDatabase/RSDatabase.xcodeproj/project.pbxproj @@ -23,6 +23,8 @@ 8400AC0C1E0CFC3100AA7C57 /* FMResultSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 84DDF1951C94FC45005E6CF5 /* FMResultSet.m */; }; 8400AC0F1E0CFC5600AA7C57 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8400AC0E1E0CFC5600AA7C57 /* libsqlite3.tbd */; }; 8400AC101E0CFD6B00AA7C57 /* RSDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 84F22C581B52E0D9000060CE /* RSDatabase.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 840405DB1F1C158C00DF0296 /* DatabaseTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405DA1F1C158C00DF0296 /* DatabaseTable.swift */; }; + 840405DC1F1C15EA00DF0296 /* DatabaseTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 840405DA1F1C158C00DF0296 /* DatabaseTable.swift */; }; 84419AD61B5ABD6D00C26BB2 /* FMDatabase+RSExtras.h in Headers */ = {isa = PBXBuildFile; fileRef = 84419AD41B5ABD6D00C26BB2 /* FMDatabase+RSExtras.h */; settings = {ATTRIBUTES = (Public, ); }; }; 84419AD71B5ABD6D00C26BB2 /* FMDatabase+RSExtras.m in Sources */ = {isa = PBXBuildFile; fileRef = 84419AD51B5ABD6D00C26BB2 /* FMDatabase+RSExtras.m */; }; 84419ADA1B5ABD7400C26BB2 /* NSString+RSDatabase.h in Headers */ = {isa = PBXBuildFile; fileRef = 84419AD81B5ABD7400C26BB2 /* NSString+RSDatabase.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -57,6 +59,7 @@ 8400ABF71E0CFBD800AA7C57 /* RSDatabase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = RSDatabase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8400ABFA1E0CFBD800AA7C57 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 8400AC0E1E0CFC5600AA7C57 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS10.2.sdk/usr/lib/libsqlite3.tbd; sourceTree = DEVELOPER_DIR; }; + 840405DA1F1C158C00DF0296 /* DatabaseTable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseTable.swift; sourceTree = ""; }; 84419AD41B5ABD6D00C26BB2 /* FMDatabase+RSExtras.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FMDatabase+RSExtras.h"; path = "RSDatabase/FMDatabase+RSExtras.h"; sourceTree = ""; }; 84419AD51B5ABD6D00C26BB2 /* FMDatabase+RSExtras.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FMDatabase+RSExtras.m"; path = "RSDatabase/FMDatabase+RSExtras.m"; sourceTree = ""; }; 84419AD81B5ABD7400C26BB2 /* NSString+RSDatabase.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSString+RSDatabase.h"; path = "RSDatabase/NSString+RSDatabase.h"; sourceTree = ""; }; @@ -150,6 +153,7 @@ 84419B041B5ABFF700C26BB2 /* FMResultSet+RSExtras.m */, 84419AD81B5ABD7400C26BB2 /* NSString+RSDatabase.h */, 84419AD91B5ABD7400C26BB2 /* NSString+RSDatabase.m */, + 840405DA1F1C158C00DF0296 /* DatabaseTable.swift */, 84DDF18A1C94FC45005E6CF5 /* FMDB */, 84F22C5A1B52E0D9000060CE /* Info.plist */, 849BF8C51C94FB8E0071D1DA /* libsqlite3.tbd */, @@ -285,7 +289,7 @@ }; 84F22C541B52E0D9000060CE = { CreatedOnToolsVersion = 7.0; - LastSwiftMigration = 0800; + LastSwiftMigration = 0900; }; 84F22C5E1B52E0D9000060CE = { CreatedOnToolsVersion = 7.0; @@ -344,6 +348,7 @@ 8400AC001E0CFC0700AA7C57 /* RSDatabaseQueue.m in Sources */, 8400AC061E0CFC0700AA7C57 /* NSString+RSDatabase.m in Sources */, 8400AC0C1E0CFC3100AA7C57 /* FMResultSet.m in Sources */, + 840405DC1F1C15EA00DF0296 /* DatabaseTable.swift in Sources */, 8400AC021E0CFC0700AA7C57 /* FMDatabase+RSExtras.m in Sources */, 8400AC081E0CFC2000AA7C57 /* FMDatabase.m in Sources */, 8400AC041E0CFC0700AA7C57 /* FMResultSet+RSExtras.m in Sources */, @@ -358,6 +363,7 @@ 84419AE71B5ABD7F00C26BB2 /* RSDatabaseQueue.m in Sources */, 84419AD71B5ABD6D00C26BB2 /* FMDatabase+RSExtras.m in Sources */, 84419ADB1B5ABD7400C26BB2 /* NSString+RSDatabase.m in Sources */, + 840405DB1F1C158C00DF0296 /* DatabaseTable.swift in Sources */, 84DDF1971C94FC45005E6CF5 /* FMDatabase.m in Sources */, 84DDF1A01C94FC45005E6CF5 /* FMResultSet.m in Sources */, 84419B061B5ABFF700C26BB2 /* FMResultSet+RSExtras.m in Sources */, @@ -531,6 +537,7 @@ 84F22C6A1B52E0D9000060CE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -543,12 +550,15 @@ PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.RSDatabase; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 3.0; }; name = Debug; }; 84F22C6B1B52E0D9000060CE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + CLANG_ENABLE_MODULES = YES; COMBINE_HIDPI_IMAGES = YES; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; @@ -561,12 +571,14 @@ PRODUCT_BUNDLE_IDENTIFIER = com.ranchero.RSDatabase; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; + SWIFT_VERSION = 3.0; }; name = Release; }; 84F22C6D1B52E0D9000060CE /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = RSDatabaseTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks"; @@ -579,6 +591,7 @@ 84F22C6E1B52E0D9000060CE /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = RSDatabaseTests/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/../Frameworks";