Move modules to Modules folder.

This commit is contained in:
Brent Simmons
2025-01-06 21:13:56 -08:00
parent 430871c94a
commit 2933d9aca0
463 changed files with 2 additions and 20 deletions

View 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
View 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/

View File

@@ -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>

View 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
View File

@@ -0,0 +1,12 @@
# RSDatabase
This is utility code for using SQLite via FMDB. Its not a persistence framework — its lower-level.
It builds as a couple frameworks — one for Mac, one for iOS.
It has no additional dependencies, but thats because FMDB is actually included — you might want to instead make sure you have the [latest FMDB](https://github.com/ccgus/fmdb), which isnt 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 dont have to write as much SQL.

View 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
}
}
}

View File

@@ -0,0 +1,61 @@
//
// DatabaseObject.swift
// RSDatabase
//
// Created by Brent Simmons on 8/7/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public typealias DatabaseDictionary = [String: Any]
public protocol DatabaseObject {
var databaseID: String { get }
func databaseDictionary() -> DatabaseDictionary?
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]?
}
public extension DatabaseObject {
func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? {
return nil
}
}
extension Array where Element == DatabaseObject {
func dictionary() -> [String: DatabaseObject] {
var d = [String: DatabaseObject]()
for object in self {
d[object.databaseID] = object
}
return d
}
func databaseIDs() -> Set<String> {
return Set(self.map { $0.databaseID })
}
func includesObjectWithDatabaseID(_ databaseID: String) -> Bool {
for object in self {
if object.databaseID == databaseID {
return true
}
}
return false
}
func databaseDictionaries() -> [DatabaseDictionary]? {
let dictionaries = self.compactMap{ $0.databaseDictionary() }
return dictionaries.isEmpty ? nil : dictionaries
}
}

View File

@@ -0,0 +1,33 @@
//
// DatabaseObjectCache.swift
// RSDatabase
//
// Created by Brent Simmons on 9/12/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public final class DatabaseObjectCache {
private var d = [String: DatabaseObject]()
public init() {
//
}
public func add(_ databaseObjects: [DatabaseObject]) {
for databaseObject in databaseObjects {
self[databaseObject.databaseID] = databaseObject
}
}
public subscript(_ databaseID: String) -> DatabaseObject? {
get {
return d[databaseID]
}
set {
d[databaseID] = newValue
}
}
}

View File

@@ -0,0 +1,259 @@
//
// DatabaseQueue.swift
// RSDatabase
//
// Created by Brent Simmons on 11/13/19.
// Copyright © 2019 Brent Simmons. All rights reserved.
//
import Foundation
import SQLite3
import 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 its always false.
public var isSuspended: Bool {
#if os(iOS)
precondition(Thread.isMainThread)
return _isSuspended
#else
return false
#endif
}
private var _isSuspended = true
private var isCallingDatabase = false
private let database: FMDatabase
private let databasePath: String
private let serialDispatchQueue: DispatchQueue
private let targetDispatchQueue: DispatchQueue
#if os(iOS)
private let databaseLock = NSLock()
#endif
/// When init returns, the database will not be suspended: it will be ready for database calls.
public init(databasePath: String) {
precondition(Thread.isMainThread)
self.serialDispatchQueue = DispatchQueue(label: "DatabaseQueue (Serial) - \(databasePath)", attributes: .initiallyInactive)
self.targetDispatchQueue = DispatchQueue(label: "DatabaseQueue (Target) - \(databasePath)")
self.serialDispatchQueue.setTarget(queue: self.targetDispatchQueue)
self.serialDispatchQueue.activate()
self.databasePath = databasePath
self.database = FMDatabase(path: databasePath)!
openDatabase()
_isSuspended = false
}
// MARK: - Suspend and Resume
/// Close the SQLite database and dont allow database calls until resumed.
/// This is for iOS, where we need to close the SQLite database in some conditions.
///
/// After calling suspend, if you call into the database before calling resume,
/// your code will not run, and runInDatabaseSync and runInTransactionSync will
/// both throw DatabaseQueueError.isSuspended.
///
/// On Mac, suspend() and resume() are no-ops, since there isnt a need for them.
public func suspend() {
#if os(iOS)
precondition(Thread.isMainThread)
guard !_isSuspended else {
return
}
_isSuspended = true
serialDispatchQueue.suspend()
targetDispatchQueue.async {
self.lockDatabase()
self.database.close()
self.unlockDatabase()
DispatchQueue.main.async {
self.serialDispatchQueue.resume()
}
}
#endif
}
/// Open the SQLite database. Allow database calls again.
/// This is also for iOS only.
public func resume() {
#if os(iOS)
precondition(Thread.isMainThread)
guard _isSuspended else {
return
}
serialDispatchQueue.suspend()
targetDispatchQueue.sync {
if _isSuspended {
lockDatabase()
openDatabase()
unlockDatabase()
_isSuspended = false
}
}
serialDispatchQueue.resume()
#endif
}
// MARK: - Make Database Calls
/// Run a DatabaseBlock synchronously. This call will block the main thread
/// potentially for a while, depending on how long it takes to execute
/// the DatabaseBlock *and* depending on how many other calls have been
/// scheduled on the queue. Use sparingly  prefer async versions.
public func runInDatabaseSync(_ databaseBlock: DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.sync {
self._runInDatabase(self.database, databaseBlock, false)
}
}
/// Run a DatabaseBlock asynchronously.
public func runInDatabase(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.async {
self._runInDatabase(self.database, databaseBlock, false)
}
}
/// Run a DatabaseBlock wrapped in a transaction synchronously.
/// Transactions help performance significantly when updating the database.
/// Nevertheless, its best to avoid this because it will block the main thread
/// prefer the async `runInTransaction` instead.
public func runInTransactionSync(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.sync {
self._runInDatabase(self.database, databaseBlock, true)
}
}
/// Run a DatabaseBlock wrapped in a transaction asynchronously.
/// Transactions help performance significantly when updating the database.
public func runInTransaction(_ databaseBlock: @escaping DatabaseBlock) {
precondition(Thread.isMainThread)
serialDispatchQueue.async {
self._runInDatabase(self.database, databaseBlock, true)
}
}
/// Run all the lines that start with "create".
/// Use this to create tables, indexes, etc.
public func runCreateStatements(_ statements: String) throws {
precondition(Thread.isMainThread)
var error: DatabaseError? = nil
runInDatabaseSync { result in
switch result {
case .success(let database):
statements.enumerateLines { (line, stop) in
if line.lowercased().hasPrefix("create") {
database.executeStatements(line)
}
stop = false
}
case .failure(let databaseError):
error = databaseError
}
}
if let error = error {
throw(error)
}
}
/// Compact the database. This should be done from time to time
/// weekly-ish? to keep up the performance level of a database.
/// Generally a thing to do at startup, if its been a while
/// since the last vacuum() call. You almost certainly want to call
/// vacuumIfNeeded instead.
public func vacuum() {
precondition(Thread.isMainThread)
runInDatabase { result in
result.database?.executeStatements("vacuum;")
}
}
/// Vacuum the database if its been more than `daysBetweenVacuums` since the last vacuum.
/// Normally you would call this right after initing a DatabaseQueue.
///
/// - Returns: true if database will be vacuumed.
@discardableResult
public func vacuumIfNeeded(daysBetweenVacuums: Int) -> Bool {
precondition(Thread.isMainThread)
let defaultsKey = "DatabaseQueue-LastVacuumDate-\(databasePath)"
let minimumVacuumInterval = TimeInterval(daysBetweenVacuums * (60 * 60 * 24)) // Doesnt have to be precise
let now = Date()
let cutoffDate = now - minimumVacuumInterval
if let lastVacuumDate = UserDefaults.standard.object(forKey: defaultsKey) as? Date {
if lastVacuumDate < cutoffDate {
vacuum()
UserDefaults.standard.set(now, forKey: defaultsKey)
return true
}
return false
}
// Never vacuumed  almost certainly a new database.
// Just set the LastVacuumDate pref to now and skip vacuuming.
UserDefaults.standard.set(now, forKey: defaultsKey)
return false
}
}
private extension DatabaseQueue {
func lockDatabase() {
#if os(iOS)
databaseLock.lock()
#endif
}
func unlockDatabase() {
#if os(iOS)
databaseLock.unlock()
#endif
}
func _runInDatabase(_ database: FMDatabase, _ databaseBlock: DatabaseBlock, _ useTransaction: Bool) {
lockDatabase()
defer {
unlockDatabase()
}
precondition(!isCallingDatabase)
isCallingDatabase = true
autoreleasepool {
if _isSuspended {
databaseBlock(.failure(.isSuspended))
}
else {
if useTransaction {
database.beginTransaction()
}
databaseBlock(.success(database))
if useTransaction {
database.commit()
}
}
}
isCallingDatabase = false
}
func openDatabase() {
database.open()
database.executeStatements("PRAGMA synchronous = 1;")
database.setShouldCacheStatements(true)
}
}

View File

@@ -0,0 +1,139 @@
//
// DatabaseTable.swift
// RSDatabase
//
// Created by Brent Simmons on 7/16/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import 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))
}
}

View File

@@ -0,0 +1,229 @@
//
// DatabaseLookupTable.swift
// RSDatabase
//
// Created by Brent Simmons on 8/5/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import 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] {
// Cant create a Set, because we cant make a Set<DatabaseObject>, because protocol-conforming objects cant be made Hashable or even Equatable.
// We still want the array to include only one copy of each object, but we have to do it the slow way. Instruments will tell us if this is a performance problem.
var relatedObjectsUniqueArray = [DatabaseObject]()
for object in objects {
guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else {
assertionFailure("uniqueArrayOfRelatedObjects: expected every object to have related objects.")
continue
}
for relatedObject in relatedObjects {
if !relatedObjectsUniqueArray.includesObjectWithDatabaseID(relatedObject.databaseID) {
relatedObjectsUniqueArray += [relatedObject]
}
}
}
return relatedObjectsUniqueArray
}
func syncRelatedObjectsAndLookupTable(_ object: DatabaseObject, _ lookupTable: RelatedObjectIDsMap, _ database: FMDatabase) {
guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else {
assertionFailure("syncRelatedObjectsAndLookupTable should be called only on objects with related objects.")
return
}
let relatedObjectIDs = relatedObjects.databaseIDs()
let lookupTableRelatedObjectIDs = lookupTable[object.databaseID] ?? Set<String>()
let relatedObjectIDsToDelete = lookupTableRelatedObjectIDs.subtracting(relatedObjectIDs)
if !relatedObjectIDsToDelete.isEmpty {
deleteLookups(for: object.databaseID, relatedObjectIDsToDelete, database)
}
let relatedObjectIDsToSave = relatedObjectIDs.subtracting(lookupTableRelatedObjectIDs)
if !relatedObjectIDsToSave.isEmpty {
saveLookups(for: object.databaseID, relatedObjectIDsToSave, database)
}
}
func saveLookups(for objectID: String, _ relatedObjectIDs: Set<String>, _ database: FMDatabase) {
for relatedObjectID in relatedObjectIDs {
let d: [NSObject: Any] = [(objectIDKey as NSString): objectID, (relatedObjectIDKey as NSString): relatedObjectID]
let _ = database.rs_insertRow(with: d, insertType: .orIgnore, tableName: name)
}
}
// MARK: Fetching
func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set<String>, _ database: FMDatabase) -> [DatabaseObject]? {
guard let relatedObjects = relatedTable.fetchObjectsWithIDs(relatedObjectIDs, in: database), !relatedObjects.isEmpty else {
return nil
}
return relatedObjects
}
func fetchRelatedObjectIDsMap(_ objectIDs: Set<String>, _ database: FMDatabase) -> RelatedObjectIDsMap? {
guard let lookupValues = fetchLookupValues(objectIDs, database) else {
return nil
}
return RelatedObjectIDsMap(lookupValues: lookupValues)
}
func fetchLookupValues(_ objectIDs: Set<String>, _ database: FMDatabase) -> Set<LookupValue>? {
guard !objectIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(objectIDKey, inValues: Array(objectIDs), tableName: name) else {
return nil
}
return lookupValuesWithResultSet(resultSet)
}
func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set<LookupValue> {
return resultSet.mapToSet(lookupValueWithRow)
}
func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? {
guard let objectID = row.string(forColumn: objectIDKey) else {
return nil
}
guard let relatedObjectID = row.string(forColumn: relatedObjectIDKey) else {
return nil
}
return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID)
}
}

View File

@@ -0,0 +1,83 @@
//
// DatabaseRelatedObjectsTable.swift
// RSDatabase
//
// Created by Brent Simmons on 9/2/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import 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)
}
}
}

View File

@@ -0,0 +1,63 @@
//
// RelatedObjectIDsMap.swift
// RSDatabase
//
// Created by Brent Simmons on 9/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Maps objectIDs to Set<String> where the Strings are relatedObjectIDs.
struct RelatedObjectIDsMap {
private let dictionary: [String: Set<String>] // objectID: Set<relatedObjectID>
init(dictionary: [String: Set<String>]) {
self.dictionary = dictionary
}
init(lookupValues: Set<LookupValue>) {
var d = [String: Set<String>]()
for lookupValue in lookupValues {
let objectID = lookupValue.objectID
let relatedObjectID: String = lookupValue.relatedObjectID
if d[objectID] == nil {
d[objectID] = Set([relatedObjectID])
}
else {
d[objectID]!.insert(relatedObjectID)
}
}
self.init(dictionary: d)
}
func objectIDs() -> Set<String> {
return Set(dictionary.keys)
}
func relatedObjectIDs() -> Set<String> {
var ids = Set<String>()
for (_, relatedObjectIDs) in dictionary {
ids.formUnion(relatedObjectIDs)
}
return ids
}
subscript(_ objectID: String) -> Set<String>? {
return dictionary[objectID]
}
}
struct LookupValue: Hashable {
let objectID: String
let relatedObjectID: String
}

View File

@@ -0,0 +1,44 @@
//
// RelatedObjectsMap.swift
// RSDatabase
//
// Created by Brent Simmons on 9/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
// Map objectID to [DatabaseObject] (related objects).
// Its used as the return value for DatabaseLookupTable.fetchRelatedObjects.
public struct RelatedObjectsMap {
private let dictionary: [String: [DatabaseObject]] // objectID: relatedObjects
init(relatedObjects: [DatabaseObject], relatedObjectIDsMap: RelatedObjectIDsMap) {
var d = [String: [DatabaseObject]]()
let relatedObjectsDictionary = relatedObjects.dictionary()
for objectID in relatedObjectIDsMap.objectIDs() {
if let relatedObjectIDs = relatedObjectIDsMap[objectID] {
let relatedObjects = relatedObjectIDs.compactMap{ relatedObjectsDictionary[$0] }
if !relatedObjects.isEmpty {
d[objectID] = relatedObjects
}
}
}
self.dictionary = d
}
public func objectIDs() -> Set<String> {
return Set(dictionary.keys)
}
public subscript(_ objectID: String) -> [DatabaseObject]? {
return dictionary[objectID]
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,225 @@
//
// FMDatabaseAdditions.m
// fmdb
//
// Created by August Mueller on 10/30/05.
// Copyright 2005 Flying Meat Inc.. All rights reserved.
//
#import "FMDatabase.h"
#import "FMDatabaseAdditions.h"
#import "TargetConditionals.h"
#import "sqlite3.h"
@interface FMDatabase (PrivateStuff)
- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args;
@end
@implementation FMDatabase (FMDatabaseAdditions)
#define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \
va_list args; \
va_start(args, query); \
FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args]; \
va_end(args); \
if (![resultSet next]) { return (type)0; } \
type ret = [resultSet sel:0]; \
[resultSet close]; \
[resultSet setParentDB:nil]; \
return ret;
- (NSString*)stringForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex);
}
- (int)intForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex);
}
- (long)longForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex);
}
- (BOOL)boolForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex);
}
- (double)doubleForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex);
}
- (NSData*)dataForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex);
}
- (NSDate*)dateForQuery:(NSString*)query, ... {
RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex);
}
- (BOOL)tableExists:(NSString*)tableName {
tableName = [tableName lowercaseString];
FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName];
//if at least one next exists, table exists
BOOL returnBool = [rs next];
//close and free object
[rs close];
return returnBool;
}
/*
get table with list of tables: result 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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

View 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

View File

@@ -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"

View File

@@ -0,0 +1,8 @@
//
// File.swift
// RSDatabase
//
// Created by Brent Simmons on 11/10/24.
//
import Foundation