Make local Database and FMDB modules. Stop using RSDatabase.
This commit is contained in:
parent
ee58096a48
commit
b662ad8ad3
|
@ -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",
|
||||
|
|
|
@ -14,7 +14,7 @@ import Foundation
|
|||
import RSCore
|
||||
import Articles
|
||||
import RSParser
|
||||
import RSDatabase
|
||||
import Database
|
||||
import ArticlesDatabase
|
||||
import RSWeb
|
||||
import os.log
|
||||
|
|
|
@ -11,7 +11,7 @@ import RSCore
|
|||
import RSWeb
|
||||
import Articles
|
||||
import ArticlesDatabase
|
||||
import RSDatabase
|
||||
import Database
|
||||
|
||||
// Main thread only.
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import Database
|
||||
import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import Database
|
||||
import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import Database
|
||||
import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
]),
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import Database
|
||||
import RSParser
|
||||
import Articles
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
import RSParser
|
||||
import Articles
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
import Articles
|
||||
|
||||
// article->authors is a many-to-many relationship.
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
import Articles
|
||||
import RSParser
|
||||
|
||||
|
|
|
@ -7,8 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
import Articles
|
||||
|
||||
extension ArticleStatus {
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import Articles
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
import RSParser
|
||||
|
||||
// MARK: - DatabaseObject
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabase
|
||||
import Database
|
||||
import Articles
|
||||
|
||||
extension Array where Element == DatabaseObject {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabase
|
||||
import Database
|
||||
import Articles
|
||||
|
||||
extension RelatedObjectsMap {
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
|
||||
public final class FetchAllUnreadCountsOperation: MainThreadOperation {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
import Articles
|
||||
import RSParser
|
||||
|
||||
|
|
|
@ -8,8 +8,7 @@
|
|||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSDatabase
|
||||
import RSDatabaseObjC
|
||||
import Database
|
||||
import Articles
|
||||
|
||||
// Article->ArticleStatus is a to-one relationship.
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -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"]),
|
||||
]
|
||||
)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 it’s 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 don’t 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 isn’t 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, it’s 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 it’s 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 it’s 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)) // Doesn’t 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
@ -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] {
|
||||
|
||||
// Can’t create a Set, because we can’t make a Set<DatabaseObject>, because protocol-conforming objects can’t 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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).
|
||||
// It’s 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]
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -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"]),
|
||||
]
|
||||
)
|
|
@ -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
|
|
@ -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
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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" */;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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")
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue