Make local Database and FMDB modules. Stop using RSDatabase.

This commit is contained in:
Brent Simmons 2024-03-10 16:39:38 -07:00
parent ee58096a48
commit b662ad8ad3
53 changed files with 5559 additions and 97 deletions

View File

@ -1,4 +1,4 @@
// swift-tools-version:5.9
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
@ -7,17 +7,16 @@ let package = Package(
products: [
.library(
name: "Account",
type: .dynamic,
targets: ["Account"]),
],
dependencies: [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMinor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(path: "../Articles"),
.package(path: "../ArticlesDatabase"),
.package(path: "../Secrets"),
.package(path: "../Database"),
.package(path: "../SyncDatabase")
],
targets: [
@ -25,13 +24,13 @@ let package = Package(
name: "Account",
dependencies: [
"RSCore",
"RSDatabase",
"RSParser",
"RSWeb",
"Articles",
"ArticlesDatabase",
"Secrets",
"SyncDatabase",
"Database"
]),
.testTarget(
name: "AccountTests",

View File

@ -14,7 +14,7 @@ import Foundation
import RSCore
import Articles
import RSParser
import RSDatabase
import Database
import ArticlesDatabase
import RSWeb
import os.log

View File

@ -11,7 +11,7 @@ import RSCore
import RSWeb
import Articles
import ArticlesDatabase
import RSDatabase
import Database
// Main thread only.

View File

@ -8,7 +8,7 @@
import Articles
import RSCore
import RSDatabase
import Database
import RSParser
import RSWeb
import SyncDatabase

View File

@ -9,7 +9,7 @@
import Articles
import RSCore
import RSDatabase
import Database
import RSParser
import RSWeb
import SyncDatabase

View File

@ -8,7 +8,7 @@
import Articles
import RSCore
import RSDatabase
import Database
import RSParser
import RSWeb
import SyncDatabase

View File

@ -1,9 +1,9 @@
// swift-tools-version:5.3
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
name: "Articles",
platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_15), .iOS(SupportedPlatform.IOSVersion.v13)],
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "Articles",

View File

@ -1,17 +1,17 @@
// swift-tools-version:5.3
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMinor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")),
]
#if swift(>=5.6)
dependencies.append(contentsOf: [
.package(path: "../Articles"),
.package(path: "../Articles"),
.package(path: "../Database"),
])
#else
dependencies.append(contentsOf: [
@ -21,11 +21,10 @@ dependencies.append(contentsOf: [
let package = Package(
name: "ArticlesDatabase",
platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_15), .iOS(SupportedPlatform.IOSVersion.v13)],
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "ArticlesDatabase",
type: .dynamic,
targets: ["ArticlesDatabase"]),
],
dependencies: dependencies,
@ -34,7 +33,7 @@ let package = Package(
name: "ArticlesDatabase",
dependencies: [
"RSCore",
"RSDatabase",
"Database",
"RSParser",
"Articles",
]),

View File

@ -8,7 +8,7 @@
import Foundation
import RSCore
import RSDatabase
import Database
import RSParser
import Articles

View File

@ -8,8 +8,7 @@
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Database
import RSParser
import Articles

View File

@ -7,8 +7,7 @@
//
import Foundation
import RSDatabase
import RSDatabaseObjC
import Database
import Articles
// article->authors is a many-to-many relationship.

View File

@ -7,8 +7,7 @@
//
import Foundation
import RSDatabase
import RSDatabaseObjC
import Database
import Articles
import RSParser

View File

@ -7,8 +7,7 @@
//
import Foundation
import RSDatabase
import RSDatabaseObjC
import Database
import Articles
extension ArticleStatus {

View File

@ -8,8 +8,7 @@
import Foundation
import Articles
import RSDatabase
import RSDatabaseObjC
import Database
import RSParser
// MARK: - DatabaseObject

View File

@ -7,7 +7,7 @@
//
import Foundation
import RSDatabase
import Database
import Articles
extension Array where Element == DatabaseObject {

View File

@ -7,7 +7,7 @@
//
import Foundation
import RSDatabase
import Database
import Articles
extension RelatedObjectsMap {

View File

@ -8,8 +8,7 @@
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Database
public final class FetchAllUnreadCountsOperation: MainThreadOperation {

View File

@ -8,8 +8,7 @@
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Database
/// Fetch the unread count for a single feed.
public final class FetchFeedUnreadCountOperation: MainThreadOperation {

View File

@ -8,8 +8,7 @@
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Database
/// Fetch the unread counts for a number of feeds.
public final class FetchUnreadCountsForFeedsOperation: MainThreadOperation {

View File

@ -8,8 +8,7 @@
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Database
import Articles
import RSParser

View File

@ -8,8 +8,7 @@
import Foundation
import RSCore
import RSDatabase
import RSDatabaseObjC
import Database
import Articles
// Article->ArticleStatus is a to-one relationship.

8
Database/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

31
Database/Package.swift Normal file
View File

@ -0,0 +1,31 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Database",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "Database",
targets: ["Database"]
)
],
dependencies: [
.package(path: "../FMDB"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "Database",
dependencies: [
"FMDB"
]
),
.testTarget(
name: "DatabaseTests",
dependencies: ["Database"]),
]
)

View File

@ -0,0 +1,54 @@
//
// Database.swift
// RSDatabase
//
// Created by Brent Simmons on 12/15/19.
// Copyright © 2019 Brent Simmons. All rights reserved.
//
import Foundation
import FMDB
public enum DatabaseError: Error, Sendable {
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 = @Sendable (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 = @Sendable (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 FMDB
/// 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 FMDB
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) {
dictionaries.forEach { (oneDictionary) in
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 FMDB
// 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 FMDB
// 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]
}
}

View File

@ -0,0 +1,12 @@
import XCTest
@testable import Database
final class DatabaseTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest
// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}

8
FMDB/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

21
FMDB/Package.swift Normal file
View File

@ -0,0 +1,21 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "FMDB",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "FMDB",
targets: ["FMDB"]),
],
targets: [
.target(
name: "FMDB"),
.testTarget(
name: "FMDBTests",
dependencies: ["FMDB"]),
]
)

View File

@ -0,0 +1,83 @@
//
// FMDatabase+QSKit.h
// RSDatabase
//
// Created by Brent Simmons on 3/3/14.
// Copyright (c) 2014 Ranchero Software, LLC. All rights reserved.
//
#import "FMDatabase.h"
@import Foundation;
typedef NS_ENUM(NSInteger, RSDatabaseInsertType) {
RSDatabaseInsertNormal,
RSDatabaseInsertOrReplace,
RSDatabaseInsertOrIgnore
};
NS_ASSUME_NONNULL_BEGIN
@interface FMDatabase (RSExtras)
// Keys and table names are assumed to be trusted. Values are not.
// delete from tableName where key in (?, ?, ?)
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName;
// delete from tableName where key=?
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// select * from tableName where key in (?, ?, ?)
- (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName;
// select * from tableName where key = ?
- (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// select * from tableName where key = ? limit 1
- (FMResultSet * _Nullable)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// select * from tableName
- (FMResultSet * _Nullable)rs_selectAllRows:(NSString *)tableName;
// select key from tableName;
- (FMResultSet * _Nullable)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName;
// select 1 from tableName where key = value limit 1;
- (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName;
// select 1 from tableName limit 1;
- (BOOL)rs_tableIsEmpty:(NSString *)tableName;
// update tableName set key1=?, key2=? where key = value
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
// update tableName set key1=?, key2=? where key in (?, ?, ?)
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName;
// update tableName set valueKey=? where where key in (?, ?, ?)
- (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName;
// insert (or replace, or ignore) into tablename (key1, key2) values (val1, val2)
- (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,180 @@
//
// FMDatabase+QSKit.m
// RSDatabase
//
// Created by Brent Simmons on 3/3/14.
// Copyright (c) 2014 Ranchero Software, LLC. All rights reserved.
//
#import "FMDatabase+RSExtras.h"
#import "NSString+RSDatabase.h"
#define LOG_SQL 0
static void logSQL(NSString *sql) {
#if LOG_SQL
NSLog(@"sql: %@", sql);
#endif
}
@implementation FMDatabase (RSExtras)
#pragma mark - Deleting
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName {
if ([values count] < 1) {
return YES;
}
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ in %@", tableName, key, placeholders];
logSQL(sql);
return [self executeUpdate:sql withArgumentsInArray:values];
}
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ = ?", tableName, key];
logSQL(sql);
return [self executeUpdate:sql, value];
}
#pragma mark - Selecting
- (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName {
NSMutableString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ in ", tableName, key];
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
[sql appendString:placeholders];
logSQL(sql);
return [self executeQuery:sql withArgumentsInArray:values];
}
- (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ?", tableName, key];
logSQL(sql);
return [self executeQuery:sql, value];
}
- (FMResultSet *)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ? limit 1", tableName, key];
logSQL(sql);
return [self executeQuery:sql, value];
}
- (FMResultSet *)rs_selectAllRows:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select * from %@", tableName];
logSQL(sql);
return [self executeQuery:sql];
}
- (FMResultSet *)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select %@ from %@", key, tableName];
logSQL(sql);
return [self executeQuery:sql];
}
- (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select 1 from %@ where %@ = ? limit 1;", tableName, key];
logSQL(sql);
FMResultSet *rs = [self executeQuery:sql, value];
return [rs next];
}
- (BOOL)rs_tableIsEmpty:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"select 1 from %@ limit 1;", tableName];
logSQL(sql);
FMResultSet *rs = [self executeQuery:sql];
BOOL isEmpty = YES;
while ([rs next]) {
isEmpty = NO;
}
return isEmpty;
}
#pragma mark - Updating
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
return [self rs_updateRowsWithDictionary:d whereKey:key inValues:@[value] tableName:tableName];
}
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName {
NSMutableArray *keys = [NSMutableArray new];
NSMutableArray *values = [NSMutableArray new];
[d enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[keys addObject:key];
[values addObject:obj];
}];
NSString *keyPlaceholders = [NSString rs_SQLKeyPlaceholderPairsWithKeys:keys];
NSString *keyValuesPlaceholder = [NSString rs_SQLValueListWithPlaceholders:keyValues.count];
NSString *sql = [NSString stringWithFormat:@"update %@ set %@ where %@ in %@", tableName, keyPlaceholders, key, keyValuesPlaceholder];
NSMutableArray *parameters = values;
[parameters addObjectsFromArray:keyValues];
logSQL(sql);
return [self executeUpdate:sql withArgumentsInArray:parameters];
}
- (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName {
NSDictionary *d = @{valueKey: value};
return [self rs_updateRowsWithDictionary:d whereKey:key inValues:keyValues tableName:tableName];
}
#pragma mark - Saving
- (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName {
NSArray *keys = d.allKeys;
NSArray *values = [d objectsForKeys:keys notFoundMarker:[NSNull null]];
NSString *sqlKeysList = [NSString rs_SQLKeysListWithArray:keys];
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
NSString *sqlBeginning = @"insert into ";
if (insertType == RSDatabaseInsertOrReplace) {
sqlBeginning = @"insert or replace into ";
}
else if (insertType == RSDatabaseInsertOrIgnore) {
sqlBeginning = @"insert or ignore into ";
}
NSString *sql = [NSString stringWithFormat:@"%@ %@ %@ values %@", sqlBeginning, tableName, sqlKeysList, placeholders];
logSQL(sql);
return [self executeUpdate:sql withArgumentsInArray:values];
}
@end

1084
FMDB/Sources/FMDB/FMDatabase.h Executable file

File diff suppressed because it is too large Load Diff

1427
FMDB/Sources/FMDB/FMDatabase.m Executable file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,281 @@
//
// FMDatabaseAdditions.h
// fmdb
//
// Created by August Mueller on 10/30/05.
// Copyright 2005 Flying Meat Inc.. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FMDatabase.h"
/** Category of additions for `<FMDatabase>` class.
### See also
- `<FMDatabase>`
*/
@interface FMDatabase (FMDatabaseAdditions)
///----------------------------------------
/// @name Return results of SQL to variable
///----------------------------------------
/** Return `int` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `int` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (int)intForQuery:(NSString*)query, ...;
/** Return `long` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `long` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (long)longForQuery:(NSString*)query, ...;
/** Return `BOOL` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `BOOL` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (BOOL)boolForQuery:(NSString*)query, ...;
/** Return `double` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `double` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (double)doubleForQuery:(NSString*)query, ...;
/** Return `NSString` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `NSString` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (NSString*)stringForQuery:(NSString*)query, ...;
/** Return `NSData` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `NSData` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (NSData*)dataForQuery:(NSString*)query, ...;
/** Return `NSDate` value for query
@param query The SQL query to be performed.
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
@return `NSDate` value.
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
*/
- (NSDate*)dateForQuery:(NSString*)query, ...;
// Notice that there's no dataNoCopyForQuery:.
// That would be a bad idea, because we close out the result set, and then what
// happens to the data that we just didn't copy? Who knows, not I.
///--------------------------------
/// @name Schema related operations
///--------------------------------
/** Does table exist in database?
@param tableName The name of the table being looked for.
@return `YES` if table found; `NO` if not found.
*/
- (BOOL)tableExists:(NSString*)tableName;
/** The schema of the database.
This will be the schema for the entire database. For each entity, each row of the result set will include the following fields:
- `type` - The type of entity (e.g. table, index, view, or trigger)
- `name` - The name of the object
- `tbl_name` - The name of the table to which the object references
- `rootpage` - The page number of the root b-tree page for tables and indices
- `sql` - The SQL that created the entity
@return `FMResultSet` of schema; `nil` on error.
@see [SQLite File Format](http://www.sqlite.org/fileformat.html)
*/
- (FMResultSet*)getSchema;
/** The schema of the database.
This will be the schema for a particular table as report by SQLite `PRAGMA`, for example:
PRAGMA table_info('employees')
This will report:
- `cid` - The column ID number
- `name` - The name of the column
- `type` - The data type specified for the column
- `notnull` - whether the field is defined as NOT NULL (i.e. values required)
- `dflt_value` - The default value for the column
- `pk` - Whether the field is part of the primary key of the table
@param tableName The name of the table for whom the schema will be returned.
@return `FMResultSet` of schema; `nil` on error.
@see [table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
*/
- (FMResultSet*)getTableSchema:(NSString*)tableName;
/** Test to see if particular column exists for particular table in database
@param columnName The name of the column.
@param tableName The name of the table.
@return `YES` if column exists in table in question; `NO` otherwise.
*/
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName;
/** Test to see if particular column exists for particular table in database
@param columnName The name of the column.
@param tableName The name of the table.
@return `YES` if column exists in table in question; `NO` otherwise.
@see columnExists:inTableWithName:
@warning Deprecated - use `<columnExists:inTableWithName:>` instead.
*/
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated));
/** Validate SQL statement
This validates SQL statement by performing `sqlite3_prepare_v2`, but not returning the results, but instead immediately calling `sqlite3_finalize`.
@param sql The SQL statement being validated.
@param error This is a pointer to a `NSError` object that will receive the autoreleased `NSError` object if there was any error. If this is `nil`, no `NSError` result will be returned.
@return `YES` if validation succeeded without incident; `NO` otherwise.
*/
- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error;
#if SQLITE_VERSION_NUMBER >= 3007017
///-----------------------------------
/// @name Application identifier tasks
///-----------------------------------
/** Retrieve application ID
@return The `uint32_t` numeric value of the application ID.
@see setApplicationID:
*/
- (uint32_t)applicationID;
/** Set the application ID
@param appID The `uint32_t` numeric value of the application ID.
@see applicationID
*/
- (void)setApplicationID:(uint32_t)appID;
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
/** Retrieve application ID string
@return The `NSString` value of the application ID.
@see setApplicationIDString:
*/
- (NSString*)applicationIDString;
/** Set the application ID string
@param string The `NSString` value of the application ID.
@see applicationIDString
*/
- (void)setApplicationIDString:(NSString*)string;
#endif
#endif
///-----------------------------------
/// @name user version identifier tasks
///-----------------------------------
/** Retrieve user version
@return The `uint32_t` numeric value of the user version.
@see setUserVersion:
*/
- (uint32_t)userVersion;
/** Set the user-version
@param version The `uint32_t` numeric value of the user version.
@see userVersion
*/
- (void)setUserVersion:(uint32_t)version;
@end

View File

@ -0,0 +1,225 @@
//
// FMDatabaseAdditions.m
// fmdb
//
// Created by August Mueller on 10/30/05.
// Copyright 2005 Flying Meat Inc.. All rights reserved.
//
#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
#import "TargetConditionals.h"
#import "sqlite3.h"
@interface FMDatabase (PrivateStuff)
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args;
@end
@implementation FMDatabase (FMDatabaseAdditions)
#define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \
va_list args; \
va_start(args, query); \
FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args]; \
va_end(args); \
if (![resultSet next]) { return (type)0; } \
type ret = [resultSet sel:0]; \
[resultSet close]; \
[resultSet setParentDB:nil]; \
return ret;
- (NSString*)stringForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex);
}
- (int)intForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex);
}
- (long)longForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex);
}
- (BOOL)boolForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex);
}
- (double)doubleForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex);
}
- (NSData*)dataForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex);
}
- (NSDate*)dateForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex);
}
- (BOOL)tableExists:(NSString*)tableName {
tableName = [tableName lowercaseString];
FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
//if at least one next exists, table exists
BOOL returnBool = [rs next];
//close and free object
[rs close];
return returnBool;
}
/*
get table with list of tables: result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
check if table exist in database (patch from OZLB)
*/
- (FMResultSet*)getSchema {
//result colums: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
FMResultSet *rs = [self executeQuery:@"SELECT type, name, tbl_name, rootpage, sql FROM (SELECT * FROM sqlite_master UNION ALL SELECT * FROM sqlite_temp_master) WHERE type != 'meta' AND name NOT LIKE 'sqlite_%' ORDER BY tbl_name, type DESC, name"];
return rs;
}
/*
get table schema: result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
*/
- (FMResultSet*)getTableSchema:(NSString*)tableName {
//result colums: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
FMResultSet *rs = [self executeQuery:[NSString stringWithFormat: @"pragma table_info('%@')", tableName]];
return rs;
}
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName {
BOOL returnBool = NO;
tableName = [tableName lowercaseString];
columnName = [columnName lowercaseString];
FMResultSet *rs = [self getTableSchema:tableName];
//check if column is present in table schema
while ([rs next]) {
if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString:columnName]) {
returnBool = YES;
break;
}
}
//If this is not done FMDatabase instance stays out of pool
[rs close];
return returnBool;
}
#if SQLITE_VERSION_NUMBER >= 3007017
- (uint32_t)applicationID {
uint32_t r = 0;
FMResultSet *rs = [self executeQuery:@"pragma application_id"];
if ([rs next]) {
r = (uint32_t)[rs longLongIntForColumnIndex:0];
}
[rs close];
return r;
}
- (void)setApplicationID:(uint32_t)appID {
NSString *query = [NSString stringWithFormat:@"pragma application_id=%d", appID];
FMResultSet *rs = [self executeQuery:query];
[rs next];
[rs close];
}
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
- (NSString*)applicationIDString {
NSString *s = NSFileTypeForHFSTypeCode([self applicationID]);
assert([s length] == 6);
s = [s substringWithRange:NSMakeRange(1, 4)];
return s;
}
- (void)setApplicationIDString:(NSString*)s {
if ([s length] != 4) {
NSLog(@"setApplicationIDString: string passed is not exactly 4 chars long. (was %ld)", [s length]);
}
[self setApplicationID:NSHFSTypeCodeFromFileType([NSString stringWithFormat:@"'%@'", s])];
}
#endif
#endif
- (uint32_t)userVersion {
uint32_t r = 0;
FMResultSet *rs = [self executeQuery:@"pragma user_version"];
if ([rs next]) {
r = (uint32_t)[rs longLongIntForColumnIndex:0];
}
[rs close];
return r;
}
- (void)setUserVersion:(uint32_t)version {
NSString *query = [NSString stringWithFormat:@"pragma user_version = %d", version];
FMResultSet *rs = [self executeQuery:query];
[rs next];
[rs close];
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated)) {
return [self columnExists:columnName inTableWithName:tableName];
}
#pragma clang diagnostic pop
- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error {
sqlite3_stmt *pStmt = NULL;
BOOL validationSucceeded = YES;
int rc = sqlite3_prepare_v2([self sqliteHandle], [sql UTF8String], -1, &pStmt, 0);
if (rc != SQLITE_OK) {
validationSucceeded = NO;
if (error) {
*error = [NSError errorWithDomain:NSCocoaErrorDomain
code:[self lastErrorCode]
userInfo:[NSDictionary dictionaryWithObject:[self lastErrorMessage]
forKey:NSLocalizedDescriptionKey]];
}
}
sqlite3_finalize(pStmt);
return validationSucceeded;
}
@end

View File

@ -0,0 +1,23 @@
//
// FMResultSet+RSExtras.h
// RSDatabase
//
// Created by Brent Simmons on 2/19/13.
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
//
#import "FMResultSet.h"
NS_ASSUME_NONNULL_BEGIN
@interface FMResultSet (RSExtras)
- (NSArray *)rs_arrayForSingleColumnResultSet; // Doesn't handle dates.
- (NSSet *)rs_setForSingleColumnResultSet;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,51 @@
//
// FMResultSet+RSExtras.m
// RSDatabase
//
// Created by Brent Simmons on 2/19/13.
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
//
#import "FMResultSet+RSExtras.h"
@implementation FMResultSet (RSExtras)
- (id)valueForKey:(NSString *)key {
if ([key containsString:@"Date"] || [key containsString:@"date"]) {
return [self dateForColumn:key];
}
return [self objectForColumnName:key];
}
- (NSArray *)rs_arrayForSingleColumnResultSet {
NSMutableArray *results = [NSMutableArray new];
while ([self next]) {
id oneObject = [self objectForColumnIndex:0];
[results addObject:oneObject];
}
return [results copy];
}
- (NSSet *)rs_setForSingleColumnResultSet {
NSMutableSet *results = [NSMutableSet new];
while ([self next]) {
id oneObject = [self objectForColumnIndex:0];
[results addObject:oneObject];
}
return [results copy];
}
@end

469
FMDB/Sources/FMDB/FMResultSet.h Executable file
View File

@ -0,0 +1,469 @@
#import <Foundation/Foundation.h>
//#import "sqlite3.h"
#ifndef __has_feature // Optional.
#define __has_feature(x) 0 // Compatibility with non-clang compilers.
#endif
#ifndef NS_RETURNS_NOT_RETAINED
#if __has_feature(attribute_ns_returns_not_retained)
#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
#else
#define NS_RETURNS_NOT_RETAINED
#endif
#endif
@class FMDatabase;
@class FMStatement;
/** Represents the results of executing a query on an `<FMDatabase>`.
### See also
- `<FMDatabase>`
*/
@interface FMResultSet : NSObject {
FMDatabase *_parentDB;
FMStatement *_statement;
NSString *_query;
NSMutableDictionary *_columnNameToIndexMap;
}
///-----------------
/// @name Properties
///-----------------
/** Executed query */
@property (atomic, retain) NSString *query;
/** `NSMutableDictionary` mapping column names to numeric index */
@property (readonly) NSMutableDictionary *columnNameToIndexMap;
/** `FMStatement` used by result set. */
@property (atomic, retain) FMStatement *statement;
///------------------------------------
/// @name Creating and closing database
///------------------------------------
/** Create result set from `<FMStatement>`
@param statement A `<FMStatement>` to be performed
@param aDB A `<FMDatabase>` to be used
@return A `FMResultSet` on success; `nil` on failure
*/
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB;
/** Close result set */
- (void)close;
- (void)setParentDB:(FMDatabase *)newDb;
///---------------------------------------
/// @name Iterating through the result set
///---------------------------------------
/** Retrieve next row for result set.
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
@return `YES` if row successfully retrieved; `NO` if end of result set reached
@see hasAnotherRow
*/
- (BOOL)next;
/** Retrieve next row for result set.
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
@param outErr A 'NSError' object to receive any error object (if any).
@return 'YES' if row successfully retrieved; 'NO' if end of result set reached
@see hasAnotherRow
*/
- (BOOL)nextWithError:(NSError **)outErr;
/** Did the last call to `<next>` succeed in retrieving another row?
@return `YES` if the last call to `<next>` succeeded in retrieving another record; `NO` if not.
@see next
@warning The `hasAnotherRow` method must follow a call to `<next>`. If the previous database interaction was something other than a call to `next`, then this method may return `NO`, whether there is another row of data or not.
*/
- (BOOL)hasAnotherRow;
///---------------------------------------------
/// @name Retrieving information from result set
///---------------------------------------------
/** How many columns in result set
@return Integer value of the number of columns.
*/
- (int)columnCount;
/** Column index for column name
@param columnName `NSString` value of the name of the column.
@return Zero-based index for column.
*/
- (int)columnIndexForName:(NSString*)columnName;
/** Column name for column index
@param columnIdx Zero-based index for column.
@return columnName `NSString` value of the name of the column.
*/
- (NSString*)columnNameForIndex:(int)columnIdx;
/** Result set integer value for column.
@param columnName `NSString` value of the name of the column.
@return `int` value of the result set's column.
*/
- (int)intForColumn:(NSString*)columnName;
/** Result set integer value for column.
@param columnIdx Zero-based index for column.
@return `int` value of the result set's column.
*/
- (int)intForColumnIndex:(int)columnIdx;
/** Result set `long` value for column.
@param columnName `NSString` value of the name of the column.
@return `long` value of the result set's column.
*/
- (long)longForColumn:(NSString*)columnName;
/** Result set long value for column.
@param columnIdx Zero-based index for column.
@return `long` value of the result set's column.
*/
- (long)longForColumnIndex:(int)columnIdx;
/** Result set `long long int` value for column.
@param columnName `NSString` value of the name of the column.
@return `long long int` value of the result set's column.
*/
- (long long int)longLongIntForColumn:(NSString*)columnName;
/** Result set `long long int` value for column.
@param columnIdx Zero-based index for column.
@return `long long int` value of the result set's column.
*/
- (long long int)longLongIntForColumnIndex:(int)columnIdx;
/** Result set `unsigned long long int` value for column.
@param columnName `NSString` value of the name of the column.
@return `unsigned long long int` value of the result set's column.
*/
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName;
/** Result set `unsigned long long int` value for column.
@param columnIdx Zero-based index for column.
@return `unsigned long long int` value of the result set's column.
*/
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx;
/** Result set `BOOL` value for column.
@param columnName `NSString` value of the name of the column.
@return `BOOL` value of the result set's column.
*/
- (BOOL)boolForColumn:(NSString*)columnName;
/** Result set `BOOL` value for column.
@param columnIdx Zero-based index for column.
@return `BOOL` value of the result set's column.
*/
- (BOOL)boolForColumnIndex:(int)columnIdx;
/** Result set `double` value for column.
@param columnName `NSString` value of the name of the column.
@return `double` value of the result set's column.
*/
- (double)doubleForColumn:(NSString*)columnName;
/** Result set `double` value for column.
@param columnIdx Zero-based index for column.
@return `double` value of the result set's column.
*/
- (double)doubleForColumnIndex:(int)columnIdx;
/** Result set `NSString` value for column.
@param columnName `NSString` value of the name of the column.
@return `NSString` value of the result set's column.
*/
- (NSString*)stringForColumn:(NSString*)columnName;
/** Result set `NSString` value for column.
@param columnIdx Zero-based index for column.
@return `NSString` value of the result set's column.
*/
- (NSString*)stringForColumnIndex:(int)columnIdx;
/** Result set `NSDate` value for column.
@param columnName `NSString` value of the name of the column.
@return `NSDate` value of the result set's column.
*/
- (NSDate*)dateForColumn:(NSString*)columnName;
/** Result set `NSDate` value for column.
@param columnIdx Zero-based index for column.
@return `NSDate` value of the result set's column.
*/
- (NSDate*)dateForColumnIndex:(int)columnIdx;
/** Result set `NSData` value for column.
This is useful when storing binary data in table (such as image or the like).
@param columnName `NSString` value of the name of the column.
@return `NSData` value of the result set's column.
*/
- (NSData*)dataForColumn:(NSString*)columnName;
/** Result set `NSData` value for column.
@param columnIdx Zero-based index for column.
@return `NSData` value of the result set's column.
*/
- (NSData*)dataForColumnIndex:(int)columnIdx;
/** Result set `(const unsigned char *)` value for column.
@param columnName `NSString` value of the name of the column.
@return `(const unsigned char *)` value of the result set's column.
*/
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName;
/** Result set `(const unsigned char *)` value for column.
@param columnIdx Zero-based index for column.
@return `(const unsigned char *)` value of the result set's column.
*/
- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx;
/** Result set object for column.
@param columnName `NSString` value of the name of the column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
@see objectForKeyedSubscript:
*/
- (id)objectForColumnName:(NSString*)columnName;
/** Result set object for column.
@param columnIdx Zero-based index for column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
@see objectAtIndexedSubscript:
*/
- (id)objectForColumnIndex:(int)columnIdx;
/** Result set object for column.
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
id result = rs[@"employee_name"];
This simplified syntax is equivalent to calling:
id result = [rs objectForKeyedSubscript:@"employee_name"];
which is, it turns out, equivalent to calling:
id result = [rs objectForColumnName:@"employee_name"];
@param columnName `NSString` value of the name of the column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
*/
- (id)objectForKeyedSubscript:(NSString *)columnName;
/** Result set object for column.
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
id result = rs[0];
This simplified syntax is equivalent to calling:
id result = [rs objectForKeyedSubscript:0];
which is, it turns out, equivalent to calling:
id result = [rs objectForColumnName:0];
@param columnIdx Zero-based index for column.
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
*/
- (id)objectAtIndexedSubscript:(int)columnIdx;
/** Result set `NSData` value for column.
@param columnName `NSString` value of the name of the column.
@return `NSData` value of the result set's column.
@warning If you are going to use this data after you iterate over the next row, or after you close the
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
If you don't, you're going to be in a world of hurt when you try and use the data.
*/
- (NSData*)dataNoCopyForColumn:(NSString*)columnName NS_RETURNS_NOT_RETAINED;
/** Result set `NSData` value for column.
@param columnIdx Zero-based index for column.
@return `NSData` value of the result set's column.
@warning If you are going to use this data after you iterate over the next row, or after you close the
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
If you don't, you're going to be in a world of hurt when you try and use the data.
*/
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED;
/** Is the column `NULL`?
@param columnIdx Zero-based index for column.
@return `YES` if column is `NULL`; `NO` if not `NULL`.
*/
- (BOOL)columnIndexIsNull:(int)columnIdx;
/** Is the column `NULL`?
@param columnName `NSString` value of the name of the column.
@return `YES` if column is `NULL`; `NO` if not `NULL`.
*/
- (BOOL)columnIsNull:(NSString*)columnName;
/** Returns a dictionary of the row results mapped to case sensitive keys of the column names.
@returns `NSDictionary` of the row results.
@warning The keys to the dictionary are case sensitive of the column names.
*/
- (NSDictionary*)resultDictionary;
/** Returns a dictionary of the row results
@see resultDictionary
@warning **Deprecated**: Please use `<resultDictionary>` instead. Also, beware that `<resultDictionary>` is case sensitive!
*/
- (NSDictionary*)resultDict __attribute__ ((deprecated));
///-----------------------------
/// @name Key value coding magic
///-----------------------------
/** Performs `setValue` to yield support for key value observing.
@param object The object for which the values will be set. This is the key-value-coding compliant object that you might, for example, observe.
*/
- (void)kvcMagic:(id)object;
@end

454
FMDB/Sources/FMDB/FMResultSet.m Executable file
View File

@ -0,0 +1,454 @@
#import "FMResultSet.h"
#import "FMDatabase.h"
#import "unistd.h"
#import "sqlite3.h"
@interface FMDatabase ()
- (void)resultSetDidClose:(FMResultSet *)resultSet;
@end
@interface FMResultSet ()
@property (nonatomic, readonly) NSDictionary *columnNameToIndexMapNonLowercased;
@end
@implementation FMResultSet
@synthesize query=_query;
@synthesize statement=_statement;
@synthesize columnNameToIndexMapNonLowercased = _columnNameToIndexMapNonLowercased;
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB {
FMResultSet *rs = [[FMResultSet alloc] init];
[rs setStatement:statement];
[rs setParentDB:aDB];
NSParameterAssert(![statement inUse]);
[statement setInUse:YES]; // weak reference
return FMDBReturnAutoreleased(rs);
}
- (void)dealloc {
[self close];
FMDBRelease(_query);
_query = nil;
FMDBRelease(_columnNameToIndexMap);
_columnNameToIndexMap = nil;
#if ! __has_feature(objc_arc)
[super dealloc];
#endif
}
- (void)close {
[_statement reset];
FMDBRelease(_statement);
_statement = nil;
// we don't need this anymore... (i think)
//[_parentDB setInUse:NO];
[_parentDB resultSetDidClose:self];
_parentDB = nil;
}
- (int)columnCount {
return sqlite3_column_count([_statement statement]);
}
- (NSMutableDictionary *)columnNameToIndexMap {
if (!_columnNameToIndexMap) {
NSDictionary *nonLowercasedMap = self.columnNameToIndexMapNonLowercased;
NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:nonLowercasedMap.count];
for (NSString *key in nonLowercasedMap.allKeys) {
[d setObject:nonLowercasedMap[key] forKey:[self _lowercaseString:key]];
}
_columnNameToIndexMap = d;
}
return _columnNameToIndexMap;
}
- (NSDictionary *)columnNameToIndexMapNonLowercased {
if (!_columnNameToIndexMapNonLowercased) {
int columnCount = sqlite3_column_count([_statement statement]);
NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
[d setObject:[NSNumber numberWithInt:columnIdx]
forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
}
_columnNameToIndexMapNonLowercased = d;
}
return _columnNameToIndexMapNonLowercased;
}
- (void)kvcMagic:(id)object {
int columnCount = sqlite3_column_count([_statement statement]);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
// check for a null row
if (c) {
NSString *s = [NSString stringWithUTF8String:c];
[object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
}
}
}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
- (NSDictionary*)resultDict {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
NSEnumerator *columnNames = [[self columnNameToIndexMap] keyEnumerator];
NSString *columnName = nil;
while ((columnName = [columnNames nextObject])) {
id objectValue = [self objectForColumnName:columnName];
[dict setObject:objectValue forKey:columnName];
}
return FMDBReturnAutoreleased([dict copy]);
}
else {
NSLog(@"Warning: There seem to be no columns in this set.");
}
return nil;
}
#pragma clang diagnostic pop
- (NSDictionary*)resultDictionary {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
int columnCount = sqlite3_column_count([_statement statement]);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)];
id objectValue = [self objectForColumnIndex:columnIdx];
[dict setObject:objectValue forKey:columnName];
}
return dict;
}
else {
NSLog(@"Warning: There seem to be no columns in this set.");
}
return nil;
}
- (BOOL)next {
return [self nextWithError:nil];
}
- (BOOL)nextWithError:(NSError **)outErr {
int rc = sqlite3_step([_statement statement]);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
NSLog(@"Database busy");
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
// all is well, let's return.
}
else if (SQLITE_ERROR == rc) {
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
else if (SQLITE_MISUSE == rc) {
// uh oh.
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
if (_parentDB) {
*outErr = [_parentDB lastError];
}
else {
// If 'next' or 'nextWithError' is called after the result set is closed,
// we need to return the appropriate error.
NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
*outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
}
}
}
else {
// wtf?
NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
if (outErr) {
*outErr = [_parentDB lastError];
}
}
if (rc != SQLITE_ROW) {
[self close];
}
return (rc == SQLITE_ROW);
}
- (BOOL)hasAnotherRow {
return sqlite3_errcode([_parentDB sqliteHandle]) == SQLITE_ROW;
}
- (int)columnIndexForName:(NSString*)columnName {
NSNumber *n = self.columnNameToIndexMapNonLowercased[columnName];
if (!n) {
columnName = [self _lowercaseString:columnName];
n = [[self columnNameToIndexMap] objectForKey:columnName];
}
if (n) {
return [n intValue];
}
NSLog(@"Warning: I could not find the column named '%@'.", columnName);
return -1;
}
- (int)intForColumn:(NSString*)columnName {
return [self intForColumnIndex:[self columnIndexForName:columnName]];
}
- (int)intForColumnIndex:(int)columnIdx {
return sqlite3_column_int([_statement statement], columnIdx);
}
- (long)longForColumn:(NSString*)columnName {
return [self longForColumnIndex:[self columnIndexForName:columnName]];
}
- (long)longForColumnIndex:(int)columnIdx {
return (long)sqlite3_column_int64([_statement statement], columnIdx);
}
- (long long int)longLongIntForColumn:(NSString*)columnName {
return [self longLongIntForColumnIndex:[self columnIndexForName:columnName]];
}
- (long long int)longLongIntForColumnIndex:(int)columnIdx {
return sqlite3_column_int64([_statement statement], columnIdx);
}
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName {
return [self unsignedLongLongIntForColumnIndex:[self columnIndexForName:columnName]];
}
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx {
return (unsigned long long int)[self longLongIntForColumnIndex:columnIdx];
}
- (BOOL)boolForColumn:(NSString*)columnName {
return [self boolForColumnIndex:[self columnIndexForName:columnName]];
}
- (BOOL)boolForColumnIndex:(int)columnIdx {
return ([self intForColumnIndex:columnIdx] != 0);
}
- (double)doubleForColumn:(NSString*)columnName {
return [self doubleForColumnIndex:[self columnIndexForName:columnName]];
}
- (double)doubleForColumnIndex:(int)columnIdx {
return sqlite3_column_double([_statement statement], columnIdx);
}
- (NSString*)stringForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
if (!c) {
// null row.
return nil;
}
return [NSString stringWithUTF8String:c];
}
- (NSString*)stringForColumn:(NSString*)columnName {
return [self stringForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSDate*)dateForColumn:(NSString*)columnName {
return [self dateForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSDate*)dateForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
return [_parentDB hasDateFormatter] ? [_parentDB dateFromString:[self stringForColumnIndex:columnIdx]] : [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIdx]];
}
- (NSData*)dataForColumn:(NSString*)columnName {
return [self dataForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSData*)dataForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
if (dataBuffer == NULL) {
return nil;
}
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
}
- (NSData*)dataNoCopyForColumn:(NSString*)columnName {
return [self dataNoCopyForColumnIndex:[self columnIndexForName:columnName]];
}
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
NSData *data = [NSData dataWithBytesNoCopy:(void *)dataBuffer length:(NSUInteger)dataSize freeWhenDone:NO];
return data;
}
- (BOOL)columnIndexIsNull:(int)columnIdx {
return sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL;
}
- (BOOL)columnIsNull:(NSString*)columnName {
return [self columnIndexIsNull:[self columnIndexForName:columnName]];
}
- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx {
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
return sqlite3_column_text([_statement statement], columnIdx);
}
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName {
return [self UTF8StringForColumnIndex:[self columnIndexForName:columnName]];
}
- (id)objectForColumnIndex:(int)columnIdx {
int columnType = sqlite3_column_type([_statement statement], columnIdx);
id returnValue = nil;
if (columnType == SQLITE_INTEGER) {
returnValue = [NSNumber numberWithLongLong:[self longLongIntForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_FLOAT) {
returnValue = [NSNumber numberWithDouble:[self doubleForColumnIndex:columnIdx]];
}
else if (columnType == SQLITE_BLOB) {
returnValue = [self dataForColumnIndex:columnIdx];
}
else {
//default to a string for everything else
returnValue = [self stringForColumnIndex:columnIdx];
}
if (returnValue == nil) {
returnValue = [NSNull null];
}
return returnValue;
}
- (id)objectForColumnName:(NSString*)columnName {
return [self objectForColumnIndex:[self columnIndexForName:columnName]];
}
// returns autoreleased NSString containing the name of the column in the result set
- (NSString*)columnNameForIndex:(int)columnIdx {
return [NSString stringWithUTF8String: sqlite3_column_name([_statement statement], columnIdx)];
}
- (void)setParentDB:(FMDatabase *)newDb {
_parentDB = newDb;
}
- (id)objectAtIndexedSubscript:(int)columnIdx {
return [self objectForColumnIndex:columnIdx];
}
- (id)objectForKeyedSubscript:(NSString *)columnName {
return [self objectForColumnName:columnName];
}
// Brent 22 Feb. 2019: Calls to lowerCaseString show up in Instruments too much.
// Given that the amount of column names in a given app is going to be pretty small,
// we can just cache the lowercase versions
- (NSString *)_lowercaseString:(NSString *)s {
static NSLock *lock = nil;
static NSMutableDictionary *lowercaseStringCache = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
lowercaseStringCache = [[NSMutableDictionary alloc] init];
});
[lock lock];
NSString *lowercaseString = lowercaseStringCache[s];
if (lowercaseString == nil) {
lowercaseString = s.lowercaseString;
lowercaseStringCache[s] = lowercaseString;
}
[lock unlock];
return lowercaseString;
}
@end

View File

@ -0,0 +1,36 @@
//
// NSString+RSDatabase.h
// RSDatabase
//
// Created by Brent Simmons on 3/27/15.
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
//
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
@interface NSString (QSDatabase)
/*Returns @"(?, ?, ?)" -- where number of ? spots is specified by numberOfValues.
numberOfValues should be greater than 0. Triggers an NSParameterAssert if not.*/
+ (nullable NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues;
/*Returns @"(someColumn, anotherColumm, thirdColumn)" -- using passed-in keys.
It's essential that you trust keys. They must not be user input.
Triggers an NSParameterAssert if keys are empty.*/
+ (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys;
/*Returns @"key1=?, key2=?" using passed-in keys. Keys must be trusted.*/
+ (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys;
@end
NS_ASSUME_NONNULL_END

View File

@ -0,0 +1,135 @@
//
// NSString+RSDatabase.m
// RSDatabase
//
// Created by Brent Simmons on 3/27/15.
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
//
#import "NSString+RSDatabase.h"
@implementation NSString (RSDatabase)
+ (NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues {
// @"(?, ?, ?)"
NSParameterAssert(numberOfValues > 0);
if (numberOfValues < 1) {
return nil;
}
static NSMutableDictionary *cache = nil;
static dispatch_once_t onceToken;
static NSLock *lock = nil;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
cache = [NSMutableDictionary new];
});
[lock lock];
NSNumber *cacheKey = @(numberOfValues);
NSString *cachedString = cache[cacheKey];
if (cachedString) {
[lock unlock];
return cachedString;
}
NSMutableString *s = [[NSMutableString alloc] initWithString:@"("];
NSUInteger i = 0;
for (i = 0; i < numberOfValues; i++) {
[s appendString:@"?"];
BOOL isLast = (i == (numberOfValues - 1));
if (!isLast) {
[s appendString:@", "];
}
}
[s appendString:@")"];
cache[cacheKey] = s;
[lock unlock];
return s;
}
+ (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys {
NSParameterAssert(keys.count > 0);
static NSMutableDictionary *cache = nil;
static dispatch_once_t onceToken;
static NSLock *lock = nil;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
cache = [NSMutableDictionary new];
});
[lock lock];
NSArray *cacheKey = keys;
NSString *cachedString = cache[cacheKey];
if (cachedString) {
[lock unlock];
return cachedString;
}
NSString *s = [NSString stringWithFormat:@"(%@)", [keys componentsJoinedByString:@", "]];
cache[cacheKey] = s;
[lock unlock];
return s;
}
+ (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys {
// key1=?, key2=?
NSParameterAssert(keys.count > 0);
static NSMutableDictionary *cache = nil;
static dispatch_once_t onceToken;
static NSLock *lock = nil;
dispatch_once(&onceToken, ^{
lock = [[NSLock alloc] init];
cache = [NSMutableDictionary new];
});
[lock lock];
NSArray *cacheKey = keys;
NSString *cachedString = cache[cacheKey];
if (cachedString) {
[lock unlock];
return cachedString;
}
NSMutableString *s = [NSMutableString stringWithString:@""];
NSUInteger i = 0;
NSUInteger numberOfKeys = [keys count];
for (i = 0; i < numberOfKeys; i++) {
NSString *oneKey = keys[i];
[s appendString:oneKey];
[s appendString:@"=?"];
BOOL isLast = (i == (numberOfKeys - 1));
if (!isLast) {
[s appendString:@", "];
}
}
cache[cacheKey] = s;
[lock unlock];
return s;
}
@end

View File

@ -0,0 +1,11 @@
// FMDB
#import "../FMDatabase.h"
#import "../FMDatabaseAdditions.h"
#import "../FMResultSet.h"
// Categories
#import "../FMDatabase+RSExtras.h"
#import "../FMResultSet+RSExtras.h"
#import "../NSString+RSDatabase.h"

View File

@ -0,0 +1,12 @@
import XCTest
@testable import FMDB
final class FMDBTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest
// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}

View File

@ -153,8 +153,6 @@
5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5138E94924D3416D00AFF0FE /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94824D3416D00AFF0FE /* RSCore */; };
5138E94A24D3416D00AFF0FE /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94824D3416D00AFF0FE /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94B24D3417A00AFF0FE /* RSDatabase */; };
5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E94B24D3417A00AFF0FE /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5138E95224D3418100AFF0FE /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95124D3418100AFF0FE /* RSParser */; };
5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95124D3418100AFF0FE /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
5138E95824D3419000AFF0FE /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95724D3419000AFF0FE /* RSWeb */; };
@ -247,8 +245,6 @@
51A66685238075AE00CB272D /* AddFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddFeedDefaultContainer.swift */; };
51A737AE24DB19730015FA66 /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737AD24DB19730015FA66 /* RSCore */; };
51A737AF24DB19730015FA66 /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737AD24DB19730015FA66 /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51A737BF24DB197F0015FA66 /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737BE24DB197F0015FA66 /* RSDatabase */; };
51A737C024DB197F0015FA66 /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737BE24DB197F0015FA66 /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51A737C524DB19B50015FA66 /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C424DB19B50015FA66 /* RSWeb */; };
51A737C624DB19B50015FA66 /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C424DB19B50015FA66 /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
51A737C824DB19CC0015FA66 /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C724DB19CC0015FA66 /* RSParser */; };
@ -420,8 +416,6 @@
653813252680E1D6007A082C /* ArticlesDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 653813232680E1D6007A082C /* ArticlesDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
653813262680E1E4007A082C /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DAEC2425F6940091EB5B /* CloudKit.framework */; };
653813282680E1EC007A082C /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 653813272680E1EC007A082C /* CrashReporter */; };
6538132D2680E205007A082C /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 6538132C2680E205007A082C /* RSDatabase */; };
6538132E2680E205007A082C /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 6538132C2680E205007A082C /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
653813302680E20C007A082C /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 6538132F2680E20C007A082C /* RSParser */; };
653813312680E20C007A082C /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 6538132F2680E20C007A082C /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; };
653813332680E220007A082C /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 653813322680E220007A082C /* RSTree */; };
@ -963,7 +957,6 @@
513F32722593EE6F0003048F /* Articles in Embed Frameworks */,
513F32812593EF180003048F /* Account in Embed Frameworks */,
5138E93B24D33E5600AFF0FE /* RSTree in Embed Frameworks */,
5138E94D24D3417A00AFF0FE /* RSDatabase in Embed Frameworks */,
513F32752593EE6F0003048F /* ArticlesDatabase in Embed Frameworks */,
);
name = "Embed Frameworks";
@ -1000,7 +993,6 @@
files = (
653813372680E224007A082C /* RSWeb in Embed Frameworks */,
653813312680E20C007A082C /* RSParser in Embed Frameworks */,
6538132E2680E205007A082C /* RSDatabase in Embed Frameworks */,
6538133A2680E22B007A082C /* Secrets in Embed Frameworks */,
653813252680E1D6007A082C /* ArticlesDatabase in Embed Frameworks */,
653813342680E220007A082C /* RSTree in Embed Frameworks */,
@ -1046,7 +1038,6 @@
513277442590FBB60064F1E7 /* Account in Embed Frameworks */,
5132775F2590FC640064F1E7 /* Articles in Embed Frameworks */,
51A737C624DB19B50015FA66 /* RSWeb in Embed Frameworks */,
51A737C024DB197F0015FA66 /* RSDatabase in Embed Frameworks */,
513277662590FC780064F1E7 /* Secrets in Embed Frameworks */,
513277652590FC640064F1E7 /* SyncDatabase in Embed Frameworks */,
513277622590FC640064F1E7 /* ArticlesDatabase in Embed Frameworks */,
@ -1337,6 +1328,8 @@
840D617E2029031C009BC708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetNewsWire_iOSTests.swift; sourceTree = "<group>"; };
840D61972029031D009BC708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
841550F42B9E3F8000D4B345 /* Database */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Database; sourceTree = "<group>"; };
841550F52B9E4D6800D4B345 /* FMDB */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FMDB; sourceTree = "<group>"; };
84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkCommandValidationStatus.swift; sourceTree = "<group>"; };
841ABA4D20145E7300980E11 /* NothingInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NothingInspectorViewController.swift; sourceTree = "<group>"; };
841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderInspectorViewController.swift; sourceTree = "<group>"; };
@ -1596,7 +1589,6 @@
5102AE6C24D17F7C0050839C /* RSCoreResources in Frameworks */,
5102AE6924D17F7C0050839C /* RSCore in Frameworks */,
653813332680E220007A082C /* RSTree in Frameworks */,
6538132D2680E205007A082C /* RSDatabase in Frameworks */,
653813262680E1E4007A082C /* CloudKit.framework in Frameworks */,
653813242680E1D6007A082C /* ArticlesDatabase in Frameworks */,
653813212680E1D0007A082C /* Articles in Frameworks */,
@ -1619,7 +1611,6 @@
179D280B26F6F93D003B2E0A /* Zip in Frameworks */,
516B695F24D2F33B00B5702F /* Account in Frameworks */,
5138E95224D3418100AFF0FE /* RSParser in Frameworks */,
5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */,
51C452B42265141B00C03939 /* WebKit.framework in Frameworks */,
513F32712593EE6F0003048F /* Articles in Frameworks */,
513F32772593EE6F0003048F /* Secrets in Frameworks */,
@ -1648,7 +1639,6 @@
514C16E124D2EF38009A3AFA /* RSCoreResources in Frameworks */,
514C16CE24D2E63F009A3AFA /* Account in Frameworks */,
519CA8E525841DB700EB079A /* CrashReporter in Frameworks */,
51A737BF24DB197F0015FA66 /* RSDatabase in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2382,6 +2372,8 @@
51CD32C324D2CD57009ABAEF /* ArticlesDatabase */,
51CD32C724D2E06C009ABAEF /* Secrets */,
51CD32A824D2CB25009ABAEF /* SyncDatabase */,
841550F42B9E3F8000D4B345 /* Database */,
841550F52B9E4D6800D4B345 /* FMDB */,
);
sourceTree = "<group>";
usesTabs = 1;
@ -2927,7 +2919,6 @@
653813202680E1D0007A082C /* Articles */,
653813232680E1D6007A082C /* ArticlesDatabase */,
653813272680E1EC007A082C /* CrashReporter */,
6538132C2680E205007A082C /* RSDatabase */,
6538132F2680E20C007A082C /* RSParser */,
653813322680E220007A082C /* RSTree */,
653813352680E224007A082C /* RSWeb */,
@ -2977,7 +2968,6 @@
516B695E24D2F33B00B5702F /* Account */,
5138E93924D33E5600AFF0FE /* RSTree */,
5138E94824D3416D00AFF0FE /* RSCore */,
5138E94B24D3417A00AFF0FE /* RSDatabase */,
5138E95124D3418100AFF0FE /* RSParser */,
5138E95724D3419000AFF0FE /* RSWeb */,
513F32702593EE6F0003048F /* Articles */,
@ -3018,7 +3008,6 @@
514C16E024D2EF38009A3AFA /* RSCoreResources */,
51C4CFF524D37DD500AF9874 /* Secrets */,
51A737AD24DB19730015FA66 /* RSCore */,
51A737BE24DB197F0015FA66 /* RSDatabase */,
51A737C424DB19B50015FA66 /* RSWeb */,
51A737C724DB19CC0015FA66 /* RSParser */,
17192AD92567B3D500AAEACA /* RSSparkle */,
@ -3144,7 +3133,6 @@
5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */,
510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */,
51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */,
51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */,
51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */,
17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */,
519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */,
@ -4799,14 +4787,6 @@
minimumVersion = 1.8.1;
};
};
51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSDatabase.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 1.0.0;
};
};
51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/Ranchero-Software/RSParser.git";
@ -4896,11 +4876,6 @@
package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */;
productName = RSCore;
};
5138E94B24D3417A00AFF0FE /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
productName = RSDatabase;
};
5138E95124D3418100AFF0FE /* RSParser */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;
@ -4965,11 +4940,6 @@
package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */;
productName = RSCore;
};
51A737BE24DB197F0015FA66 /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
productName = RSDatabase;
};
51A737C424DB19B50015FA66 /* RSWeb */ = {
isa = XCSwiftPackageProductDependency;
package = 51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */;
@ -5019,11 +4989,6 @@
package = 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */;
productName = CrashReporter;
};
6538132C2680E205007A082C /* RSDatabase */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */;
productName = RSDatabase;
};
6538132F2680E20C007A082C /* RSParser */ = {
isa = XCSwiftPackageProductDependency;
package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */;

View File

@ -19,15 +19,6 @@
"version": "1.0.7"
}
},
{
"package": "RSDatabase",
"repositoryURL": "https://github.com/Ranchero-Software/RSDatabase.git",
"state": {
"branch": null,
"revision": "a6c5f1622320f745cc9a0a910d1bed1e2eaf15e3",
"version": "1.0.0"
}
},
{
"package": "RSParser",
"repositoryURL": "https://github.com/Ranchero-Software/RSParser.git",

View File

@ -1,9 +1,9 @@
// swift-tools-version:5.9
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
name: "Secrets",
platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_15), .iOS(SupportedPlatform.IOSVersion.v13)],
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "Secrets",

View File

@ -1,14 +1,14 @@
// swift-tools-version:5.9
// swift-tools-version: 5.10
import PackageDescription
var dependencies: [Package.Dependency] = [
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMinor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "1.0.0")),
]
#if swift(>=5.6)
dependencies.append(contentsOf: [
.package(path: "../Articles"),
.package(path: "../Articles"),
.package(path: "../Database"),
])
#else
dependencies.append(contentsOf: [
@ -18,11 +18,10 @@ dependencies.append(contentsOf: [
let package = Package(
name: "SyncDatabase",
platforms: [.macOS(SupportedPlatform.MacOSVersion.v10_15), .iOS(SupportedPlatform.IOSVersion.v13)],
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "SyncDatabase",
type: .dynamic,
targets: ["SyncDatabase"]),
],
dependencies: dependencies,
@ -31,9 +30,12 @@ let package = Package(
name: "SyncDatabase",
dependencies: [
"RSCore",
"RSDatabase",
"Database",
"Articles",
]
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
)
]
)