mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Move modules to Modules folder.
This commit is contained in:
18
Modules/RSDatabase/.github/workflows/build.yml
vendored
Normal file
18
Modules/RSDatabase/.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: CI
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: macOS-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v1
|
||||
|
||||
- name: Switch to Xcode 12
|
||||
run: sudo xcode-select -s /Applications/Xcode_12.app
|
||||
|
||||
- name: Run Build
|
||||
run: swift build
|
||||
61
Modules/RSDatabase/.gitignore
vendored
Normal file
61
Modules/RSDatabase/.gitignore
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# Xcode
|
||||
#
|
||||
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
|
||||
|
||||
## Build generated
|
||||
.build/
|
||||
build/
|
||||
DerivedData/
|
||||
|
||||
## Various settings
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata/
|
||||
|
||||
## Other
|
||||
*.moved-aside
|
||||
*.xcuserstate
|
||||
|
||||
## Obj-C/Swift specific
|
||||
*.hmap
|
||||
*.ipa
|
||||
*.dSYM.zip
|
||||
*.dSYM
|
||||
|
||||
# CocoaPods
|
||||
#
|
||||
# We recommend against adding the Pods directory to your .gitignore. However
|
||||
# you should judge for yourself, the pros and cons are mentioned at:
|
||||
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
||||
#
|
||||
# Pods/
|
||||
|
||||
# Carthage
|
||||
#
|
||||
# Add this line if you want to avoid checking in source code from Carthage dependencies.
|
||||
# Carthage/Checkouts
|
||||
|
||||
Carthage/Build
|
||||
|
||||
# fastlane
|
||||
#
|
||||
# It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
|
||||
# screenshots whenever they are needed.
|
||||
# For more information about the recommended setup visit:
|
||||
# https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md
|
||||
|
||||
fastlane/report.xml
|
||||
fastlane/screenshots
|
||||
|
||||
#Code Injection
|
||||
#
|
||||
# After new code Injection tools there's a generated folder /iOSInjectionProject
|
||||
# https://github.com/johnno1962/injectionforxcode
|
||||
|
||||
iOSInjectionProject/
|
||||
@@ -0,0 +1,67 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1620"
|
||||
version = "1.7">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES"
|
||||
buildArchitectures = "Automatic">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "RSDatabase"
|
||||
BuildableName = "RSDatabase"
|
||||
BlueprintName = "RSDatabase"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
shouldAutocreateTestPlan = "YES">
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Release"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "RSDatabase"
|
||||
BuildableName = "RSDatabase"
|
||||
BlueprintName = "RSDatabase"
|
||||
ReferencedContainer = "container:">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
34
Modules/RSDatabase/Package.swift
Normal file
34
Modules/RSDatabase/Package.swift
Normal file
@@ -0,0 +1,34 @@
|
||||
// swift-tools-version:5.10
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "RSDatabase",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
.library(
|
||||
name: "RSDatabase",
|
||||
type: .dynamic,
|
||||
targets: ["RSDatabase"]),
|
||||
.library(
|
||||
name: "RSDatabaseObjC",
|
||||
type: .dynamic,
|
||||
targets: ["RSDatabaseObjC"]),
|
||||
],
|
||||
dependencies: [
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "RSDatabase",
|
||||
dependencies: ["RSDatabaseObjC"],
|
||||
swiftSettings: [.unsafeFlags(["-warnings-as-errors"])]
|
||||
),
|
||||
.target(
|
||||
name: "RSDatabaseObjC",
|
||||
dependencies: []
|
||||
),
|
||||
.testTarget(
|
||||
name: "RSDatabaseTests",
|
||||
dependencies: ["RSDatabase"]),
|
||||
]
|
||||
)
|
||||
12
Modules/RSDatabase/README.md
Executable file
12
Modules/RSDatabase/README.md
Executable file
@@ -0,0 +1,12 @@
|
||||
# RSDatabase
|
||||
This is utility code for using SQLite via FMDB. It’s not a persistence framework — it’s lower-level.
|
||||
|
||||
It builds as a couple frameworks — one for Mac, one for iOS.
|
||||
|
||||
It has no additional dependencies, but that’s because FMDB is actually included — you might want to instead make sure you have the [latest FMDB](https://github.com/ccgus/fmdb), which isn’t necessarily included here.
|
||||
|
||||
#### What to look at
|
||||
|
||||
The main thing is `RSDatabaseQueue`, which allows you to talk to SQLite-via-FMDB using a serial queue.
|
||||
|
||||
The second thing is `FMDatabase+RSExtras`, which provides methods for a bunch of common queries and updates, so you don’t have to write as much SQL.
|
||||
53
Modules/RSDatabase/Sources/RSDatabase/Database.swift
Normal file
53
Modules/RSDatabase/Sources/RSDatabase/Database.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Database.swift
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 12/15/19.
|
||||
// Copyright © 2019 Brent Simmons. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabaseObjC
|
||||
|
||||
public enum DatabaseError: Error {
|
||||
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 = (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 = (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
|
||||
}
|
||||
}
|
||||
}
|
||||
61
Modules/RSDatabase/Sources/RSDatabase/DatabaseObject.swift
Normal file
61
Modules/RSDatabase/Sources/RSDatabase/DatabaseObject.swift
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// DatabaseObject.swift
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 8/7/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias DatabaseDictionary = [String: Any]
|
||||
|
||||
public protocol DatabaseObject {
|
||||
|
||||
var databaseID: String { get }
|
||||
|
||||
func databaseDictionary() -> DatabaseDictionary?
|
||||
|
||||
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]?
|
||||
}
|
||||
|
||||
public extension DatabaseObject {
|
||||
|
||||
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? {
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Array where Element == DatabaseObject {
|
||||
|
||||
func dictionary() -> [String: DatabaseObject] {
|
||||
|
||||
var d = [String: DatabaseObject]()
|
||||
for object in self {
|
||||
d[object.databaseID] = object
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
func databaseIDs() -> Set<String> {
|
||||
|
||||
return Set(self.map { $0.databaseID })
|
||||
}
|
||||
|
||||
func includesObjectWithDatabaseID(_ databaseID: String) -> Bool {
|
||||
|
||||
for object in self {
|
||||
if object.databaseID == databaseID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func databaseDictionaries() -> [DatabaseDictionary]? {
|
||||
|
||||
let dictionaries = self.compactMap{ $0.databaseDictionary() }
|
||||
return dictionaries.isEmpty ? nil : dictionaries
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
259
Modules/RSDatabase/Sources/RSDatabase/DatabaseQueue.swift
Normal file
259
Modules/RSDatabase/Sources/RSDatabase/DatabaseQueue.swift
Normal file
@@ -0,0 +1,259 @@
|
||||
//
|
||||
// DatabaseQueue.swift
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 11/13/19.
|
||||
// Copyright © 2019 Brent Simmons. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import SQLite3
|
||||
import RSDatabaseObjC
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
139
Modules/RSDatabase/Sources/RSDatabase/DatabaseTable.swift
Normal file
139
Modules/RSDatabase/Sources/RSDatabase/DatabaseTable.swift
Normal file
@@ -0,0 +1,139 @@
|
||||
//
|
||||
// DatabaseTable.swift
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 7/16/17.
|
||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSDatabaseObjC
|
||||
|
||||
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) {
|
||||
|
||||
for oneDictionary in dictionaries {
|
||||
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 RSDatabaseObjC
|
||||
|
||||
// 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 RSDatabaseObjC
|
||||
|
||||
// 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]
|
||||
}
|
||||
}
|
||||
83
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.h
Executable file
83
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.h
Executable file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// FMDatabase+QSKit.h
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 3/3/14.
|
||||
// Copyright (c) 2014 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FMDatabase.h"
|
||||
|
||||
@import Foundation;
|
||||
|
||||
typedef NS_ENUM(NSInteger, RSDatabaseInsertType) {
|
||||
RSDatabaseInsertNormal,
|
||||
RSDatabaseInsertOrReplace,
|
||||
RSDatabaseInsertOrIgnore
|
||||
};
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FMDatabase (RSExtras)
|
||||
|
||||
|
||||
// Keys and table names are assumed to be trusted. Values are not.
|
||||
|
||||
|
||||
// delete from tableName where key in (?, ?, ?)
|
||||
|
||||
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName;
|
||||
|
||||
// delete from tableName where key=?
|
||||
|
||||
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
|
||||
|
||||
|
||||
// select * from tableName where key in (?, ?, ?)
|
||||
|
||||
- (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName;
|
||||
|
||||
// select * from tableName where key = ?
|
||||
|
||||
- (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
|
||||
|
||||
// select * from tableName where key = ? limit 1
|
||||
|
||||
- (FMResultSet * _Nullable)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
|
||||
|
||||
// select * from tableName
|
||||
|
||||
- (FMResultSet * _Nullable)rs_selectAllRows:(NSString *)tableName;
|
||||
|
||||
// select key from tableName;
|
||||
|
||||
- (FMResultSet * _Nullable)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName;
|
||||
|
||||
// select 1 from tableName where key = value limit 1;
|
||||
|
||||
- (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName;
|
||||
|
||||
// select 1 from tableName limit 1;
|
||||
|
||||
- (BOOL)rs_tableIsEmpty:(NSString *)tableName;
|
||||
|
||||
|
||||
// update tableName set key1=?, key2=? where key = value
|
||||
|
||||
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName;
|
||||
|
||||
// update tableName set key1=?, key2=? where key in (?, ?, ?)
|
||||
|
||||
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName;
|
||||
|
||||
// update tableName set valueKey=? where where key in (?, ?, ?)
|
||||
|
||||
- (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName;
|
||||
|
||||
// insert (or replace, or ignore) into tablename (key1, key2) values (val1, val2)
|
||||
|
||||
- (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
180
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.m
Executable file
180
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase+RSExtras.m
Executable file
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// FMDatabase+QSKit.m
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 3/3/14.
|
||||
// Copyright (c) 2014 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FMDatabase+RSExtras.h"
|
||||
#import "NSString+RSDatabase.h"
|
||||
|
||||
|
||||
#define LOG_SQL 0
|
||||
|
||||
static void logSQL(NSString *sql) {
|
||||
#if LOG_SQL
|
||||
NSLog(@"sql: %@", sql);
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@implementation FMDatabase (RSExtras)
|
||||
|
||||
|
||||
#pragma mark - Deleting
|
||||
|
||||
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName {
|
||||
|
||||
if ([values count] < 1) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
|
||||
NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ in %@", tableName, key, placeholders];
|
||||
logSQL(sql);
|
||||
|
||||
return [self executeUpdate:sql withArgumentsInArray:values];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
|
||||
|
||||
NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ = ?", tableName, key];
|
||||
logSQL(sql);
|
||||
return [self executeUpdate:sql, value];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Selecting
|
||||
|
||||
- (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName {
|
||||
|
||||
NSMutableString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ in ", tableName, key];
|
||||
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
|
||||
[sql appendString:placeholders];
|
||||
logSQL(sql);
|
||||
|
||||
return [self executeQuery:sql withArgumentsInArray:values];
|
||||
}
|
||||
|
||||
|
||||
- (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
|
||||
|
||||
NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ?", tableName, key];
|
||||
logSQL(sql);
|
||||
return [self executeQuery:sql, value];
|
||||
}
|
||||
|
||||
|
||||
- (FMResultSet *)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
|
||||
|
||||
NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ? limit 1", tableName, key];
|
||||
logSQL(sql);
|
||||
return [self executeQuery:sql, value];
|
||||
}
|
||||
|
||||
|
||||
- (FMResultSet *)rs_selectAllRows:(NSString *)tableName {
|
||||
|
||||
NSString *sql = [NSString stringWithFormat:@"select * from %@", tableName];
|
||||
logSQL(sql);
|
||||
return [self executeQuery:sql];
|
||||
}
|
||||
|
||||
|
||||
- (FMResultSet *)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName {
|
||||
|
||||
NSString *sql = [NSString stringWithFormat:@"select %@ from %@", key, tableName];
|
||||
logSQL(sql);
|
||||
return [self executeQuery:sql];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName {
|
||||
|
||||
NSString *sql = [NSString stringWithFormat:@"select 1 from %@ where %@ = ? limit 1;", tableName, key];
|
||||
logSQL(sql);
|
||||
FMResultSet *rs = [self executeQuery:sql, value];
|
||||
|
||||
return [rs next];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)rs_tableIsEmpty:(NSString *)tableName {
|
||||
|
||||
NSString *sql = [NSString stringWithFormat:@"select 1 from %@ limit 1;", tableName];
|
||||
logSQL(sql);
|
||||
FMResultSet *rs = [self executeQuery:sql];
|
||||
|
||||
BOOL isEmpty = YES;
|
||||
while ([rs next]) {
|
||||
isEmpty = NO;
|
||||
}
|
||||
return isEmpty;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Updating
|
||||
|
||||
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName {
|
||||
|
||||
return [self rs_updateRowsWithDictionary:d whereKey:key inValues:@[value] tableName:tableName];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName {
|
||||
|
||||
NSMutableArray *keys = [NSMutableArray new];
|
||||
NSMutableArray *values = [NSMutableArray new];
|
||||
|
||||
[d enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
|
||||
[keys addObject:key];
|
||||
[values addObject:obj];
|
||||
}];
|
||||
|
||||
NSString *keyPlaceholders = [NSString rs_SQLKeyPlaceholderPairsWithKeys:keys];
|
||||
NSString *keyValuesPlaceholder = [NSString rs_SQLValueListWithPlaceholders:keyValues.count];
|
||||
NSString *sql = [NSString stringWithFormat:@"update %@ set %@ where %@ in %@", tableName, keyPlaceholders, key, keyValuesPlaceholder];
|
||||
|
||||
NSMutableArray *parameters = values;
|
||||
[parameters addObjectsFromArray:keyValues];
|
||||
logSQL(sql);
|
||||
|
||||
return [self executeUpdate:sql withArgumentsInArray:parameters];
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName {
|
||||
|
||||
NSDictionary *d = @{valueKey: value};
|
||||
return [self rs_updateRowsWithDictionary:d whereKey:key inValues:keyValues tableName:tableName];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Saving
|
||||
|
||||
- (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName {
|
||||
|
||||
NSArray *keys = d.allKeys;
|
||||
NSArray *values = [d objectsForKeys:keys notFoundMarker:[NSNull null]];
|
||||
|
||||
NSString *sqlKeysList = [NSString rs_SQLKeysListWithArray:keys];
|
||||
NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count];
|
||||
|
||||
NSString *sqlBeginning = @"insert into ";
|
||||
if (insertType == RSDatabaseInsertOrReplace) {
|
||||
sqlBeginning = @"insert or replace into ";
|
||||
}
|
||||
else if (insertType == RSDatabaseInsertOrIgnore) {
|
||||
sqlBeginning = @"insert or ignore into ";
|
||||
}
|
||||
|
||||
NSString *sql = [NSString stringWithFormat:@"%@ %@ %@ values %@", sqlBeginning, tableName, sqlKeysList, placeholders];
|
||||
logSQL(sql);
|
||||
|
||||
return [self executeUpdate:sql withArgumentsInArray:values];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
1084
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase.h
Executable file
1084
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase.h
Executable file
File diff suppressed because it is too large
Load Diff
1427
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase.m
Executable file
1427
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabase.m
Executable file
File diff suppressed because it is too large
Load Diff
281
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.h
Executable file
281
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.h
Executable file
@@ -0,0 +1,281 @@
|
||||
//
|
||||
// FMDatabaseAdditions.h
|
||||
// fmdb
|
||||
//
|
||||
// Created by August Mueller on 10/30/05.
|
||||
// Copyright 2005 Flying Meat Inc.. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FMDatabase.h"
|
||||
|
||||
|
||||
/** Category of additions for `<FMDatabase>` class.
|
||||
|
||||
### See also
|
||||
|
||||
- `<FMDatabase>`
|
||||
*/
|
||||
|
||||
@interface FMDatabase (FMDatabaseAdditions)
|
||||
|
||||
///----------------------------------------
|
||||
/// @name Return results of SQL to variable
|
||||
///----------------------------------------
|
||||
|
||||
/** Return `int` value for query
|
||||
|
||||
@param query The SQL query to be performed.
|
||||
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
|
||||
|
||||
@return `int` value.
|
||||
|
||||
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
|
||||
*/
|
||||
|
||||
- (int)intForQuery:(NSString*)query, ...;
|
||||
|
||||
/** Return `long` value for query
|
||||
|
||||
@param query The SQL query to be performed.
|
||||
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
|
||||
|
||||
@return `long` value.
|
||||
|
||||
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
|
||||
*/
|
||||
|
||||
- (long)longForQuery:(NSString*)query, ...;
|
||||
|
||||
/** Return `BOOL` value for query
|
||||
|
||||
@param query The SQL query to be performed.
|
||||
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
|
||||
|
||||
@return `BOOL` value.
|
||||
|
||||
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
|
||||
*/
|
||||
|
||||
- (BOOL)boolForQuery:(NSString*)query, ...;
|
||||
|
||||
/** Return `double` value for query
|
||||
|
||||
@param query The SQL query to be performed.
|
||||
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
|
||||
|
||||
@return `double` value.
|
||||
|
||||
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
|
||||
*/
|
||||
|
||||
- (double)doubleForQuery:(NSString*)query, ...;
|
||||
|
||||
/** Return `NSString` value for query
|
||||
|
||||
@param query The SQL query to be performed.
|
||||
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
|
||||
|
||||
@return `NSString` value.
|
||||
|
||||
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
|
||||
*/
|
||||
|
||||
- (NSString*)stringForQuery:(NSString*)query, ...;
|
||||
|
||||
/** Return `NSData` value for query
|
||||
|
||||
@param query The SQL query to be performed.
|
||||
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
|
||||
|
||||
@return `NSData` value.
|
||||
|
||||
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
|
||||
*/
|
||||
|
||||
- (NSData*)dataForQuery:(NSString*)query, ...;
|
||||
|
||||
/** Return `NSDate` value for query
|
||||
|
||||
@param query The SQL query to be performed.
|
||||
@param ... A list of parameters that will be bound to the `?` placeholders in the SQL query.
|
||||
|
||||
@return `NSDate` value.
|
||||
|
||||
@note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project.
|
||||
*/
|
||||
|
||||
- (NSDate*)dateForQuery:(NSString*)query, ...;
|
||||
|
||||
|
||||
// Notice that there's no dataNoCopyForQuery:.
|
||||
// That would be a bad idea, because we close out the result set, and then what
|
||||
// happens to the data that we just didn't copy? Who knows, not I.
|
||||
|
||||
|
||||
///--------------------------------
|
||||
/// @name Schema related operations
|
||||
///--------------------------------
|
||||
|
||||
/** Does table exist in database?
|
||||
|
||||
@param tableName The name of the table being looked for.
|
||||
|
||||
@return `YES` if table found; `NO` if not found.
|
||||
*/
|
||||
|
||||
- (BOOL)tableExists:(NSString*)tableName;
|
||||
|
||||
/** The schema of the database.
|
||||
|
||||
This will be the schema for the entire database. For each entity, each row of the result set will include the following fields:
|
||||
|
||||
- `type` - The type of entity (e.g. table, index, view, or trigger)
|
||||
- `name` - The name of the object
|
||||
- `tbl_name` - The name of the table to which the object references
|
||||
- `rootpage` - The page number of the root b-tree page for tables and indices
|
||||
- `sql` - The SQL that created the entity
|
||||
|
||||
@return `FMResultSet` of schema; `nil` on error.
|
||||
|
||||
@see [SQLite File Format](http://www.sqlite.org/fileformat.html)
|
||||
*/
|
||||
|
||||
- (FMResultSet*)getSchema;
|
||||
|
||||
/** The schema of the database.
|
||||
|
||||
This will be the schema for a particular table as report by SQLite `PRAGMA`, for example:
|
||||
|
||||
PRAGMA table_info('employees')
|
||||
|
||||
This will report:
|
||||
|
||||
- `cid` - The column ID number
|
||||
- `name` - The name of the column
|
||||
- `type` - The data type specified for the column
|
||||
- `notnull` - whether the field is defined as NOT NULL (i.e. values required)
|
||||
- `dflt_value` - The default value for the column
|
||||
- `pk` - Whether the field is part of the primary key of the table
|
||||
|
||||
@param tableName The name of the table for whom the schema will be returned.
|
||||
|
||||
@return `FMResultSet` of schema; `nil` on error.
|
||||
|
||||
@see [table_info](http://www.sqlite.org/pragma.html#pragma_table_info)
|
||||
*/
|
||||
|
||||
- (FMResultSet*)getTableSchema:(NSString*)tableName;
|
||||
|
||||
/** Test to see if particular column exists for particular table in database
|
||||
|
||||
@param columnName The name of the column.
|
||||
|
||||
@param tableName The name of the table.
|
||||
|
||||
@return `YES` if column exists in table in question; `NO` otherwise.
|
||||
*/
|
||||
|
||||
- (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName;
|
||||
|
||||
/** Test to see if particular column exists for particular table in database
|
||||
|
||||
@param columnName The name of the column.
|
||||
|
||||
@param tableName The name of the table.
|
||||
|
||||
@return `YES` if column exists in table in question; `NO` otherwise.
|
||||
|
||||
@see columnExists:inTableWithName:
|
||||
|
||||
@warning Deprecated - use `<columnExists:inTableWithName:>` instead.
|
||||
*/
|
||||
|
||||
- (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated));
|
||||
|
||||
|
||||
/** Validate SQL statement
|
||||
|
||||
This validates SQL statement by performing `sqlite3_prepare_v2`, but not returning the results, but instead immediately calling `sqlite3_finalize`.
|
||||
|
||||
@param sql The SQL statement being validated.
|
||||
|
||||
@param error This is a pointer to a `NSError` object that will receive the autoreleased `NSError` object if there was any error. If this is `nil`, no `NSError` result will be returned.
|
||||
|
||||
@return `YES` if validation succeeded without incident; `NO` otherwise.
|
||||
|
||||
*/
|
||||
|
||||
- (BOOL)validateSQL:(NSString*)sql error:(NSError**)error;
|
||||
|
||||
|
||||
#if SQLITE_VERSION_NUMBER >= 3007017
|
||||
|
||||
///-----------------------------------
|
||||
/// @name Application identifier tasks
|
||||
///-----------------------------------
|
||||
|
||||
/** Retrieve application ID
|
||||
|
||||
@return The `uint32_t` numeric value of the application ID.
|
||||
|
||||
@see setApplicationID:
|
||||
*/
|
||||
|
||||
- (uint32_t)applicationID;
|
||||
|
||||
/** Set the application ID
|
||||
|
||||
@param appID The `uint32_t` numeric value of the application ID.
|
||||
|
||||
@see applicationID
|
||||
*/
|
||||
|
||||
- (void)setApplicationID:(uint32_t)appID;
|
||||
|
||||
#if TARGET_OS_MAC && !TARGET_OS_IPHONE
|
||||
/** Retrieve application ID string
|
||||
|
||||
@return The `NSString` value of the application ID.
|
||||
|
||||
@see setApplicationIDString:
|
||||
*/
|
||||
|
||||
|
||||
- (NSString*)applicationIDString;
|
||||
|
||||
/** Set the application ID string
|
||||
|
||||
@param string The `NSString` value of the application ID.
|
||||
|
||||
@see applicationIDString
|
||||
*/
|
||||
|
||||
- (void)setApplicationIDString:(NSString*)string;
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
///-----------------------------------
|
||||
/// @name user version identifier tasks
|
||||
///-----------------------------------
|
||||
|
||||
/** Retrieve user version
|
||||
|
||||
@return The `uint32_t` numeric value of the user version.
|
||||
|
||||
@see setUserVersion:
|
||||
*/
|
||||
|
||||
- (uint32_t)userVersion;
|
||||
|
||||
/** Set the user-version
|
||||
|
||||
@param version The `uint32_t` numeric value of the user version.
|
||||
|
||||
@see userVersion
|
||||
*/
|
||||
|
||||
- (void)setUserVersion:(uint32_t)version;
|
||||
|
||||
@end
|
||||
225
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.m
Executable file
225
Modules/RSDatabase/Sources/RSDatabaseObjC/FMDatabaseAdditions.m
Executable file
@@ -0,0 +1,225 @@
|
||||
//
|
||||
// FMDatabaseAdditions.m
|
||||
// fmdb
|
||||
//
|
||||
// Created by August Mueller on 10/30/05.
|
||||
// Copyright 2005 Flying Meat Inc.. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FMDatabase.h"
|
||||
#import "FMDatabaseAdditions.h"
|
||||
#import "TargetConditionals.h"
|
||||
#import "sqlite3.h"
|
||||
|
||||
@interface FMDatabase (PrivateStuff)
|
||||
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args;
|
||||
@end
|
||||
|
||||
@implementation FMDatabase (FMDatabaseAdditions)
|
||||
|
||||
#define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \
|
||||
va_list args; \
|
||||
va_start(args, query); \
|
||||
FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args]; \
|
||||
va_end(args); \
|
||||
if (![resultSet next]) { return (type)0; } \
|
||||
type ret = [resultSet sel:0]; \
|
||||
[resultSet close]; \
|
||||
[resultSet setParentDB:nil]; \
|
||||
return ret;
|
||||
|
||||
|
||||
- (NSString*)stringForQuery:(NSString*)query, ... {
|
||||
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex);
|
||||
}
|
||||
|
||||
- (int)intForQuery:(NSString*)query, ... {
|
||||
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex);
|
||||
}
|
||||
|
||||
- (long)longForQuery:(NSString*)query, ... {
|
||||
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex);
|
||||
}
|
||||
|
||||
- (BOOL)boolForQuery:(NSString*)query, ... {
|
||||
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex);
|
||||
}
|
||||
|
||||
- (double)doubleForQuery:(NSString*)query, ... {
|
||||
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex);
|
||||
}
|
||||
|
||||
- (NSData*)dataForQuery:(NSString*)query, ... {
|
||||
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex);
|
||||
}
|
||||
|
||||
- (NSDate*)dateForQuery:(NSString*)query, ... {
|
||||
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex);
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)tableExists:(NSString*)tableName {
|
||||
|
||||
tableName = [tableName lowercaseString];
|
||||
|
||||
FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
|
||||
|
||||
//if at least one next exists, table exists
|
||||
BOOL returnBool = [rs next];
|
||||
|
||||
//close and free object
|
||||
[rs close];
|
||||
|
||||
return returnBool;
|
||||
}
|
||||
|
||||
/*
|
||||
get table with list of tables: result columns: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING]
|
||||
check if table exist in database (patch from OZLB)
|
||||
*/
|
||||
- (FMResultSet*)getSchema {
|
||||
|
||||
//result columns: 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 columns: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER]
|
||||
*/
|
||||
- (FMResultSet*)getTableSchema:(NSString*)tableName {
|
||||
|
||||
//result columns: 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
|
||||
23
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.h
Executable file
23
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.h
Executable file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// FMResultSet+RSExtras.h
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 2/19/13.
|
||||
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
#import "FMResultSet.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FMResultSet (RSExtras)
|
||||
|
||||
|
||||
- (NSArray *)rs_arrayForSingleColumnResultSet; // Doesn't handle dates.
|
||||
|
||||
- (NSSet *)rs_setForSingleColumnResultSet;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
51
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.m
Executable file
51
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet+RSExtras.m
Executable file
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// FMResultSet+RSExtras.m
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 2/19/13.
|
||||
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FMResultSet+RSExtras.h"
|
||||
|
||||
|
||||
@implementation FMResultSet (RSExtras)
|
||||
|
||||
|
||||
- (id)valueForKey:(NSString *)key {
|
||||
|
||||
if ([key containsString:@"Date"] || [key containsString:@"date"]) {
|
||||
return [self dateForColumn:key];
|
||||
}
|
||||
|
||||
return [self objectForColumnName:key];
|
||||
}
|
||||
|
||||
|
||||
- (NSArray *)rs_arrayForSingleColumnResultSet {
|
||||
|
||||
NSMutableArray *results = [NSMutableArray new];
|
||||
|
||||
while ([self next]) {
|
||||
id oneObject = [self objectForColumnIndex:0];
|
||||
[results addObject:oneObject];
|
||||
}
|
||||
|
||||
return [results copy];
|
||||
}
|
||||
|
||||
|
||||
- (NSSet *)rs_setForSingleColumnResultSet {
|
||||
|
||||
NSMutableSet *results = [NSMutableSet new];
|
||||
|
||||
while ([self next]) {
|
||||
id oneObject = [self objectForColumnIndex:0];
|
||||
[results addObject:oneObject];
|
||||
}
|
||||
|
||||
return [results copy];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
469
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet.h
Executable file
469
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet.h
Executable file
@@ -0,0 +1,469 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
//#import "sqlite3.h"
|
||||
|
||||
#ifndef __has_feature // Optional.
|
||||
#define __has_feature(x) 0 // Compatibility with non-clang compilers.
|
||||
#endif
|
||||
|
||||
#ifndef NS_RETURNS_NOT_RETAINED
|
||||
#if __has_feature(attribute_ns_returns_not_retained)
|
||||
#define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained))
|
||||
#else
|
||||
#define NS_RETURNS_NOT_RETAINED
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@class FMDatabase;
|
||||
@class FMStatement;
|
||||
|
||||
/** Represents the results of executing a query on an `<FMDatabase>`.
|
||||
|
||||
### See also
|
||||
|
||||
- `<FMDatabase>`
|
||||
*/
|
||||
|
||||
@interface FMResultSet : NSObject {
|
||||
FMDatabase *_parentDB;
|
||||
FMStatement *_statement;
|
||||
|
||||
NSString *_query;
|
||||
NSMutableDictionary *_columnNameToIndexMap;
|
||||
}
|
||||
|
||||
///-----------------
|
||||
/// @name Properties
|
||||
///-----------------
|
||||
|
||||
/** Executed query */
|
||||
|
||||
@property (atomic, retain) NSString *query;
|
||||
|
||||
/** `NSMutableDictionary` mapping column names to numeric index */
|
||||
|
||||
@property (readonly) NSMutableDictionary *columnNameToIndexMap;
|
||||
|
||||
/** `FMStatement` used by result set. */
|
||||
|
||||
@property (atomic, retain) FMStatement *statement;
|
||||
|
||||
///------------------------------------
|
||||
/// @name Creating and closing database
|
||||
///------------------------------------
|
||||
|
||||
/** Create result set from `<FMStatement>`
|
||||
|
||||
@param statement A `<FMStatement>` to be performed
|
||||
|
||||
@param aDB A `<FMDatabase>` to be used
|
||||
|
||||
@return A `FMResultSet` on success; `nil` on failure
|
||||
*/
|
||||
|
||||
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB;
|
||||
|
||||
/** Close result set */
|
||||
|
||||
- (void)close;
|
||||
|
||||
- (void)setParentDB:(FMDatabase *)newDb;
|
||||
|
||||
///---------------------------------------
|
||||
/// @name Iterating through the result set
|
||||
///---------------------------------------
|
||||
|
||||
/** Retrieve next row for result set.
|
||||
|
||||
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
|
||||
|
||||
@return `YES` if row successfully retrieved; `NO` if end of result set reached
|
||||
|
||||
@see hasAnotherRow
|
||||
*/
|
||||
|
||||
- (BOOL)next;
|
||||
|
||||
/** Retrieve next row for result set.
|
||||
|
||||
You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one.
|
||||
|
||||
@param outErr A 'NSError' object to receive any error object (if any).
|
||||
|
||||
@return 'YES' if row successfully retrieved; 'NO' if end of result set reached
|
||||
|
||||
@see hasAnotherRow
|
||||
*/
|
||||
|
||||
- (BOOL)nextWithError:(NSError **)outErr;
|
||||
|
||||
/** Did the last call to `<next>` succeed in retrieving another row?
|
||||
|
||||
@return `YES` if the last call to `<next>` succeeded in retrieving another record; `NO` if not.
|
||||
|
||||
@see next
|
||||
|
||||
@warning The `hasAnotherRow` method must follow a call to `<next>`. If the previous database interaction was something other than a call to `next`, then this method may return `NO`, whether there is another row of data or not.
|
||||
*/
|
||||
|
||||
- (BOOL)hasAnotherRow;
|
||||
|
||||
///---------------------------------------------
|
||||
/// @name Retrieving information from result set
|
||||
///---------------------------------------------
|
||||
|
||||
/** How many columns in result set
|
||||
|
||||
@return Integer value of the number of columns.
|
||||
*/
|
||||
|
||||
- (int)columnCount;
|
||||
|
||||
/** Column index for column name
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return Zero-based index for column.
|
||||
*/
|
||||
|
||||
- (int)columnIndexForName:(NSString*)columnName;
|
||||
|
||||
/** Column name for column index
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return columnName `NSString` value of the name of the column.
|
||||
*/
|
||||
|
||||
- (NSString*)columnNameForIndex:(int)columnIdx;
|
||||
|
||||
/** Result set integer value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `int` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (int)intForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set integer value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `int` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (int)intForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `long` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `long` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (long)longForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set long value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `long` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (long)longForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `long long int` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `long long int` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (long long int)longLongIntForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set `long long int` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `long long int` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (long long int)longLongIntForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `unsigned long long int` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `unsigned long long int` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set `unsigned long long int` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `unsigned long long int` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `BOOL` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `BOOL` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (BOOL)boolForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set `BOOL` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `BOOL` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (BOOL)boolForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `double` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `double` value of the result set's column.
|
||||
|
||||
*/
|
||||
|
||||
- (double)doubleForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set `double` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `double` value of the result set's column.
|
||||
|
||||
*/
|
||||
|
||||
- (double)doubleForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `NSString` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `NSString` value of the result set's column.
|
||||
|
||||
*/
|
||||
|
||||
- (NSString*)stringForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set `NSString` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `NSString` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (NSString*)stringForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `NSDate` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `NSDate` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (NSDate*)dateForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set `NSDate` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `NSDate` value of the result set's column.
|
||||
|
||||
*/
|
||||
|
||||
- (NSDate*)dateForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `NSData` value for column.
|
||||
|
||||
This is useful when storing binary data in table (such as image or the like).
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `NSData` value of the result set's column.
|
||||
|
||||
*/
|
||||
|
||||
- (NSData*)dataForColumn:(NSString*)columnName;
|
||||
|
||||
/** Result set `NSData` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `NSData` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (NSData*)dataForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set `(const unsigned char *)` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `(const unsigned char *)` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName;
|
||||
|
||||
/** Result set `(const unsigned char *)` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `(const unsigned char *)` value of the result set's column.
|
||||
*/
|
||||
|
||||
- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set object for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
|
||||
|
||||
@see objectForKeyedSubscript:
|
||||
*/
|
||||
|
||||
- (id)objectForColumnName:(NSString*)columnName;
|
||||
|
||||
/** Result set object for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
|
||||
|
||||
@see objectAtIndexedSubscript:
|
||||
*/
|
||||
|
||||
- (id)objectForColumnIndex:(int)columnIdx;
|
||||
|
||||
/** Result set object for column.
|
||||
|
||||
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
|
||||
|
||||
id result = rs[@"employee_name"];
|
||||
|
||||
This simplified syntax is equivalent to calling:
|
||||
|
||||
id result = [rs objectForKeyedSubscript:@"employee_name"];
|
||||
|
||||
which is, it turns out, equivalent to calling:
|
||||
|
||||
id result = [rs objectForColumnName:@"employee_name"];
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
|
||||
*/
|
||||
|
||||
- (id)objectForKeyedSubscript:(NSString *)columnName;
|
||||
|
||||
/** Result set object for column.
|
||||
|
||||
This method allows the use of the "boxed" syntax supported in Modern Objective-C. For example, by defining this method, the following syntax is now supported:
|
||||
|
||||
id result = rs[0];
|
||||
|
||||
This simplified syntax is equivalent to calling:
|
||||
|
||||
id result = [rs objectForKeyedSubscript:0];
|
||||
|
||||
which is, it turns out, equivalent to calling:
|
||||
|
||||
id result = [rs objectForColumnName:0];
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object.
|
||||
*/
|
||||
|
||||
- (id)objectAtIndexedSubscript:(int)columnIdx;
|
||||
|
||||
/** Result set `NSData` value for column.
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `NSData` value of the result set's column.
|
||||
|
||||
@warning If you are going to use this data after you iterate over the next row, or after you close the
|
||||
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
|
||||
If you don't, you're going to be in a world of hurt when you try and use the data.
|
||||
|
||||
*/
|
||||
|
||||
- (NSData*)dataNoCopyForColumn:(NSString*)columnName NS_RETURNS_NOT_RETAINED;
|
||||
|
||||
/** Result set `NSData` value for column.
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `NSData` value of the result set's column.
|
||||
|
||||
@warning If you are going to use this data after you iterate over the next row, or after you close the
|
||||
result set, make sure to make a copy of the data first (or just use `<dataForColumn:>`/`<dataForColumnIndex:>`)
|
||||
If you don't, you're going to be in a world of hurt when you try and use the data.
|
||||
|
||||
*/
|
||||
|
||||
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED;
|
||||
|
||||
/** Is the column `NULL`?
|
||||
|
||||
@param columnIdx Zero-based index for column.
|
||||
|
||||
@return `YES` if column is `NULL`; `NO` if not `NULL`.
|
||||
*/
|
||||
|
||||
- (BOOL)columnIndexIsNull:(int)columnIdx;
|
||||
|
||||
/** Is the column `NULL`?
|
||||
|
||||
@param columnName `NSString` value of the name of the column.
|
||||
|
||||
@return `YES` if column is `NULL`; `NO` if not `NULL`.
|
||||
*/
|
||||
|
||||
- (BOOL)columnIsNull:(NSString*)columnName;
|
||||
|
||||
|
||||
/** Returns a dictionary of the row results mapped to case sensitive keys of the column names.
|
||||
|
||||
@returns `NSDictionary` of the row results.
|
||||
|
||||
@warning The keys to the dictionary are case sensitive of the column names.
|
||||
*/
|
||||
|
||||
- (NSDictionary*)resultDictionary;
|
||||
|
||||
/** Returns a dictionary of the row results
|
||||
|
||||
@see resultDictionary
|
||||
|
||||
@warning **Deprecated**: Please use `<resultDictionary>` instead. Also, beware that `<resultDictionary>` is case sensitive!
|
||||
*/
|
||||
|
||||
- (NSDictionary*)resultDict __attribute__ ((deprecated));
|
||||
|
||||
///-----------------------------
|
||||
/// @name Key value coding magic
|
||||
///-----------------------------
|
||||
|
||||
/** Performs `setValue` to yield support for key value observing.
|
||||
|
||||
@param object The object for which the values will be set. This is the key-value-coding compliant object that you might, for example, observe.
|
||||
|
||||
*/
|
||||
|
||||
- (void)kvcMagic:(id)object;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
454
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet.m
Executable file
454
Modules/RSDatabase/Sources/RSDatabaseObjC/FMResultSet.m
Executable file
@@ -0,0 +1,454 @@
|
||||
#import "FMResultSet.h"
|
||||
#import "FMDatabase.h"
|
||||
#import "unistd.h"
|
||||
#import "sqlite3.h"
|
||||
|
||||
@interface FMDatabase ()
|
||||
- (void)resultSetDidClose:(FMResultSet *)resultSet;
|
||||
@end
|
||||
|
||||
@interface FMResultSet ()
|
||||
@property (nonatomic, readonly) NSDictionary *columnNameToIndexMapNonLowercased;
|
||||
@end
|
||||
|
||||
@implementation FMResultSet
|
||||
@synthesize query=_query;
|
||||
@synthesize statement=_statement;
|
||||
@synthesize columnNameToIndexMapNonLowercased = _columnNameToIndexMapNonLowercased;
|
||||
|
||||
+ (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB {
|
||||
|
||||
FMResultSet *rs = [[FMResultSet alloc] init];
|
||||
|
||||
[rs setStatement:statement];
|
||||
[rs setParentDB:aDB];
|
||||
|
||||
NSParameterAssert(![statement inUse]);
|
||||
[statement setInUse:YES]; // weak reference
|
||||
|
||||
return FMDBReturnAutoreleased(rs);
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self close];
|
||||
|
||||
FMDBRelease(_query);
|
||||
_query = nil;
|
||||
|
||||
FMDBRelease(_columnNameToIndexMap);
|
||||
_columnNameToIndexMap = nil;
|
||||
|
||||
#if ! __has_feature(objc_arc)
|
||||
[super dealloc];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
[_statement reset];
|
||||
FMDBRelease(_statement);
|
||||
_statement = nil;
|
||||
|
||||
// we don't need this anymore... (i think)
|
||||
//[_parentDB setInUse:NO];
|
||||
[_parentDB resultSetDidClose:self];
|
||||
_parentDB = nil;
|
||||
}
|
||||
|
||||
- (int)columnCount {
|
||||
return sqlite3_column_count([_statement statement]);
|
||||
}
|
||||
|
||||
- (NSMutableDictionary *)columnNameToIndexMap {
|
||||
if (!_columnNameToIndexMap) {
|
||||
NSDictionary *nonLowercasedMap = self.columnNameToIndexMapNonLowercased;
|
||||
NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:nonLowercasedMap.count];
|
||||
for (NSString *key in nonLowercasedMap.allKeys) {
|
||||
[d setObject:nonLowercasedMap[key] forKey:[self _lowercaseString:key]];
|
||||
}
|
||||
_columnNameToIndexMap = d;
|
||||
}
|
||||
return _columnNameToIndexMap;
|
||||
}
|
||||
|
||||
- (NSDictionary *)columnNameToIndexMapNonLowercased {
|
||||
if (!_columnNameToIndexMapNonLowercased) {
|
||||
int columnCount = sqlite3_column_count([_statement statement]);
|
||||
NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount];
|
||||
int columnIdx = 0;
|
||||
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
|
||||
[d setObject:[NSNumber numberWithInt:columnIdx]
|
||||
forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
|
||||
}
|
||||
_columnNameToIndexMapNonLowercased = d;
|
||||
}
|
||||
return _columnNameToIndexMapNonLowercased;
|
||||
}
|
||||
|
||||
- (void)kvcMagic:(id)object {
|
||||
|
||||
int columnCount = sqlite3_column_count([_statement statement]);
|
||||
|
||||
int columnIdx = 0;
|
||||
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
|
||||
|
||||
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
|
||||
|
||||
// check for a null row
|
||||
if (c) {
|
||||
NSString *s = [NSString stringWithUTF8String:c];
|
||||
|
||||
[object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-implementations"
|
||||
|
||||
- (NSDictionary*)resultDict {
|
||||
|
||||
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
|
||||
|
||||
if (num_cols > 0) {
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
|
||||
|
||||
NSEnumerator *columnNames = [[self columnNameToIndexMap] keyEnumerator];
|
||||
NSString *columnName = nil;
|
||||
while ((columnName = [columnNames nextObject])) {
|
||||
id objectValue = [self objectForColumnName:columnName];
|
||||
[dict setObject:objectValue forKey:columnName];
|
||||
}
|
||||
|
||||
return FMDBReturnAutoreleased([dict copy]);
|
||||
}
|
||||
else {
|
||||
NSLog(@"Warning: There seem to be no columns in this set.");
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
- (NSDictionary*)resultDictionary {
|
||||
|
||||
NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]);
|
||||
|
||||
if (num_cols > 0) {
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
|
||||
|
||||
int columnCount = sqlite3_column_count([_statement statement]);
|
||||
|
||||
int columnIdx = 0;
|
||||
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
|
||||
|
||||
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)];
|
||||
id objectValue = [self objectForColumnIndex:columnIdx];
|
||||
[dict setObject:objectValue forKey:columnName];
|
||||
}
|
||||
|
||||
return dict;
|
||||
}
|
||||
else {
|
||||
NSLog(@"Warning: There seem to be no columns in this set.");
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
- (BOOL)next {
|
||||
return [self nextWithError:nil];
|
||||
}
|
||||
|
||||
- (BOOL)nextWithError:(NSError **)outErr {
|
||||
|
||||
int rc = sqlite3_step([_statement statement]);
|
||||
|
||||
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
|
||||
NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]);
|
||||
NSLog(@"Database busy");
|
||||
if (outErr) {
|
||||
*outErr = [_parentDB lastError];
|
||||
}
|
||||
}
|
||||
else if (SQLITE_DONE == rc || SQLITE_ROW == rc) {
|
||||
// all is well, let's return.
|
||||
}
|
||||
else if (SQLITE_ERROR == rc) {
|
||||
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
|
||||
if (outErr) {
|
||||
*outErr = [_parentDB lastError];
|
||||
}
|
||||
}
|
||||
else if (SQLITE_MISUSE == rc) {
|
||||
// uh oh.
|
||||
NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
|
||||
if (outErr) {
|
||||
if (_parentDB) {
|
||||
*outErr = [_parentDB lastError];
|
||||
}
|
||||
else {
|
||||
// If 'next' or 'nextWithError' is called after the result set is closed,
|
||||
// we need to return the appropriate error.
|
||||
NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey];
|
||||
*outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage];
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
else {
|
||||
// wtf?
|
||||
NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle]));
|
||||
if (outErr) {
|
||||
*outErr = [_parentDB lastError];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (rc != SQLITE_ROW) {
|
||||
[self close];
|
||||
}
|
||||
|
||||
return (rc == SQLITE_ROW);
|
||||
}
|
||||
|
||||
- (BOOL)hasAnotherRow {
|
||||
return sqlite3_errcode([_parentDB sqliteHandle]) == SQLITE_ROW;
|
||||
}
|
||||
|
||||
- (int)columnIndexForName:(NSString*)columnName {
|
||||
NSNumber *n = self.columnNameToIndexMapNonLowercased[columnName];
|
||||
if (!n) {
|
||||
columnName = [self _lowercaseString:columnName];
|
||||
n = [[self columnNameToIndexMap] objectForKey:columnName];
|
||||
}
|
||||
|
||||
if (n) {
|
||||
return [n intValue];
|
||||
}
|
||||
|
||||
NSLog(@"Warning: I could not find the column named '%@'.", columnName);
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (int)intForColumn:(NSString*)columnName {
|
||||
return [self intForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (int)intForColumnIndex:(int)columnIdx {
|
||||
return sqlite3_column_int([_statement statement], columnIdx);
|
||||
}
|
||||
|
||||
- (long)longForColumn:(NSString*)columnName {
|
||||
return [self longForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (long)longForColumnIndex:(int)columnIdx {
|
||||
return (long)sqlite3_column_int64([_statement statement], columnIdx);
|
||||
}
|
||||
|
||||
- (long long int)longLongIntForColumn:(NSString*)columnName {
|
||||
return [self longLongIntForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (long long int)longLongIntForColumnIndex:(int)columnIdx {
|
||||
return sqlite3_column_int64([_statement statement], columnIdx);
|
||||
}
|
||||
|
||||
- (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName {
|
||||
return [self unsignedLongLongIntForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx {
|
||||
return (unsigned long long int)[self longLongIntForColumnIndex:columnIdx];
|
||||
}
|
||||
|
||||
- (BOOL)boolForColumn:(NSString*)columnName {
|
||||
return [self boolForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (BOOL)boolForColumnIndex:(int)columnIdx {
|
||||
return ([self intForColumnIndex:columnIdx] != 0);
|
||||
}
|
||||
|
||||
- (double)doubleForColumn:(NSString*)columnName {
|
||||
return [self doubleForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (double)doubleForColumnIndex:(int)columnIdx {
|
||||
return sqlite3_column_double([_statement statement], columnIdx);
|
||||
}
|
||||
|
||||
- (NSString*)stringForColumnIndex:(int)columnIdx {
|
||||
|
||||
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx);
|
||||
|
||||
if (!c) {
|
||||
// null row.
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSString stringWithUTF8String:c];
|
||||
}
|
||||
|
||||
- (NSString*)stringForColumn:(NSString*)columnName {
|
||||
return [self stringForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (NSDate*)dateForColumn:(NSString*)columnName {
|
||||
return [self dateForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (NSDate*)dateForColumnIndex:(int)columnIdx {
|
||||
|
||||
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [_parentDB hasDateFormatter] ? [_parentDB dateFromString:[self stringForColumnIndex:columnIdx]] : [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIdx]];
|
||||
}
|
||||
|
||||
|
||||
- (NSData*)dataForColumn:(NSString*)columnName {
|
||||
return [self dataForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (NSData*)dataForColumnIndex:(int)columnIdx {
|
||||
|
||||
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
|
||||
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
|
||||
|
||||
if (dataBuffer == NULL) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
|
||||
}
|
||||
|
||||
|
||||
- (NSData*)dataNoCopyForColumn:(NSString*)columnName {
|
||||
return [self dataNoCopyForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (NSData*)dataNoCopyForColumnIndex:(int)columnIdx {
|
||||
|
||||
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx);
|
||||
int dataSize = sqlite3_column_bytes([_statement statement], columnIdx);
|
||||
|
||||
NSData *data = [NSData dataWithBytesNoCopy:(void *)dataBuffer length:(NSUInteger)dataSize freeWhenDone:NO];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)columnIndexIsNull:(int)columnIdx {
|
||||
return sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL;
|
||||
}
|
||||
|
||||
- (BOOL)columnIsNull:(NSString*)columnName {
|
||||
return [self columnIndexIsNull:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx {
|
||||
|
||||
if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return sqlite3_column_text([_statement statement], columnIdx);
|
||||
}
|
||||
|
||||
- (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName {
|
||||
return [self UTF8StringForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
- (id)objectForColumnIndex:(int)columnIdx {
|
||||
int columnType = sqlite3_column_type([_statement statement], columnIdx);
|
||||
|
||||
id returnValue = nil;
|
||||
|
||||
if (columnType == SQLITE_INTEGER) {
|
||||
returnValue = [NSNumber numberWithLongLong:[self longLongIntForColumnIndex:columnIdx]];
|
||||
}
|
||||
else if (columnType == SQLITE_FLOAT) {
|
||||
returnValue = [NSNumber numberWithDouble:[self doubleForColumnIndex:columnIdx]];
|
||||
}
|
||||
else if (columnType == SQLITE_BLOB) {
|
||||
returnValue = [self dataForColumnIndex:columnIdx];
|
||||
}
|
||||
else {
|
||||
//default to a string for everything else
|
||||
returnValue = [self stringForColumnIndex:columnIdx];
|
||||
}
|
||||
|
||||
if (returnValue == nil) {
|
||||
returnValue = [NSNull null];
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
- (id)objectForColumnName:(NSString*)columnName {
|
||||
return [self objectForColumnIndex:[self columnIndexForName:columnName]];
|
||||
}
|
||||
|
||||
// returns autoreleased NSString containing the name of the column in the result set
|
||||
- (NSString*)columnNameForIndex:(int)columnIdx {
|
||||
return [NSString stringWithUTF8String: sqlite3_column_name([_statement statement], columnIdx)];
|
||||
}
|
||||
|
||||
- (void)setParentDB:(FMDatabase *)newDb {
|
||||
_parentDB = newDb;
|
||||
}
|
||||
|
||||
- (id)objectAtIndexedSubscript:(int)columnIdx {
|
||||
return [self objectForColumnIndex:columnIdx];
|
||||
}
|
||||
|
||||
- (id)objectForKeyedSubscript:(NSString *)columnName {
|
||||
return [self objectForColumnName:columnName];
|
||||
}
|
||||
|
||||
// Brent 22 Feb. 2019: Calls to lowerCaseString show up in Instruments too much.
|
||||
// Given that the amount of column names in a given app is going to be pretty small,
|
||||
// we can just cache the lowercase versions
|
||||
- (NSString *)_lowercaseString:(NSString *)s {
|
||||
static NSLock *lock = nil;
|
||||
static NSMutableDictionary *lowercaseStringCache = nil;
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
lock = [[NSLock alloc] init];
|
||||
lowercaseStringCache = [[NSMutableDictionary alloc] init];
|
||||
});
|
||||
|
||||
[lock lock];
|
||||
NSString *lowercaseString = lowercaseStringCache[s];
|
||||
if (lowercaseString == nil) {
|
||||
lowercaseString = s.lowercaseString;
|
||||
lowercaseStringCache[s] = lowercaseString;
|
||||
}
|
||||
[lock unlock];
|
||||
|
||||
return lowercaseString;
|
||||
}
|
||||
|
||||
@end
|
||||
36
Modules/RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.h
Executable file
36
Modules/RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.h
Executable file
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// NSString+RSDatabase.h
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 3/27/15.
|
||||
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSString (QSDatabase)
|
||||
|
||||
|
||||
/*Returns @"(?, ?, ?)" -- where number of ? spots is specified by numberOfValues.
|
||||
numberOfValues should be greater than 0. Triggers an NSParameterAssert if not.*/
|
||||
|
||||
+ (nullable NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues;
|
||||
|
||||
|
||||
/*Returns @"(someColumn, anotherColumm, thirdColumn)" -- using passed-in keys.
|
||||
It's essential that you trust keys. They must not be user input.
|
||||
Triggers an NSParameterAssert if keys are empty.*/
|
||||
|
||||
+ (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys;
|
||||
|
||||
|
||||
/*Returns @"key1=?, key2=?" using passed-in keys. Keys must be trusted.*/
|
||||
|
||||
+ (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
135
Modules/RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.m
Executable file
135
Modules/RSDatabase/Sources/RSDatabaseObjC/NSString+RSDatabase.m
Executable file
@@ -0,0 +1,135 @@
|
||||
//
|
||||
// NSString+RSDatabase.m
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 3/27/15.
|
||||
// Copyright (c) 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSString+RSDatabase.h"
|
||||
|
||||
|
||||
@implementation NSString (RSDatabase)
|
||||
|
||||
|
||||
+ (NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues {
|
||||
|
||||
// @"(?, ?, ?)"
|
||||
|
||||
NSParameterAssert(numberOfValues > 0);
|
||||
if (numberOfValues < 1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
static NSMutableDictionary *cache = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
static NSLock *lock = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
lock = [[NSLock alloc] init];
|
||||
cache = [NSMutableDictionary new];
|
||||
});
|
||||
|
||||
[lock lock];
|
||||
NSNumber *cacheKey = @(numberOfValues);
|
||||
NSString *cachedString = cache[cacheKey];
|
||||
if (cachedString) {
|
||||
[lock unlock];
|
||||
return cachedString;
|
||||
}
|
||||
|
||||
NSMutableString *s = [[NSMutableString alloc] initWithString:@"("];
|
||||
NSUInteger i = 0;
|
||||
|
||||
for (i = 0; i < numberOfValues; i++) {
|
||||
|
||||
[s appendString:@"?"];
|
||||
BOOL isLast = (i == (numberOfValues - 1));
|
||||
if (!isLast) {
|
||||
[s appendString:@", "];
|
||||
}
|
||||
}
|
||||
|
||||
[s appendString:@")"];
|
||||
|
||||
cache[cacheKey] = s;
|
||||
[lock unlock];
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
+ (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys {
|
||||
|
||||
NSParameterAssert(keys.count > 0);
|
||||
|
||||
static NSMutableDictionary *cache = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
static NSLock *lock = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
lock = [[NSLock alloc] init];
|
||||
cache = [NSMutableDictionary new];
|
||||
});
|
||||
|
||||
[lock lock];
|
||||
NSArray *cacheKey = keys;
|
||||
NSString *cachedString = cache[cacheKey];
|
||||
if (cachedString) {
|
||||
[lock unlock];
|
||||
return cachedString;
|
||||
}
|
||||
|
||||
NSString *s = [NSString stringWithFormat:@"(%@)", [keys componentsJoinedByString:@", "]];
|
||||
|
||||
cache[cacheKey] = s;
|
||||
[lock unlock];
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
+ (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys {
|
||||
|
||||
// key1=?, key2=?
|
||||
|
||||
NSParameterAssert(keys.count > 0);
|
||||
|
||||
static NSMutableDictionary *cache = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
static NSLock *lock = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
lock = [[NSLock alloc] init];
|
||||
cache = [NSMutableDictionary new];
|
||||
});
|
||||
|
||||
[lock lock];
|
||||
NSArray *cacheKey = keys;
|
||||
NSString *cachedString = cache[cacheKey];
|
||||
if (cachedString) {
|
||||
[lock unlock];
|
||||
return cachedString;
|
||||
}
|
||||
|
||||
NSMutableString *s = [NSMutableString stringWithString:@""];
|
||||
|
||||
NSUInteger i = 0;
|
||||
NSUInteger numberOfKeys = [keys count];
|
||||
|
||||
for (i = 0; i < numberOfKeys; i++) {
|
||||
|
||||
NSString *oneKey = keys[i];
|
||||
[s appendString:oneKey];
|
||||
[s appendString:@"=?"];
|
||||
BOOL isLast = (i == (numberOfKeys - 1));
|
||||
if (!isLast) {
|
||||
[s appendString:@", "];
|
||||
}
|
||||
}
|
||||
|
||||
cache[cacheKey] = s;
|
||||
[lock unlock];
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
63
Modules/RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.h
Executable file
63
Modules/RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.h
Executable file
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// RSDatabaseQueue.h
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 10/19/13.
|
||||
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Foundation;
|
||||
#import "FMDatabase.h"
|
||||
|
||||
// This has been deprecated — use DatabaseQueue instead.
|
||||
|
||||
@class RSDatabaseQueue;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@protocol RSDatabaseQueueDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
- (void)makeFunctionsForDatabase:(FMDatabase *)database queue:(RSDatabaseQueue *)queue;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
// Everything runs on a serial queue.
|
||||
|
||||
typedef void (^RSDatabaseBlock)(FMDatabase * __nonnull database);
|
||||
|
||||
|
||||
@interface RSDatabaseQueue : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString *databasePath; // For debugging use, so you can open the database in sqlite3.
|
||||
|
||||
- (instancetype)initWithFilepath:(NSString *)filepath excludeFromBackup:(BOOL)excludeFromBackup;
|
||||
|
||||
@property (nonatomic, weak) id<RSDatabaseQueueDelegate> delegate;
|
||||
|
||||
// You can feed it the contents of a file that includes comments, etc.
|
||||
// Lines that start with case-insensitive "create " are executed.
|
||||
- (void)createTablesUsingStatements:(NSString *)createStatements;
|
||||
- (void)createTablesUsingStatementsSync:(NSString *)createStatements;
|
||||
|
||||
- (void)update:(RSDatabaseBlock)updateBlock;
|
||||
- (void)updateSync:(RSDatabaseBlock)updateBlock;
|
||||
|
||||
- (void)runInDatabase:(RSDatabaseBlock)databaseBlock; // Same as update, but no transaction.
|
||||
|
||||
- (void)fetch:(RSDatabaseBlock)fetchBlock;
|
||||
- (void)fetchSync:(RSDatabaseBlock)fetchBlock;
|
||||
|
||||
- (void)vacuum;
|
||||
- (void)vacuumIfNeeded; // defaultsKey = @"lastVacuumDate"; interval is 6 days.
|
||||
- (void)vacuumIfNeeded:(NSString *)defaultsKey intervalBetweenVacuums:(NSTimeInterval)intervalBetweenVacuums;
|
||||
|
||||
- (NSArray *)arrayWithSingleColumnResultSet:(FMResultSet *)rs;
|
||||
|
||||
- (void)close;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
227
Modules/RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.m
Executable file
227
Modules/RSDatabase/Sources/RSDatabaseObjC/RSDatabaseQueue.m
Executable file
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// RSDatabaseQueue.m
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 10/19/13.
|
||||
// Copyright (c) 2013 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "RSDatabaseQueue.h"
|
||||
#import <sqlite3.h>
|
||||
|
||||
// This has been deprecated — use DatabaseQueue instead.
|
||||
|
||||
@interface RSDatabaseQueue ()
|
||||
|
||||
@property (nonatomic, strong, readwrite) NSString *databasePath;
|
||||
@property (nonatomic, assign) BOOL excludeFromBackup;
|
||||
@property (nonatomic, strong, readonly) dispatch_queue_t serialDispatchQueue;
|
||||
@property (nonatomic) BOOL closing;
|
||||
@property (nonatomic) BOOL closed;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation RSDatabaseQueue
|
||||
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
- (instancetype)initWithFilepath:(NSString *)filepath excludeFromBackup:(BOOL)excludeFromBackup {
|
||||
|
||||
self = [super init];
|
||||
if (self == nil)
|
||||
return self;
|
||||
|
||||
_databasePath = filepath;
|
||||
|
||||
_serialDispatchQueue = dispatch_queue_create([[NSString stringWithFormat:@"RSDatabaseQueue serial queue - %@", filepath.lastPathComponent] UTF8String], DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
_excludeFromBackup = excludeFromBackup;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Database
|
||||
|
||||
- (FMDatabase *)database {
|
||||
|
||||
/*I've always done it this way -- kept a per-thread database in the threadDictionary -- and I know it's solid. Maybe it's not necessary with a serial queue, but my understanding was that SQLite wanted a different database per thread (and a serial queue may run on different threads).*/
|
||||
|
||||
if (self.closed) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
|
||||
FMDatabase *database = threadDictionary[self.databasePath];
|
||||
|
||||
if (!database || !database.open) {
|
||||
|
||||
database = [FMDatabase databaseWithPath:self.databasePath];
|
||||
[database open];
|
||||
[database executeUpdate:@"PRAGMA synchronous = 1;"];
|
||||
[database setShouldCacheStatements:YES];
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(makeFunctionsForDatabase:queue:)]) {
|
||||
[self.delegate makeFunctionsForDatabase:database queue:self];
|
||||
}
|
||||
|
||||
threadDictionary[self.databasePath] = database;
|
||||
|
||||
if (self.excludeFromBackup) {
|
||||
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
NSURL *URL = [NSURL fileURLWithPath:self.databasePath isDirectory:NO];
|
||||
NSError *error = nil;
|
||||
[URL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:&error];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
#pragma mark - API
|
||||
|
||||
- (void)createTablesUsingStatements:(NSString *)createStatements {
|
||||
|
||||
[self runInDatabase:^(FMDatabase *database) {
|
||||
[self runCreateStatements:createStatements database:database];
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
- (void)createTablesUsingStatementsSync:(NSString *)createStatements {
|
||||
|
||||
[self runInDatabaseSync:^(FMDatabase *database) {
|
||||
[self runCreateStatements:createStatements database:database];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)runCreateStatements:(NSString *)createStatements database:(FMDatabase *)database {
|
||||
|
||||
[createStatements enumerateLinesUsingBlock:^(NSString *line, BOOL *stop) {
|
||||
if ([line.lowercaseString hasPrefix:@"create "]) {
|
||||
[database executeUpdate:line];
|
||||
}
|
||||
*stop = NO;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)update:(RSDatabaseBlock)updateBlock {
|
||||
|
||||
dispatch_async(self.serialDispatchQueue, ^{
|
||||
[self runInTransaction:updateBlock];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)updateSync:(RSDatabaseBlock)updateBlock {
|
||||
|
||||
dispatch_sync(self.serialDispatchQueue, ^{
|
||||
[self runInTransaction:updateBlock];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)runInTransaction:(RSDatabaseBlock)databaseBlock {
|
||||
|
||||
@autoreleasepool {
|
||||
FMDatabase *database = [self database];
|
||||
[database beginTransaction];
|
||||
databaseBlock(database);
|
||||
[database commit];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)runInDatabase:(RSDatabaseBlock)databaseBlock {
|
||||
|
||||
dispatch_async(self.serialDispatchQueue, ^{
|
||||
@autoreleasepool {
|
||||
databaseBlock([self database]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)runInDatabaseSync:(RSDatabaseBlock)databaseBlock {
|
||||
|
||||
dispatch_sync(self.serialDispatchQueue, ^{
|
||||
@autoreleasepool {
|
||||
databaseBlock([self database]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)fetch:(RSDatabaseBlock)fetchBlock {
|
||||
|
||||
[self runInDatabase:fetchBlock];
|
||||
}
|
||||
|
||||
|
||||
- (void)fetchSync:(RSDatabaseBlock)fetchBlock {
|
||||
|
||||
dispatch_sync(self.serialDispatchQueue, ^{
|
||||
@autoreleasepool {
|
||||
fetchBlock([self database]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
- (void)vacuum {
|
||||
|
||||
dispatch_async(self.serialDispatchQueue, ^{
|
||||
@autoreleasepool {
|
||||
[[self database] executeUpdate:@"vacuum;"];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)vacuumIfNeeded {
|
||||
|
||||
NSTimeInterval interval = (24 * 60 * 60) * 6; // 6 days
|
||||
[self vacuumIfNeeded:@"lastVacuumDate" intervalBetweenVacuums:interval];
|
||||
}
|
||||
|
||||
|
||||
- (void)vacuumIfNeeded:(NSString *)defaultsKey intervalBetweenVacuums:(NSTimeInterval)intervalBetweenVacuums {
|
||||
|
||||
NSDate *lastVacuumDate = [[NSUserDefaults standardUserDefaults] objectForKey:defaultsKey];
|
||||
if (!lastVacuumDate || ![lastVacuumDate isKindOfClass:[NSDate class]]) {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:defaultsKey];
|
||||
return;
|
||||
}
|
||||
|
||||
NSDate *cutoffDate = [[NSDate date] dateByAddingTimeInterval: -(intervalBetweenVacuums)];
|
||||
if ([cutoffDate earlierDate:lastVacuumDate] == lastVacuumDate) {
|
||||
[[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:defaultsKey];
|
||||
[self vacuum];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (NSArray *)arrayWithSingleColumnResultSet:(FMResultSet *)rs {
|
||||
|
||||
NSMutableArray *results = [NSMutableArray new];
|
||||
while ([rs next]) {
|
||||
id oneObject = [rs objectForColumnIndex:0];
|
||||
if (oneObject) {
|
||||
[results addObject:oneObject];
|
||||
}
|
||||
}
|
||||
|
||||
return [results copy];
|
||||
}
|
||||
|
||||
- (void)close {
|
||||
self.closing = YES;
|
||||
[self runInDatabaseSync:^(FMDatabase *database) {
|
||||
self.closed = YES;
|
||||
[database close];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
// RSDatabaseObjC
|
||||
|
||||
// FMDB
|
||||
|
||||
#import "../FMDatabase.h"
|
||||
#import "../FMDatabaseAdditions.h"
|
||||
#import "../FMResultSet.h"
|
||||
|
||||
// Categories
|
||||
|
||||
#import "../FMDatabase+RSExtras.h"
|
||||
#import "../FMResultSet+RSExtras.h"
|
||||
#import "../NSString+RSDatabase.h"
|
||||
|
||||
// RSDatabase
|
||||
|
||||
#import "../RSDatabaseQueue.h"
|
||||
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// File.swift
|
||||
// RSDatabase
|
||||
//
|
||||
// Created by Brent Simmons on 11/10/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
Reference in New Issue
Block a user