├── .github └── workflows │ └── build.yml ├── .gitignore ├── .swiftpm └── xcode │ └── package.xcworkspace │ └── contents.xcworkspacedata ├── LICENSE ├── Package.swift ├── README.md ├── Sources ├── RSDatabase │ ├── Database.swift │ ├── DatabaseObject.swift │ ├── DatabaseObjectCache.swift │ ├── DatabaseQueue.swift │ ├── DatabaseTable.swift │ └── Related Objects │ │ ├── DatabaseLookupTable.swift │ │ ├── DatabaseRelatedObjectsTable.swift │ │ ├── RelatedObjectIDsMap.swift │ │ └── RelatedObjectsMap.swift └── RSDatabaseObjC │ ├── FMDatabase+RSExtras.h │ ├── FMDatabase+RSExtras.m │ ├── FMDatabase.h │ ├── FMDatabase.m │ ├── FMDatabaseAdditions.h │ ├── FMDatabaseAdditions.m │ ├── FMResultSet+RSExtras.h │ ├── FMResultSet+RSExtras.m │ ├── FMResultSet.h │ ├── FMResultSet.m │ ├── NSString+RSDatabase.h │ ├── NSString+RSDatabase.m │ └── include │ └── RSDatabaseObjC.h └── Tests └── RSDatabaseTests └── ODBTests.swift /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: macOS-latest 9 | 10 | steps: 11 | - uses: maxim-lobanov/setup-xcode@v1 12 | with: 13 | xcode-version: latest-stable 14 | 15 | - name: Checkout Project 16 | uses: actions/checkout@v1 17 | 18 | - name: Run Build 19 | run: swift build 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | .build/ 7 | build/ 8 | DerivedData/ 9 | 10 | ## Various settings 11 | *.pbxuser 12 | !default.pbxuser 13 | *.mode1v3 14 | !default.mode1v3 15 | *.mode2v3 16 | !default.mode2v3 17 | *.perspectivev3 18 | !default.perspectivev3 19 | xcuserdata/ 20 | 21 | ## Other 22 | *.moved-aside 23 | *.xcuserstate 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | *.ipa 28 | *.dSYM.zip 29 | *.dSYM 30 | 31 | # CocoaPods 32 | # 33 | # We recommend against adding the Pods directory to your .gitignore. However 34 | # you should judge for yourself, the pros and cons are mentioned at: 35 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 36 | # 37 | # Pods/ 38 | 39 | # Carthage 40 | # 41 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 42 | # Carthage/Checkouts 43 | 44 | Carthage/Build 45 | 46 | # fastlane 47 | # 48 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 49 | # screenshots whenever they are needed. 50 | # For more information about the recommended setup visit: 51 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 52 | 53 | fastlane/report.xml 54 | fastlane/screenshots 55 | 56 | #Code Injection 57 | # 58 | # After new code Injection tools there's a generated folder /iOSInjectionProject 59 | # https://github.com/johnno1962/injectionforxcode 60 | 61 | iOSInjectionProject/ 62 | -------------------------------------------------------------------------------- /.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 brentsimmons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.9 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "RSDatabase", 7 | platforms: [.macOS(.v10_15), .iOS(.v13)], 8 | products: [ 9 | .library( 10 | name: "RSDatabase", 11 | type: .dynamic, 12 | targets: ["RSDatabase"]), 13 | .library( 14 | name: "RSDatabaseObjC", 15 | type: .dynamic, 16 | targets: ["RSDatabaseObjC"]), 17 | ], 18 | dependencies: [ 19 | ], 20 | targets: [ 21 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 22 | // Targets can depend on other targets in this package, and on products in packages this package depends on. 23 | .target( 24 | name: "RSDatabase", 25 | dependencies: ["RSDatabaseObjC"] 26 | ), 27 | .target( 28 | name: "RSDatabaseObjC", 29 | dependencies: [] 30 | ), 31 | .testTarget( 32 | name: "RSDatabaseTests", 33 | dependencies: ["RSDatabase"] 34 | ), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RSDatabase 2 | This is utility code for using SQLite via FMDB. It’s not a persistence framework — it’s lower-level. 3 | 4 | It builds as a couple frameworks — one for Mac, one for iOS. 5 | 6 | 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. 7 | 8 | #### What to look at 9 | 10 | The main thing is `RSDatabaseQueue`, which allows you to talk to SQLite-via-FMDB using a serial queue. 11 | 12 | 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. 13 | -------------------------------------------------------------------------------- /Sources/RSDatabase/Database.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Database.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 12/15/19. 6 | // Copyright © 2019 Brent Simmons. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RSDatabaseObjC 11 | 12 | public enum DatabaseError: Error { 13 | case isSuspended // On iOS, to support background refreshing, a database may be suspended. 14 | } 15 | 16 | /// Result type that provides an FMDatabase or a DatabaseError. 17 | public typealias DatabaseResult = Result 18 | 19 | /// Block that executes database code or handles DatabaseQueueError. 20 | public typealias DatabaseBlock = (DatabaseResult) -> Void 21 | 22 | /// Completion block that provides an optional DatabaseError. 23 | public typealias DatabaseCompletionBlock = (DatabaseError?) -> Void 24 | 25 | /// Result type for fetching an Int or getting a DatabaseError. 26 | public typealias DatabaseIntResult = Result 27 | 28 | /// Completion block for DatabaseIntResult. 29 | public typealias DatabaseIntCompletionBlock = (DatabaseIntResult) -> Void 30 | 31 | // MARK: - Extensions 32 | 33 | public extension DatabaseResult { 34 | /// Convenience for getting the database from a DatabaseResult. 35 | var database: FMDatabase? { 36 | switch self { 37 | case .success(let database): 38 | return database 39 | case .failure: 40 | return nil 41 | } 42 | } 43 | 44 | /// Convenience for getting the error from a DatabaseResult. 45 | var error: DatabaseError? { 46 | switch self { 47 | case .success: 48 | return nil 49 | case .failure(let error): 50 | return error 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/RSDatabase/DatabaseObject.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseObject.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 8/7/17. 6 | // Copyright © 2017 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias DatabaseDictionary = [String: Any] 12 | 13 | public protocol DatabaseObject { 14 | 15 | var databaseID: String { get } 16 | 17 | func databaseDictionary() -> DatabaseDictionary? 18 | 19 | func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? 20 | } 21 | 22 | public extension DatabaseObject { 23 | 24 | func relatedObjectsWithName(_ name: String) -> [DatabaseObject]? { 25 | 26 | return nil 27 | } 28 | } 29 | 30 | extension Array where Element == DatabaseObject { 31 | 32 | func dictionary() -> [String: DatabaseObject] { 33 | 34 | var d = [String: DatabaseObject]() 35 | for object in self { 36 | d[object.databaseID] = object 37 | } 38 | return d 39 | } 40 | 41 | func databaseIDs() -> Set { 42 | 43 | return Set(self.map { $0.databaseID }) 44 | } 45 | 46 | func includesObjectWithDatabaseID(_ databaseID: String) -> Bool { 47 | 48 | for object in self { 49 | if object.databaseID == databaseID { 50 | return true 51 | } 52 | } 53 | return false 54 | } 55 | 56 | func databaseDictionaries() -> [DatabaseDictionary]? { 57 | 58 | let dictionaries = self.compactMap{ $0.databaseDictionary() } 59 | return dictionaries.isEmpty ? nil : dictionaries 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/RSDatabase/DatabaseObjectCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseObjectCache.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 9/12/17. 6 | // Copyright © 2017 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class DatabaseObjectCache { 12 | 13 | private var d = [String: DatabaseObject]() 14 | 15 | public init() { 16 | // 17 | } 18 | public func add(_ databaseObjects: [DatabaseObject]) { 19 | 20 | for databaseObject in databaseObjects { 21 | self[databaseObject.databaseID] = databaseObject 22 | } 23 | } 24 | 25 | public subscript(_ databaseID: String) -> DatabaseObject? { 26 | get { 27 | return d[databaseID] 28 | } 29 | set { 30 | d[databaseID] = newValue 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Sources/RSDatabase/DatabaseQueue.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseQueue.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 11/13/19. 6 | // Copyright © 2019 Brent Simmons. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import SQLite3 11 | import RSDatabaseObjC 12 | 13 | /// Manage a serial queue and a SQLite database. 14 | /// 15 | /// Important note: on iOS, the queue can be suspended 16 | /// in order to support background refreshing. 17 | public final class DatabaseQueue { 18 | 19 | /// Check to see if the queue is suspended. Read-only. 20 | /// Calling suspend() and resume() will change the value of this property. 21 | /// This will return true only on iOS — on macOS it’s always false. 22 | public var isSuspended: Bool { 23 | #if os(iOS) 24 | return _isSuspended 25 | #else 26 | return false 27 | #endif 28 | } 29 | 30 | private var _isSuspended = true 31 | private var isCallingDatabase = false 32 | private let database: FMDatabase 33 | private let databasePath: String 34 | private let serialDispatchQueue: DispatchQueue 35 | private let targetDispatchQueue: DispatchQueue 36 | #if os(iOS) 37 | private let databaseLock = NSLock() 38 | #endif 39 | 40 | /// When init returns, the database will not be suspended: it will be ready for database calls. 41 | public init(databasePath: String) { 42 | self.serialDispatchQueue = DispatchQueue(label: "DatabaseQueue (Serial) - \(databasePath)", attributes: .initiallyInactive) 43 | self.targetDispatchQueue = DispatchQueue(label: "DatabaseQueue (Target) - \(databasePath)") 44 | self.serialDispatchQueue.setTarget(queue: self.targetDispatchQueue) 45 | self.serialDispatchQueue.activate() 46 | 47 | self.databasePath = databasePath 48 | self.database = FMDatabase(path: databasePath)! 49 | openDatabase() 50 | _isSuspended = false 51 | } 52 | 53 | // MARK: - Suspend and Resume 54 | 55 | /// Close the SQLite database and don’t allow database calls until resumed. 56 | /// This is for iOS, where we need to close the SQLite database in some conditions. 57 | /// 58 | /// After calling suspend, if you call into the database before calling resume, 59 | /// your code will not run, and runInDatabaseSync and runInTransactionSync will 60 | /// both throw DatabaseQueueError.isSuspended. 61 | /// 62 | /// On Mac, suspend() and resume() are no-ops, since there isn’t a need for them. 63 | public func suspend() { 64 | #if os(iOS) 65 | guard !_isSuspended else { 66 | return 67 | } 68 | 69 | _isSuspended = true 70 | 71 | serialDispatchQueue.suspend() 72 | targetDispatchQueue.async { 73 | self.lockDatabase() 74 | self.database.close() 75 | self.unlockDatabase() 76 | DispatchQueue.main.async { 77 | self.serialDispatchQueue.resume() 78 | } 79 | } 80 | #endif 81 | } 82 | 83 | /// Open the SQLite database. Allow database calls again. 84 | /// This is also for iOS only. 85 | public func resume() { 86 | #if os(iOS) 87 | guard _isSuspended else { 88 | return 89 | } 90 | 91 | serialDispatchQueue.suspend() 92 | targetDispatchQueue.sync { 93 | if _isSuspended { 94 | lockDatabase() 95 | openDatabase() 96 | unlockDatabase() 97 | _isSuspended = false 98 | } 99 | } 100 | serialDispatchQueue.resume() 101 | #endif 102 | } 103 | 104 | // MARK: - Make Database Calls 105 | 106 | /// Run a DatabaseBlock synchronously. This call will block the main thread 107 | /// potentially for a while, depending on how long it takes to execute 108 | /// the DatabaseBlock *and* depending on how many other calls have been 109 | /// scheduled on the queue. Use sparingly — prefer async versions. 110 | public func runInDatabaseSync(_ databaseBlock: DatabaseBlock) { 111 | serialDispatchQueue.sync { 112 | self._runInDatabase(self.database, databaseBlock, false) 113 | } 114 | } 115 | 116 | /// Run a DatabaseBlock asynchronously. 117 | public func runInDatabase(_ databaseBlock: @escaping DatabaseBlock) { 118 | serialDispatchQueue.async { 119 | self._runInDatabase(self.database, databaseBlock, false) 120 | } 121 | } 122 | 123 | /// Run a DatabaseBlock wrapped in a transaction synchronously. 124 | /// Transactions help performance significantly when updating the database. 125 | /// Nevertheless, it’s best to avoid this because it will block the main thread — 126 | /// prefer the async `runInTransaction` instead. 127 | public func runInTransactionSync(_ databaseBlock: @escaping DatabaseBlock) { 128 | serialDispatchQueue.sync { 129 | self._runInDatabase(self.database, databaseBlock, true) 130 | } 131 | } 132 | 133 | /// Run a DatabaseBlock wrapped in a transaction asynchronously. 134 | /// Transactions help performance significantly when updating the database. 135 | public func runInTransaction(_ databaseBlock: @escaping DatabaseBlock) { 136 | serialDispatchQueue.async { 137 | self._runInDatabase(self.database, databaseBlock, true) 138 | } 139 | } 140 | 141 | /// Run all the lines that start with "create". 142 | /// Use this to create tables, indexes, etc. 143 | public func runCreateStatements(_ statements: String) throws { 144 | var error: DatabaseError? = nil 145 | runInDatabaseSync { result in 146 | switch result { 147 | case .success(let database): 148 | statements.enumerateLines { (line, stop) in 149 | if line.lowercased().hasPrefix("create") { 150 | database.executeStatements(line) 151 | } 152 | stop = false 153 | } 154 | case .failure(let databaseError): 155 | error = databaseError 156 | } 157 | } 158 | if let error = error { 159 | throw(error) 160 | } 161 | } 162 | 163 | /// Compact the database. This should be done from time to time — 164 | /// weekly-ish? — to keep up the performance level of a database. 165 | /// Generally a thing to do at startup, if it’s been a while 166 | /// since the last vacuum() call. You almost certainly want to call 167 | /// vacuumIfNeeded instead. 168 | public func vacuum() { 169 | runInDatabase { result in 170 | result.database?.executeStatements("vacuum;") 171 | } 172 | } 173 | 174 | /// Vacuum the database if it’s been more than `daysBetweenVacuums` since the last vacuum. 175 | /// Normally you would call this right after initing a DatabaseQueue. 176 | /// 177 | /// - Returns: true if database will be vacuumed. 178 | @discardableResult 179 | public func vacuumIfNeeded(daysBetweenVacuums: Int) -> Bool { 180 | let defaultsKey = "DatabaseQueue-LastVacuumDate-\(databasePath)" 181 | let minimumVacuumInterval = TimeInterval(daysBetweenVacuums * (60 * 60 * 24)) // Doesn’t have to be precise 182 | let now = Date() 183 | let cutoffDate = now - minimumVacuumInterval 184 | if let lastVacuumDate = UserDefaults.standard.object(forKey: defaultsKey) as? Date { 185 | if lastVacuumDate < cutoffDate { 186 | vacuum() 187 | UserDefaults.standard.set(now, forKey: defaultsKey) 188 | return true 189 | } 190 | return false 191 | } 192 | 193 | // Never vacuumed — almost certainly a new database. 194 | // Just set the LastVacuumDate pref to now and skip vacuuming. 195 | UserDefaults.standard.set(now, forKey: defaultsKey) 196 | return false 197 | } 198 | } 199 | 200 | private extension DatabaseQueue { 201 | 202 | func lockDatabase() { 203 | #if os(iOS) 204 | databaseLock.lock() 205 | #endif 206 | } 207 | 208 | func unlockDatabase() { 209 | #if os(iOS) 210 | databaseLock.unlock() 211 | #endif 212 | } 213 | 214 | func _runInDatabase(_ database: FMDatabase, _ databaseBlock: DatabaseBlock, _ useTransaction: Bool) { 215 | lockDatabase() 216 | defer { 217 | unlockDatabase() 218 | } 219 | 220 | precondition(!isCallingDatabase) 221 | 222 | isCallingDatabase = true 223 | autoreleasepool { 224 | if _isSuspended { 225 | databaseBlock(.failure(.isSuspended)) 226 | } 227 | else { 228 | if useTransaction { 229 | database.beginTransaction() 230 | } 231 | databaseBlock(.success(database)) 232 | if useTransaction { 233 | database.commit() 234 | } 235 | } 236 | } 237 | isCallingDatabase = false 238 | } 239 | 240 | func openDatabase() { 241 | database.open() 242 | database.executeStatements("PRAGMA synchronous = 1;") 243 | database.setShouldCacheStatements(true) 244 | } 245 | } 246 | 247 | -------------------------------------------------------------------------------- /Sources/RSDatabase/DatabaseTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseTable.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 7/16/17. 6 | // Copyright © 2017 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RSDatabaseObjC 11 | 12 | public protocol DatabaseTable { 13 | 14 | var name: String { get } 15 | } 16 | 17 | public extension DatabaseTable { 18 | 19 | // MARK: Fetching 20 | 21 | func selectRowsWhere(key: String, equals value: Any, in database: FMDatabase) -> FMResultSet? { 22 | 23 | return database.rs_selectRowsWhereKey(key, equalsValue: value, tableName: name) 24 | } 25 | 26 | func selectSingleRowWhere(key: String, equals value: Any, in database: FMDatabase) -> FMResultSet? { 27 | 28 | return database.rs_selectSingleRowWhereKey(key, equalsValue: value, tableName: name) 29 | } 30 | 31 | func selectRowsWhere(key: String, inValues values: [Any], in database: FMDatabase) -> FMResultSet? { 32 | 33 | if values.isEmpty { 34 | return nil 35 | } 36 | return database.rs_selectRowsWhereKey(key, inValues: values, tableName: name) 37 | } 38 | 39 | // MARK: Deleting 40 | 41 | func deleteRowsWhere(key: String, equalsAnyValue values: [Any], in database: FMDatabase) { 42 | 43 | if values.isEmpty { 44 | return 45 | } 46 | database.rs_deleteRowsWhereKey(key, inValues: values, tableName: name) 47 | } 48 | 49 | // MARK: Updating 50 | 51 | func updateRowsWithValue(_ value: Any, valueKey: String, whereKey: String, matches: [Any], database: FMDatabase) { 52 | 53 | let _ = database.rs_updateRows(withValue: value, valueKey: valueKey, whereKey: whereKey, inValues: matches, tableName: self.name) 54 | } 55 | 56 | func updateRowsWithDictionary(_ dictionary: DatabaseDictionary, whereKey: String, matches: Any, database: FMDatabase) { 57 | 58 | let _ = database.rs_updateRows(with: dictionary, whereKey: whereKey, equalsValue: matches, tableName: self.name) 59 | } 60 | 61 | // MARK: Saving 62 | 63 | func insertRows(_ dictionaries: [DatabaseDictionary], insertType: RSDatabaseInsertType, in database: FMDatabase) { 64 | 65 | dictionaries.forEach { (oneDictionary) in 66 | let _ = database.rs_insertRow(with: oneDictionary, insertType: insertType, tableName: self.name) 67 | } 68 | } 69 | 70 | func insertRow(_ rowDictionary: DatabaseDictionary, insertType: RSDatabaseInsertType, in database: FMDatabase) { 71 | 72 | insertRows([rowDictionary], insertType: insertType, in: database) 73 | } 74 | 75 | // MARK: Counting 76 | 77 | func numberWithCountResultSet(_ resultSet: FMResultSet) -> Int { 78 | 79 | guard resultSet.next() else { 80 | return 0 81 | } 82 | return Int(resultSet.int(forColumnIndex: 0)) 83 | } 84 | 85 | func numberWithSQLAndParameters(_ sql: String, _ parameters: [Any], in database: FMDatabase) -> Int { 86 | 87 | if let resultSet = database.executeQuery(sql, withArgumentsIn: parameters) { 88 | return numberWithCountResultSet(resultSet) 89 | } 90 | return 0 91 | } 92 | 93 | // MARK: Mapping 94 | 95 | func mapResultSet(_ resultSet: FMResultSet, _ completion: (_ resultSet: FMResultSet) -> T?) -> [T] { 96 | 97 | var objects = [T]() 98 | while resultSet.next() { 99 | if let obj = completion(resultSet) { 100 | objects += [obj] 101 | } 102 | } 103 | return objects 104 | } 105 | 106 | // MARK: Columns 107 | 108 | func containsColumn(_ columnName: String, in database: FMDatabase) -> Bool { 109 | if let resultSet = database.executeQuery("select * from \(name) limit 1;", withArgumentsIn: nil) { 110 | if let columnMap = resultSet.columnNameToIndexMap { 111 | if let _ = columnMap[columnName.lowercased()] { 112 | return true 113 | } 114 | } 115 | } 116 | return false 117 | } 118 | } 119 | 120 | public extension FMResultSet { 121 | 122 | func compactMap(_ completion: (_ row: FMResultSet) -> T?) -> [T] { 123 | 124 | var objects = [T]() 125 | while next() { 126 | if let obj = completion(self) { 127 | objects += [obj] 128 | } 129 | } 130 | close() 131 | return objects 132 | } 133 | 134 | func mapToSet(_ completion: (_ row: FMResultSet) -> T?) -> Set { 135 | 136 | return Set(compactMap(completion)) 137 | } 138 | } 139 | 140 | -------------------------------------------------------------------------------- /Sources/RSDatabase/Related Objects/DatabaseLookupTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseLookupTable.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 8/5/17. 6 | // Copyright © 2017 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RSDatabaseObjC 11 | 12 | // Implement a lookup table for a many-to-many relationship. 13 | // Example: CREATE TABLE if not EXISTS authorLookup (authorID TEXT NOT NULL, articleID TEXT NOT NULL, PRIMARY KEY(authorID, articleID)); 14 | // articleID is objectID; authorID is relatedObjectID. 15 | 16 | public final class DatabaseLookupTable { 17 | 18 | private let name: String 19 | private let objectIDKey: String 20 | private let relatedObjectIDKey: String 21 | private let relationshipName: String 22 | private let relatedTable: DatabaseRelatedObjectsTable 23 | private var objectIDsWithNoRelatedObjects = Set() 24 | 25 | public init(name: String, objectIDKey: String, relatedObjectIDKey: String, relatedTable: DatabaseRelatedObjectsTable, relationshipName: String) { 26 | 27 | self.name = name 28 | self.objectIDKey = objectIDKey 29 | self.relatedObjectIDKey = relatedObjectIDKey 30 | self.relatedTable = relatedTable 31 | self.relationshipName = relationshipName 32 | } 33 | 34 | public func fetchRelatedObjects(for objectIDs: Set, in database: FMDatabase) -> RelatedObjectsMap? { 35 | 36 | let objectIDsThatMayHaveRelatedObjects = objectIDs.subtracting(objectIDsWithNoRelatedObjects) 37 | if objectIDsThatMayHaveRelatedObjects.isEmpty { 38 | return nil 39 | } 40 | 41 | guard let relatedObjectIDsMap = fetchRelatedObjectIDsMap(objectIDsThatMayHaveRelatedObjects, database) else { 42 | objectIDsWithNoRelatedObjects.formUnion(objectIDsThatMayHaveRelatedObjects) 43 | return nil 44 | } 45 | 46 | if let relatedObjects = fetchRelatedObjectsWithIDs(relatedObjectIDsMap.relatedObjectIDs(), database) { 47 | 48 | let relatedObjectsMap = RelatedObjectsMap(relatedObjects: relatedObjects, relatedObjectIDsMap: relatedObjectIDsMap) 49 | 50 | let objectIDsWithNoFetchedRelatedObjects = objectIDsThatMayHaveRelatedObjects.subtracting(relatedObjectsMap.objectIDs()) 51 | objectIDsWithNoRelatedObjects.formUnion(objectIDsWithNoFetchedRelatedObjects) 52 | 53 | return relatedObjectsMap 54 | } 55 | 56 | return nil 57 | } 58 | 59 | public func saveRelatedObjects(for objects: [DatabaseObject], in database: FMDatabase) { 60 | 61 | var objectsWithNoRelationships = [DatabaseObject]() 62 | var objectsWithRelationships = [DatabaseObject]() 63 | 64 | for object in objects { 65 | if let relatedObjects = object.relatedObjectsWithName(relationshipName), !relatedObjects.isEmpty { 66 | objectsWithRelationships += [object] 67 | } 68 | else { 69 | objectsWithNoRelationships += [object] 70 | } 71 | } 72 | 73 | removeRelationships(for: objectsWithNoRelationships, database) 74 | updateRelationships(for: objectsWithRelationships, database) 75 | 76 | objectIDsWithNoRelatedObjects.formUnion(objectsWithNoRelationships.databaseIDs()) 77 | objectIDsWithNoRelatedObjects.subtract(objectsWithRelationships.databaseIDs()) 78 | } 79 | } 80 | 81 | // MARK: - Private 82 | 83 | private extension DatabaseLookupTable { 84 | 85 | // MARK: Removing 86 | 87 | func removeRelationships(for objects: [DatabaseObject], _ database: FMDatabase) { 88 | 89 | let objectIDs = objects.databaseIDs() 90 | let objectIDsToRemove = objectIDs.subtracting(objectIDsWithNoRelatedObjects) 91 | if objectIDsToRemove.isEmpty { 92 | return 93 | } 94 | 95 | database.rs_deleteRowsWhereKey(objectIDKey, inValues: Array(objectIDsToRemove), tableName: name) 96 | } 97 | 98 | func deleteLookups(for objectID: String, _ relatedObjectIDs: Set, _ database: FMDatabase) { 99 | 100 | guard !relatedObjectIDs.isEmpty else { 101 | assertionFailure("deleteLookups: expected non-empty relatedObjectIDs") 102 | return 103 | } 104 | 105 | // delete from authorLookup where articleID=? and authorID in (?,?,?) 106 | let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(relatedObjectIDs.count))! 107 | let sql = "delete from \(name) where \(objectIDKey)=? and \(relatedObjectIDKey) in \(placeholders)" 108 | 109 | let parameters: [Any] = [objectID] + Array(relatedObjectIDs) 110 | let _ = database.executeUpdate(sql, withArgumentsIn: parameters) 111 | } 112 | 113 | // MARK: Saving/Updating 114 | 115 | func updateRelationships(for objects: [DatabaseObject], _ database: FMDatabase) { 116 | 117 | if objects.isEmpty { 118 | return 119 | } 120 | 121 | if let lookupTable = fetchRelatedObjectIDsMap(objects.databaseIDs(), database) { 122 | for object in objects { 123 | syncRelatedObjectsAndLookupTable(object, lookupTable, database) 124 | } 125 | } 126 | 127 | // Save the actual related objects. 128 | 129 | let relatedObjectsToSave = uniqueArrayOfRelatedObjects(with: objects) 130 | if relatedObjectsToSave.isEmpty { 131 | assertionFailure("updateRelationships: expected relatedObjectsToSave would not be empty. This should be unreachable.") 132 | return 133 | } 134 | 135 | relatedTable.save(relatedObjectsToSave, in: database) 136 | } 137 | 138 | func uniqueArrayOfRelatedObjects(with objects: [DatabaseObject]) -> [DatabaseObject] { 139 | 140 | // Can’t create a Set, because we can’t make a Set, because protocol-conforming objects can’t be made Hashable or even Equatable. 141 | // 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. 142 | 143 | var relatedObjectsUniqueArray = [DatabaseObject]() 144 | for object in objects { 145 | guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else { 146 | assertionFailure("uniqueArrayOfRelatedObjects: expected every object to have related objects.") 147 | continue 148 | } 149 | for relatedObject in relatedObjects { 150 | if !relatedObjectsUniqueArray.includesObjectWithDatabaseID(relatedObject.databaseID) { 151 | relatedObjectsUniqueArray += [relatedObject] 152 | } 153 | } 154 | } 155 | return relatedObjectsUniqueArray 156 | } 157 | 158 | func syncRelatedObjectsAndLookupTable(_ object: DatabaseObject, _ lookupTable: RelatedObjectIDsMap, _ database: FMDatabase) { 159 | 160 | guard let relatedObjects = object.relatedObjectsWithName(relationshipName) else { 161 | assertionFailure("syncRelatedObjectsAndLookupTable should be called only on objects with related objects.") 162 | return 163 | } 164 | 165 | let relatedObjectIDs = relatedObjects.databaseIDs() 166 | let lookupTableRelatedObjectIDs = lookupTable[object.databaseID] ?? Set() 167 | 168 | let relatedObjectIDsToDelete = lookupTableRelatedObjectIDs.subtracting(relatedObjectIDs) 169 | if !relatedObjectIDsToDelete.isEmpty { 170 | deleteLookups(for: object.databaseID, relatedObjectIDsToDelete, database) 171 | } 172 | 173 | let relatedObjectIDsToSave = relatedObjectIDs.subtracting(lookupTableRelatedObjectIDs) 174 | if !relatedObjectIDsToSave.isEmpty { 175 | saveLookups(for: object.databaseID, relatedObjectIDsToSave, database) 176 | } 177 | } 178 | 179 | func saveLookups(for objectID: String, _ relatedObjectIDs: Set, _ database: FMDatabase) { 180 | 181 | for relatedObjectID in relatedObjectIDs { 182 | let d: [NSObject: Any] = [(objectIDKey as NSString): objectID, (relatedObjectIDKey as NSString): relatedObjectID] 183 | let _ = database.rs_insertRow(with: d, insertType: .orIgnore, tableName: name) 184 | } 185 | } 186 | 187 | // MARK: Fetching 188 | 189 | func fetchRelatedObjectsWithIDs(_ relatedObjectIDs: Set, _ database: FMDatabase) -> [DatabaseObject]? { 190 | 191 | guard let relatedObjects = relatedTable.fetchObjectsWithIDs(relatedObjectIDs, in: database), !relatedObjects.isEmpty else { 192 | return nil 193 | } 194 | return relatedObjects 195 | } 196 | 197 | func fetchRelatedObjectIDsMap(_ objectIDs: Set, _ database: FMDatabase) -> RelatedObjectIDsMap? { 198 | 199 | guard let lookupValues = fetchLookupValues(objectIDs, database) else { 200 | return nil 201 | } 202 | return RelatedObjectIDsMap(lookupValues: lookupValues) 203 | } 204 | 205 | func fetchLookupValues(_ objectIDs: Set, _ database: FMDatabase) -> Set? { 206 | 207 | guard !objectIDs.isEmpty, let resultSet = database.rs_selectRowsWhereKey(objectIDKey, inValues: Array(objectIDs), tableName: name) else { 208 | return nil 209 | } 210 | return lookupValuesWithResultSet(resultSet) 211 | } 212 | 213 | func lookupValuesWithResultSet(_ resultSet: FMResultSet) -> Set { 214 | 215 | return resultSet.mapToSet(lookupValueWithRow) 216 | } 217 | 218 | func lookupValueWithRow(_ row: FMResultSet) -> LookupValue? { 219 | 220 | guard let objectID = row.string(forColumn: objectIDKey) else { 221 | return nil 222 | } 223 | guard let relatedObjectID = row.string(forColumn: relatedObjectIDKey) else { 224 | return nil 225 | } 226 | return LookupValue(objectID: objectID, relatedObjectID: relatedObjectID) 227 | } 228 | } 229 | 230 | -------------------------------------------------------------------------------- /Sources/RSDatabase/Related Objects/DatabaseRelatedObjectsTable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DatabaseRelatedObjectsTable.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 9/2/17. 6 | // Copyright © 2017 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import RSDatabaseObjC 11 | 12 | // Protocol for a database table for related objects — authors and attachments in NetNewsWire, for instance. 13 | 14 | public protocol DatabaseRelatedObjectsTable: DatabaseTable { 15 | 16 | var databaseIDKey: String { get} 17 | var cache: DatabaseObjectCache { get } 18 | 19 | func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject]? 20 | func objectsWithResultSet(_ resultSet: FMResultSet) -> [DatabaseObject] 21 | func objectWithRow(_ row: FMResultSet) -> DatabaseObject? 22 | 23 | func save(_ objects: [DatabaseObject], in database: FMDatabase) 24 | } 25 | 26 | public extension DatabaseRelatedObjectsTable { 27 | 28 | // MARK: Default implementations 29 | 30 | func fetchObjectsWithIDs(_ databaseIDs: Set, in database: FMDatabase) -> [DatabaseObject]? { 31 | 32 | if databaseIDs.isEmpty { 33 | return nil 34 | } 35 | 36 | var cachedObjects = [DatabaseObject]() 37 | var databaseIDsToFetch = Set() 38 | 39 | for databaseID in databaseIDs { 40 | if let cachedObject = cache[databaseID] { 41 | cachedObjects += [cachedObject] 42 | } 43 | else { 44 | databaseIDsToFetch.insert(databaseID) 45 | } 46 | } 47 | 48 | if databaseIDsToFetch.isEmpty { 49 | return cachedObjects 50 | } 51 | 52 | guard let resultSet = selectRowsWhere(key: databaseIDKey, inValues: Array(databaseIDsToFetch), in: database) else { 53 | return cachedObjects 54 | } 55 | 56 | let fetchedDatabaseObjects = objectsWithResultSet(resultSet) 57 | cache.add(fetchedDatabaseObjects) 58 | 59 | return cachedObjects + fetchedDatabaseObjects 60 | } 61 | 62 | func objectsWithResultSet(_ resultSet: FMResultSet) -> [DatabaseObject] { 63 | 64 | return resultSet.compactMap(objectWithRow) 65 | } 66 | 67 | func save(_ objects: [DatabaseObject], in database: FMDatabase) { 68 | 69 | // Objects in cache must already exist in database. Filter them out. 70 | let objectsToSave = objects.filter { (object) -> Bool in 71 | if let _ = cache[object.databaseID] { 72 | return false 73 | } 74 | return true 75 | } 76 | 77 | cache.add(objectsToSave) 78 | if let databaseDictionaries = objectsToSave.databaseDictionaries() { 79 | insertRows(databaseDictionaries, insertType: .orIgnore, in: database) 80 | } 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /Sources/RSDatabase/Related Objects/RelatedObjectIDsMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelatedObjectIDsMap.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 9/10/17. 6 | // Copyright © 2017 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Maps objectIDs to Set where the Strings are relatedObjectIDs. 12 | 13 | struct RelatedObjectIDsMap { 14 | 15 | private let dictionary: [String: Set] // objectID: Set 16 | 17 | init(dictionary: [String: Set]) { 18 | 19 | self.dictionary = dictionary 20 | } 21 | 22 | init(lookupValues: Set) { 23 | 24 | var d = [String: Set]() 25 | 26 | for lookupValue in lookupValues { 27 | let objectID = lookupValue.objectID 28 | let relatedObjectID: String = lookupValue.relatedObjectID 29 | if d[objectID] == nil { 30 | d[objectID] = Set([relatedObjectID]) 31 | } 32 | else { 33 | d[objectID]!.insert(relatedObjectID) 34 | } 35 | } 36 | 37 | self.init(dictionary: d) 38 | } 39 | 40 | func objectIDs() -> Set { 41 | 42 | return Set(dictionary.keys) 43 | } 44 | 45 | func relatedObjectIDs() -> Set { 46 | 47 | var ids = Set() 48 | for (_, relatedObjectIDs) in dictionary { 49 | ids.formUnion(relatedObjectIDs) 50 | } 51 | return ids 52 | } 53 | 54 | subscript(_ objectID: String) -> Set? { 55 | return dictionary[objectID] 56 | } 57 | } 58 | 59 | struct LookupValue: Hashable { 60 | 61 | let objectID: String 62 | let relatedObjectID: String 63 | } 64 | -------------------------------------------------------------------------------- /Sources/RSDatabase/Related Objects/RelatedObjectsMap.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RelatedObjectsMap.swift 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 9/10/17. 6 | // Copyright © 2017 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Map objectID to [DatabaseObject] (related objects). 12 | // It’s used as the return value for DatabaseLookupTable.fetchRelatedObjects. 13 | 14 | public struct RelatedObjectsMap { 15 | 16 | private let dictionary: [String: [DatabaseObject]] // objectID: relatedObjects 17 | 18 | init(relatedObjects: [DatabaseObject], relatedObjectIDsMap: RelatedObjectIDsMap) { 19 | 20 | var d = [String: [DatabaseObject]]() 21 | let relatedObjectsDictionary = relatedObjects.dictionary() 22 | 23 | for objectID in relatedObjectIDsMap.objectIDs() { 24 | 25 | if let relatedObjectIDs = relatedObjectIDsMap[objectID] { 26 | let relatedObjects = relatedObjectIDs.compactMap{ relatedObjectsDictionary[$0] } 27 | if !relatedObjects.isEmpty { 28 | d[objectID] = relatedObjects 29 | } 30 | } 31 | } 32 | 33 | self.dictionary = d 34 | } 35 | 36 | public func objectIDs() -> Set { 37 | 38 | return Set(dictionary.keys) 39 | } 40 | 41 | public subscript(_ objectID: String) -> [DatabaseObject]? { 42 | return dictionary[objectID] 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMDatabase+RSExtras.h: -------------------------------------------------------------------------------- 1 | // 2 | // FMDatabase+QSKit.h 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 3/3/14. 6 | // Copyright (c) 2014 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "FMDatabase.h" 10 | 11 | @import Foundation; 12 | 13 | typedef NS_ENUM(NSInteger, RSDatabaseInsertType) { 14 | RSDatabaseInsertNormal, 15 | RSDatabaseInsertOrReplace, 16 | RSDatabaseInsertOrIgnore 17 | }; 18 | 19 | NS_ASSUME_NONNULL_BEGIN 20 | 21 | @interface FMDatabase (RSExtras) 22 | 23 | 24 | // Keys and table names are assumed to be trusted. Values are not. 25 | 26 | 27 | // delete from tableName where key in (?, ?, ?) 28 | 29 | - (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName; 30 | 31 | // delete from tableName where key=? 32 | 33 | - (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName; 34 | 35 | 36 | // select * from tableName where key in (?, ?, ?) 37 | 38 | - (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName; 39 | 40 | // select * from tableName where key = ? 41 | 42 | - (FMResultSet * _Nullable)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName; 43 | 44 | // select * from tableName where key = ? limit 1 45 | 46 | - (FMResultSet * _Nullable)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName; 47 | 48 | // select * from tableName 49 | 50 | - (FMResultSet * _Nullable)rs_selectAllRows:(NSString *)tableName; 51 | 52 | // select key from tableName; 53 | 54 | - (FMResultSet * _Nullable)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName; 55 | 56 | // select 1 from tableName where key = value limit 1; 57 | 58 | - (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName; 59 | 60 | // select 1 from tableName limit 1; 61 | 62 | - (BOOL)rs_tableIsEmpty:(NSString *)tableName; 63 | 64 | 65 | // update tableName set key1=?, key2=? where key = value 66 | 67 | - (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName; 68 | 69 | // update tableName set key1=?, key2=? where key in (?, ?, ?) 70 | 71 | - (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName; 72 | 73 | // update tableName set valueKey=? where where key in (?, ?, ?) 74 | 75 | - (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName; 76 | 77 | // insert (or replace, or ignore) into tablename (key1, key2) values (val1, val2) 78 | 79 | - (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName; 80 | 81 | @end 82 | 83 | NS_ASSUME_NONNULL_END 84 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMDatabase+RSExtras.m: -------------------------------------------------------------------------------- 1 | // 2 | // FMDatabase+QSKit.m 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 3/3/14. 6 | // Copyright (c) 2014 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "FMDatabase+RSExtras.h" 10 | #import "NSString+RSDatabase.h" 11 | 12 | 13 | #define LOG_SQL 0 14 | 15 | static void logSQL(NSString *sql) { 16 | #if LOG_SQL 17 | NSLog(@"sql: %@", sql); 18 | #endif 19 | } 20 | 21 | 22 | @implementation FMDatabase (RSExtras) 23 | 24 | 25 | #pragma mark - Deleting 26 | 27 | - (BOOL)rs_deleteRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName { 28 | 29 | if ([values count] < 1) { 30 | return YES; 31 | } 32 | 33 | NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count]; 34 | NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ in %@", tableName, key, placeholders]; 35 | logSQL(sql); 36 | 37 | return [self executeUpdate:sql withArgumentsInArray:values]; 38 | } 39 | 40 | 41 | - (BOOL)rs_deleteRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName { 42 | 43 | NSString *sql = [NSString stringWithFormat:@"delete from %@ where %@ = ?", tableName, key]; 44 | logSQL(sql); 45 | return [self executeUpdate:sql, value]; 46 | } 47 | 48 | 49 | #pragma mark - Selecting 50 | 51 | - (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key inValues:(NSArray *)values tableName:(NSString *)tableName { 52 | 53 | NSMutableString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ in ", tableName, key]; 54 | NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count]; 55 | [sql appendString:placeholders]; 56 | logSQL(sql); 57 | 58 | return [self executeQuery:sql withArgumentsInArray:values]; 59 | } 60 | 61 | 62 | - (FMResultSet *)rs_selectRowsWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName { 63 | 64 | NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ?", tableName, key]; 65 | logSQL(sql); 66 | return [self executeQuery:sql, value]; 67 | } 68 | 69 | 70 | - (FMResultSet *)rs_selectSingleRowWhereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName { 71 | 72 | NSString *sql = [NSMutableString stringWithFormat:@"select * from %@ where %@ = ? limit 1", tableName, key]; 73 | logSQL(sql); 74 | return [self executeQuery:sql, value]; 75 | } 76 | 77 | 78 | - (FMResultSet *)rs_selectAllRows:(NSString *)tableName { 79 | 80 | NSString *sql = [NSString stringWithFormat:@"select * from %@", tableName]; 81 | logSQL(sql); 82 | return [self executeQuery:sql]; 83 | } 84 | 85 | 86 | - (FMResultSet *)rs_selectColumnWithKey:(NSString *)key tableName:(NSString *)tableName { 87 | 88 | NSString *sql = [NSString stringWithFormat:@"select %@ from %@", key, tableName]; 89 | logSQL(sql); 90 | return [self executeQuery:sql]; 91 | } 92 | 93 | 94 | - (BOOL)rs_rowExistsWithValue:(id)value forKey:(NSString *)key tableName:(NSString *)tableName { 95 | 96 | NSString *sql = [NSString stringWithFormat:@"select 1 from %@ where %@ = ? limit 1;", tableName, key]; 97 | logSQL(sql); 98 | FMResultSet *rs = [self executeQuery:sql, value]; 99 | 100 | return [rs next]; 101 | } 102 | 103 | 104 | - (BOOL)rs_tableIsEmpty:(NSString *)tableName { 105 | 106 | NSString *sql = [NSString stringWithFormat:@"select 1 from %@ limit 1;", tableName]; 107 | logSQL(sql); 108 | FMResultSet *rs = [self executeQuery:sql]; 109 | 110 | BOOL isEmpty = YES; 111 | while ([rs next]) { 112 | isEmpty = NO; 113 | } 114 | return isEmpty; 115 | } 116 | 117 | 118 | #pragma mark - Updating 119 | 120 | - (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key equalsValue:(id)value tableName:(NSString *)tableName { 121 | 122 | return [self rs_updateRowsWithDictionary:d whereKey:key inValues:@[value] tableName:tableName]; 123 | } 124 | 125 | 126 | - (BOOL)rs_updateRowsWithDictionary:(NSDictionary *)d whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName { 127 | 128 | NSMutableArray *keys = [NSMutableArray new]; 129 | NSMutableArray *values = [NSMutableArray new]; 130 | 131 | [d enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { 132 | [keys addObject:key]; 133 | [values addObject:obj]; 134 | }]; 135 | 136 | NSString *keyPlaceholders = [NSString rs_SQLKeyPlaceholderPairsWithKeys:keys]; 137 | NSString *keyValuesPlaceholder = [NSString rs_SQLValueListWithPlaceholders:keyValues.count]; 138 | NSString *sql = [NSString stringWithFormat:@"update %@ set %@ where %@ in %@", tableName, keyPlaceholders, key, keyValuesPlaceholder]; 139 | 140 | NSMutableArray *parameters = values; 141 | [parameters addObjectsFromArray:keyValues]; 142 | logSQL(sql); 143 | 144 | return [self executeUpdate:sql withArgumentsInArray:parameters]; 145 | } 146 | 147 | 148 | - (BOOL)rs_updateRowsWithValue:(id)value valueKey:(NSString *)valueKey whereKey:(NSString *)key inValues:(NSArray *)keyValues tableName:(NSString *)tableName { 149 | 150 | NSDictionary *d = @{valueKey: value}; 151 | return [self rs_updateRowsWithDictionary:d whereKey:key inValues:keyValues tableName:tableName]; 152 | } 153 | 154 | 155 | #pragma mark - Saving 156 | 157 | - (BOOL)rs_insertRowWithDictionary:(NSDictionary *)d insertType:(RSDatabaseInsertType)insertType tableName:(NSString *)tableName { 158 | 159 | NSArray *keys = d.allKeys; 160 | NSArray *values = [d objectsForKeys:keys notFoundMarker:[NSNull null]]; 161 | 162 | NSString *sqlKeysList = [NSString rs_SQLKeysListWithArray:keys]; 163 | NSString *placeholders = [NSString rs_SQLValueListWithPlaceholders:values.count]; 164 | 165 | NSString *sqlBeginning = @"insert into "; 166 | if (insertType == RSDatabaseInsertOrReplace) { 167 | sqlBeginning = @"insert or replace into "; 168 | } 169 | else if (insertType == RSDatabaseInsertOrIgnore) { 170 | sqlBeginning = @"insert or ignore into "; 171 | } 172 | 173 | NSString *sql = [NSString stringWithFormat:@"%@ %@ %@ values %@", sqlBeginning, tableName, sqlKeysList, placeholders]; 174 | logSQL(sql); 175 | 176 | return [self executeUpdate:sql withArgumentsInArray:values]; 177 | } 178 | 179 | @end 180 | 181 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMDatabase.h: -------------------------------------------------------------------------------- 1 | #import 2 | //#import "sqlite3.h" 3 | #import "FMResultSet.h" 4 | //#import "FMDatabasePool.h" 5 | 6 | 7 | #if ! __has_feature(objc_arc) 8 | #define FMDBAutorelease(__v) ([__v autorelease]); 9 | #define FMDBReturnAutoreleased FMDBAutorelease 10 | 11 | #define FMDBRetain(__v) ([__v retain]); 12 | #define FMDBReturnRetained FMDBRetain 13 | 14 | #define FMDBRelease(__v) ([__v release]); 15 | 16 | #define FMDBDispatchQueueRelease(__v) (dispatch_release(__v)); 17 | #else 18 | // -fobjc-arc 19 | #define FMDBAutorelease(__v) 20 | #define FMDBReturnAutoreleased(__v) (__v) 21 | 22 | #define FMDBRetain(__v) 23 | #define FMDBReturnRetained(__v) (__v) 24 | 25 | #define FMDBRelease(__v) 26 | 27 | // If OS_OBJECT_USE_OBJC=1, then the dispatch objects will be treated like ObjC objects 28 | // and will participate in ARC. 29 | // See the section on "Dispatch Queues and Automatic Reference Counting" in "Grand Central Dispatch (GCD) Reference" for details. 30 | #if OS_OBJECT_USE_OBJC 31 | #define FMDBDispatchQueueRelease(__v) 32 | #else 33 | #define FMDBDispatchQueueRelease(__v) (dispatch_release(__v)); 34 | #endif 35 | #endif 36 | 37 | #if !__has_feature(objc_instancetype) 38 | #define instancetype id 39 | #endif 40 | 41 | 42 | typedef int(^FMDBExecuteStatementsCallbackBlock)(NSDictionary *resultsDictionary); 43 | 44 | 45 | /** A SQLite ([http://sqlite.org/](http://sqlite.org/)) Objective-C wrapper. 46 | 47 | ### Usage 48 | The three main classes in FMDB are: 49 | 50 | - `FMDatabase` - Represents a single SQLite database. Used for executing SQL statements. 51 | - `` - Represents the results of executing a query on an `FMDatabase`. 52 | - `` - If you want to perform queries and updates on multiple threads, you'll want to use this class. 53 | 54 | ### See also 55 | 56 | - `` - A pool of `FMDatabase` objects. 57 | - `` - A wrapper for `sqlite_stmt`. 58 | 59 | ### External links 60 | 61 | - [FMDB on GitHub](https://github.com/ccgus/fmdb) including introductory documentation 62 | - [SQLite web site](http://sqlite.org/) 63 | - [FMDB mailing list](http://groups.google.com/group/fmdb) 64 | - [SQLite FAQ](http://www.sqlite.org/faq.html) 65 | 66 | @warning Do not instantiate a single `FMDatabase` object and use it across multiple threads. Instead, use ``. 67 | 68 | */ 69 | 70 | #pragma clang diagnostic push 71 | #pragma clang diagnostic ignored "-Wobjc-interface-ivars" 72 | 73 | 74 | @interface FMDatabase : NSObject { 75 | 76 | NSString* _databasePath; 77 | BOOL _logsErrors; 78 | BOOL _crashOnErrors; 79 | BOOL _traceExecution; 80 | BOOL _checkedOut; 81 | BOOL _shouldCacheStatements; 82 | BOOL _isExecutingStatement; 83 | BOOL _inTransaction; 84 | NSTimeInterval _maxBusyRetryTimeInterval; 85 | NSTimeInterval _startBusyRetryTime; 86 | 87 | NSMutableDictionary *_cachedStatements; 88 | NSMutableSet *_openResultSets; 89 | NSMutableSet *_openFunctions; 90 | 91 | NSDateFormatter *_dateFormat; 92 | } 93 | 94 | ///----------------- 95 | /// @name Properties 96 | ///----------------- 97 | 98 | /** Whether should trace execution */ 99 | 100 | @property (atomic, assign) BOOL traceExecution; 101 | 102 | /** Whether checked out or not */ 103 | 104 | @property (atomic, assign) BOOL checkedOut; 105 | 106 | /** Crash on errors */ 107 | 108 | @property (atomic, assign) BOOL crashOnErrors; 109 | 110 | /** Logs errors */ 111 | 112 | @property (atomic, assign) BOOL logsErrors; 113 | 114 | /** Dictionary of cached statements */ 115 | 116 | @property (atomic, retain) NSMutableDictionary *cachedStatements; 117 | 118 | ///--------------------- 119 | /// @name Initialization 120 | ///--------------------- 121 | 122 | /** Create a `FMDatabase` object. 123 | 124 | An `FMDatabase` is created with a path to a SQLite database file. This path can be one of these three: 125 | 126 | 1. A file system path. The file does not have to exist on disk. If it does not exist, it is created for you. 127 | 2. An empty string (`@""`). An empty database is created at a temporary location. This database is deleted with the `FMDatabase` connection is closed. 128 | 3. `nil`. An in-memory database is created. This database will be destroyed with the `FMDatabase` connection is closed. 129 | 130 | For example, to create/open a database in your Mac OS X `tmp` folder: 131 | 132 | FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"]; 133 | 134 | Or, in iOS, you might open a database in the app's `Documents` directory: 135 | 136 | NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; 137 | NSString *dbPath = [docsPath stringByAppendingPathComponent:@"test.db"]; 138 | FMDatabase *db = [FMDatabase databaseWithPath:dbPath]; 139 | 140 | (For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [http://www.sqlite.org/inmemorydb.html](http://www.sqlite.org/inmemorydb.html)) 141 | 142 | @param inPath Path of database file 143 | 144 | @return `FMDatabase` object if successful; `nil` if failure. 145 | 146 | */ 147 | 148 | + (instancetype)databaseWithPath:(NSString*)inPath; 149 | 150 | /** Initialize a `FMDatabase` object. 151 | 152 | An `FMDatabase` is created with a path to a SQLite database file. This path can be one of these three: 153 | 154 | 1. A file system path. The file does not have to exist on disk. If it does not exist, it is created for you. 155 | 2. An empty string (`@""`). An empty database is created at a temporary location. This database is deleted with the `FMDatabase` connection is closed. 156 | 3. `nil`. An in-memory database is created. This database will be destroyed with the `FMDatabase` connection is closed. 157 | 158 | For example, to create/open a database in your Mac OS X `tmp` folder: 159 | 160 | FMDatabase *db = [FMDatabase databaseWithPath:@"/tmp/tmp.db"]; 161 | 162 | Or, in iOS, you might open a database in the app's `Documents` directory: 163 | 164 | NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0]; 165 | NSString *dbPath = [docsPath stringByAppendingPathComponent:@"test.db"]; 166 | FMDatabase *db = [FMDatabase databaseWithPath:dbPath]; 167 | 168 | (For more information on temporary and in-memory databases, read the sqlite documentation on the subject: [http://www.sqlite.org/inmemorydb.html](http://www.sqlite.org/inmemorydb.html)) 169 | 170 | @param inPath Path of database file 171 | 172 | @return `FMDatabase` object if successful; `nil` if failure. 173 | 174 | */ 175 | 176 | - (instancetype)initWithPath:(NSString*)inPath; 177 | 178 | 179 | ///----------------------------------- 180 | /// @name Opening and closing database 181 | ///----------------------------------- 182 | 183 | /** Opening a new database connection 184 | 185 | The database is opened for reading and writing, and is created if it does not already exist. 186 | 187 | @return `YES` if successful, `NO` on error. 188 | 189 | @see [sqlite3_open()](http://sqlite.org/c3ref/open.html) 190 | @see openWithFlags: 191 | @see close 192 | */ 193 | 194 | - (BOOL)open; 195 | 196 | /** Opening a new database connection with flags and an optional virtual file system (VFS) 197 | 198 | @param flags one of the following three values, optionally combined with the `SQLITE_OPEN_NOMUTEX`, `SQLITE_OPEN_FULLMUTEX`, `SQLITE_OPEN_SHAREDCACHE`, `SQLITE_OPEN_PRIVATECACHE`, and/or `SQLITE_OPEN_URI` flags: 199 | 200 | `SQLITE_OPEN_READONLY` 201 | 202 | The database is opened in read-only mode. If the database does not already exist, an error is returned. 203 | 204 | `SQLITE_OPEN_READWRITE` 205 | 206 | The database is opened for reading and writing if possible, or reading only if the file is write protected by the operating system. In either case the database must already exist, otherwise an error is returned. 207 | 208 | `SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE` 209 | 210 | The database is opened for reading and writing, and is created if it does not already exist. This is the behavior that is always used for `open` method. 211 | 212 | If vfs is given the value is passed to the vfs parameter of sqlite3_open_v2. 213 | 214 | @return `YES` if successful, `NO` on error. 215 | 216 | @see [sqlite3_open_v2()](http://sqlite.org/c3ref/open.html) 217 | @see open 218 | @see close 219 | */ 220 | 221 | #if SQLITE_VERSION_NUMBER >= 3005000 222 | - (BOOL)openWithFlags:(int)flags; 223 | - (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName; 224 | #endif 225 | 226 | /** Closing a database connection 227 | 228 | @return `YES` if success, `NO` on error. 229 | 230 | @see [sqlite3_close()](http://sqlite.org/c3ref/close.html) 231 | @see open 232 | @see openWithFlags: 233 | */ 234 | 235 | - (BOOL)close; 236 | 237 | /** Test to see if we have a good connection to the database. 238 | 239 | This will confirm whether: 240 | 241 | - is database open 242 | - if open, it will try a simple SELECT statement and confirm that it succeeds. 243 | 244 | @return `YES` if everything succeeds, `NO` on failure. 245 | */ 246 | 247 | - (BOOL)goodConnection; 248 | 249 | 250 | ///---------------------- 251 | /// @name Perform updates 252 | ///---------------------- 253 | 254 | /** Execute single update statement 255 | 256 | This method executes a single SQL update statement (i.e. any SQL that does not return results, such as `UPDATE`, `INSERT`, or `DELETE`. This method employs [`sqlite3_prepare_v2`](http://sqlite.org/c3ref/prepare.html), [`sqlite3_bind`](http://sqlite.org/c3ref/bind_blob.html) to bind values to `?` placeholders in the SQL with the optional list of parameters, and [`sqlite_step`](http://sqlite.org/c3ref/step.html) to perform the update. 257 | 258 | The optional values provided to this method should be objects (e.g. `NSString`, `NSNumber`, `NSNull`, `NSDate`, and `NSData` objects), not fundamental data types (e.g. `int`, `long`, `NSInteger`, etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's `description` method. 259 | 260 | @param sql The SQL to be performed, with optional `?` placeholders. 261 | 262 | @param outErr A reference to the `NSError` pointer to be updated with an auto released `NSError` object if an error if an error occurs. If `nil`, no `NSError` object will be returned. 263 | 264 | @param ... Optional parameters to bind to `?` placeholders in the SQL statement. These should be Objective-C objects (e.g. `NSString`, `NSNumber`, etc.), not fundamental C data types (e.g. `int`, `char *`, etc.). 265 | 266 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 267 | 268 | @see lastError 269 | @see lastErrorCode 270 | @see lastErrorMessage 271 | @see [`sqlite3_bind`](http://sqlite.org/c3ref/bind_blob.html) 272 | */ 273 | 274 | - (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ...; 275 | 276 | /** Execute single update statement 277 | 278 | @see executeUpdate:withErrorAndBindings: 279 | 280 | @warning **Deprecated**: Please use `` instead. 281 | */ 282 | 283 | - (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ... __attribute__ ((deprecated)); 284 | 285 | /** Execute single update statement 286 | 287 | This method executes a single SQL update statement (i.e. any SQL that does not return results, such as `UPDATE`, `INSERT`, or `DELETE`. This method employs [`sqlite3_prepare_v2`](http://sqlite.org/c3ref/prepare.html), [`sqlite3_bind`](http://sqlite.org/c3ref/bind_blob.html) to bind values to `?` placeholders in the SQL with the optional list of parameters, and [`sqlite_step`](http://sqlite.org/c3ref/step.html) to perform the update. 288 | 289 | The optional values provided to this method should be objects (e.g. `NSString`, `NSNumber`, `NSNull`, `NSDate`, and `NSData` objects), not fundamental data types (e.g. `int`, `long`, `NSInteger`, etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's `description` method. 290 | 291 | @param sql The SQL to be performed, with optional `?` placeholders. 292 | 293 | @param ... Optional parameters to bind to `?` placeholders in the SQL statement. These should be Objective-C objects (e.g. `NSString`, `NSNumber`, etc.), not fundamental C data types (e.g. `int`, `char *`, etc.). 294 | 295 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 296 | 297 | @see lastError 298 | @see lastErrorCode 299 | @see lastErrorMessage 300 | @see [`sqlite3_bind`](http://sqlite.org/c3ref/bind_blob.html) 301 | 302 | @note This technique supports the use of `?` placeholders in the SQL, automatically binding any supplied value parameters to those placeholders. This approach is more robust than techniques that entail using `stringWithFormat` to manually build SQL statements, which can be problematic if the values happened to include any characters that needed to be quoted. 303 | 304 | @note If you want to use this from Swift, please note that you must include `FMDatabaseVariadic.swift` in your project. Without that, you cannot use this method directly, and instead have to use methods such as ``. 305 | */ 306 | 307 | - (BOOL)executeUpdate:(NSString*)sql, ...; 308 | 309 | /** Execute single update statement 310 | 311 | This method executes a single SQL update statement (i.e. any SQL that does not return results, such as `UPDATE`, `INSERT`, or `DELETE`. This method employs [`sqlite3_prepare_v2`](http://sqlite.org/c3ref/prepare.html) and [`sqlite_step`](http://sqlite.org/c3ref/step.html) to perform the update. Unlike the other `executeUpdate` methods, this uses printf-style formatters (e.g. `%s`, `%d`, etc.) to build the SQL. Do not use `?` placeholders in the SQL if you use this method. 312 | 313 | @param format The SQL to be performed, with `printf`-style escape sequences. 314 | 315 | @param ... Optional parameters to bind to use in conjunction with the `printf`-style escape sequences in the SQL statement. 316 | 317 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 318 | 319 | @see executeUpdate: 320 | @see lastError 321 | @see lastErrorCode 322 | @see lastErrorMessage 323 | 324 | @note This method does not technically perform a traditional printf-style replacement. What this method actually does is replace the printf-style percent sequences with a SQLite `?` placeholder, and then bind values to that placeholder. Thus the following command 325 | 326 | [db executeUpdateWithFormat:@"INSERT INTO test (name) VALUES (%@)", @"Gus"]; 327 | 328 | is actually replacing the `%@` with `?` placeholder, and then performing something equivalent to `` 329 | 330 | [db executeUpdate:@"INSERT INTO test (name) VALUES (?)", @"Gus"]; 331 | 332 | There are two reasons why this distinction is important. First, the printf-style escape sequences can only be used where it is permissible to use a SQLite `?` placeholder. You can use it only for values in SQL statements, but not for table names or column names or any other non-value context. This method also cannot be used in conjunction with `pragma` statements and the like. Second, note the lack of quotation marks in the SQL. The `VALUES` clause was _not_ `VALUES ('%@')` (like you might have to do if you built a SQL statement using `NSString` method `stringWithFormat`), but rather simply `VALUES (%@)`. 333 | */ 334 | 335 | - (BOOL)executeUpdateWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1,2); 336 | 337 | /** Execute single update statement 338 | 339 | This method executes a single SQL update statement (i.e. any SQL that does not return results, such as `UPDATE`, `INSERT`, or `DELETE`. This method employs [`sqlite3_prepare_v2`](http://sqlite.org/c3ref/prepare.html) and [`sqlite3_bind`](http://sqlite.org/c3ref/bind_blob.html) binding any `?` placeholders in the SQL with the optional list of parameters. 340 | 341 | The optional values provided to this method should be objects (e.g. `NSString`, `NSNumber`, `NSNull`, `NSDate`, and `NSData` objects), not fundamental data types (e.g. `int`, `long`, `NSInteger`, etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's `description` method. 342 | 343 | @param sql The SQL to be performed, with optional `?` placeholders. 344 | 345 | @param arguments A `NSArray` of objects to be used when binding values to the `?` placeholders in the SQL statement. 346 | 347 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 348 | 349 | @see lastError 350 | @see lastErrorCode 351 | @see lastErrorMessage 352 | */ 353 | 354 | - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments; 355 | 356 | /** Execute single update statement 357 | 358 | This method executes a single SQL update statement (i.e. any SQL that does not return results, such as `UPDATE`, `INSERT`, or `DELETE`. This method employs [`sqlite3_prepare_v2`](http://sqlite.org/c3ref/prepare.html) and [`sqlite_step`](http://sqlite.org/c3ref/step.html) to perform the update. Unlike the other `executeUpdate` methods, this uses printf-style formatters (e.g. `%s`, `%d`, etc.) to build the SQL. 359 | 360 | The optional values provided to this method should be objects (e.g. `NSString`, `NSNumber`, `NSNull`, `NSDate`, and `NSData` objects), not fundamental data types (e.g. `int`, `long`, `NSInteger`, etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's `description` method. 361 | 362 | @param sql The SQL to be performed, with optional `?` placeholders. 363 | 364 | @param arguments A `NSDictionary` of objects keyed by column names that will be used when binding values to the `?` placeholders in the SQL statement. 365 | 366 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 367 | 368 | @see lastError 369 | @see lastErrorCode 370 | @see lastErrorMessage 371 | */ 372 | 373 | - (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments; 374 | 375 | 376 | /** Execute single update statement 377 | 378 | This method executes a single SQL update statement (i.e. any SQL that does not return results, such as `UPDATE`, `INSERT`, or `DELETE`. This method employs [`sqlite3_prepare_v2`](http://sqlite.org/c3ref/prepare.html) and [`sqlite_step`](http://sqlite.org/c3ref/step.html) to perform the update. Unlike the other `executeUpdate` methods, this uses printf-style formatters (e.g. `%s`, `%d`, etc.) to build the SQL. 379 | 380 | The optional values provided to this method should be objects (e.g. `NSString`, `NSNumber`, `NSNull`, `NSDate`, and `NSData` objects), not fundamental data types (e.g. `int`, `long`, `NSInteger`, etc.). This method automatically handles the aforementioned object types, and all other object types will be interpreted as text values using the object's `description` method. 381 | 382 | @param sql The SQL to be performed, with optional `?` placeholders. 383 | 384 | @param args A `va_list` of arguments. 385 | 386 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 387 | 388 | @see lastError 389 | @see lastErrorCode 390 | @see lastErrorMessage 391 | */ 392 | 393 | - (BOOL)executeUpdate:(NSString*)sql withVAList: (va_list)args; 394 | 395 | /** Execute multiple SQL statements 396 | 397 | This executes a series of SQL statements that are combined in a single string (e.g. the SQL generated by the `sqlite3` command line `.dump` command). This accepts no value parameters, but rather simply expects a single string with multiple SQL statements, each terminated with a semicolon. This uses `sqlite3_exec`. 398 | 399 | @param sql The SQL to be performed 400 | 401 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 402 | 403 | @see executeStatements:withResultBlock: 404 | @see [sqlite3_exec()](http://sqlite.org/c3ref/exec.html) 405 | 406 | */ 407 | 408 | - (BOOL)executeStatements:(NSString *)sql; 409 | 410 | /** Execute multiple SQL statements with callback handler 411 | 412 | This executes a series of SQL statements that are combined in a single string (e.g. the SQL generated by the `sqlite3` command line `.dump` command). This accepts no value parameters, but rather simply expects a single string with multiple SQL statements, each terminated with a semicolon. This uses `sqlite3_exec`. 413 | 414 | @param sql The SQL to be performed. 415 | @param block A block that will be called for any result sets returned by any SQL statements. 416 | Note, if you supply this block, it must return integer value, zero upon success (this would be a good opportunity to use SQLITE_OK), 417 | non-zero value upon failure (which will stop the bulk execution of the SQL). If a statement returns values, the block will be called with the results from the query in NSDictionary *resultsDictionary. 418 | This may be `nil` if you don't care to receive any results. 419 | 420 | @return `YES` upon success; `NO` upon failure. If failed, you can call ``, 421 | ``, or `` for diagnostic information regarding the failure. 422 | 423 | @see executeStatements: 424 | @see [sqlite3_exec()](http://sqlite.org/c3ref/exec.html) 425 | 426 | */ 427 | 428 | - (BOOL)executeStatements:(NSString *)sql withResultBlock:(FMDBExecuteStatementsCallbackBlock)block; 429 | 430 | /** Last insert rowid 431 | 432 | Each entry in an SQLite table has a unique 64-bit signed integer key called the "rowid". The rowid is always available as an undeclared column named `ROWID`, `OID`, or `_ROWID_` as long as those names are not also used by explicitly declared columns. If the table has a column of type `INTEGER PRIMARY KEY` then that column is another alias for the rowid. 433 | 434 | This routine returns the rowid of the most recent successful `INSERT` into the database from the database connection in the first argument. As of SQLite version 3.7.7, this routines records the last insert rowid of both ordinary tables and virtual tables. If no successful `INSERT`s have ever occurred on that database connection, zero is returned. 435 | 436 | @return The rowid of the last inserted row. 437 | 438 | @see [sqlite3_last_insert_rowid()](http://sqlite.org/c3ref/last_insert_rowid.html) 439 | 440 | */ 441 | 442 | - (long long int)lastInsertRowId; 443 | 444 | /** The number of rows changed by prior SQL statement. 445 | 446 | This function returns the number of database rows that were changed or inserted or deleted by the most recently completed SQL statement on the database connection specified by the first parameter. Only changes that are directly specified by the INSERT, UPDATE, or DELETE statement are counted. 447 | 448 | @return The number of rows changed by prior SQL statement. 449 | 450 | @see [sqlite3_changes()](http://sqlite.org/c3ref/changes.html) 451 | 452 | */ 453 | 454 | - (int)changes; 455 | 456 | 457 | ///------------------------- 458 | /// @name Retrieving results 459 | ///------------------------- 460 | 461 | /** Execute select statement 462 | 463 | Executing queries returns an `` object if successful, and `nil` upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the `` and `` methods to determine why a query failed. 464 | 465 | In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[FMResultSet next]>`) from one record to the other. 466 | 467 | This method employs [`sqlite3_bind`](http://sqlite.org/c3ref/bind_blob.html) for any optional value parameters. This properly escapes any characters that need escape sequences (e.g. quotation marks), which eliminates simple SQL errors as well as protects against SQL injection attacks. This method natively handles `NSString`, `NSNumber`, `NSNull`, `NSDate`, and `NSData` objects. All other object types will be interpreted as text values using the object's `description` method. 468 | 469 | @param sql The SELECT statement to be performed, with optional `?` placeholders. 470 | 471 | @param ... Optional parameters to bind to `?` placeholders in the SQL statement. These should be Objective-C objects (e.g. `NSString`, `NSNumber`, etc.), not fundamental C data types (e.g. `int`, `char *`, etc.). 472 | 473 | @return A `` for the result set upon success; `nil` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 474 | 475 | @see FMResultSet 476 | @see [`FMResultSet next`](<[FMResultSet next]>) 477 | @see [`sqlite3_bind`](http://sqlite.org/c3ref/bind_blob.html) 478 | 479 | @note If you want to use this from Swift, please note that you must include `FMDatabaseVariadic.swift` in your project. Without that, you cannot use this method directly, and instead have to use methods such as ``. 480 | */ 481 | 482 | - (FMResultSet *)executeQuery:(NSString*)sql, ...; 483 | 484 | /** Execute select statement 485 | 486 | Executing queries returns an `` object if successful, and `nil` upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the `` and `` methods to determine why a query failed. 487 | 488 | In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[FMResultSet next]>`) from one record to the other. 489 | 490 | @param format The SQL to be performed, with `printf`-style escape sequences. 491 | 492 | @param ... Optional parameters to bind to use in conjunction with the `printf`-style escape sequences in the SQL statement. 493 | 494 | @return A `` for the result set upon success; `nil` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 495 | 496 | @see executeQuery: 497 | @see FMResultSet 498 | @see [`FMResultSet next`](<[FMResultSet next]>) 499 | 500 | @note This method does not technically perform a traditional printf-style replacement. What this method actually does is replace the printf-style percent sequences with a SQLite `?` placeholder, and then bind values to that placeholder. Thus the following command 501 | 502 | [db executeQueryWithFormat:@"SELECT * FROM test WHERE name=%@", @"Gus"]; 503 | 504 | is actually replacing the `%@` with `?` placeholder, and then performing something equivalent to `` 505 | 506 | [db executeQuery:@"SELECT * FROM test WHERE name=?", @"Gus"]; 507 | 508 | There are two reasons why this distinction is important. First, the printf-style escape sequences can only be used where it is permissible to use a SQLite `?` placeholder. You can use it only for values in SQL statements, but not for table names or column names or any other non-value context. This method also cannot be used in conjunction with `pragma` statements and the like. Second, note the lack of quotation marks in the SQL. The `WHERE` clause was _not_ `WHERE name='%@'` (like you might have to do if you built a SQL statement using `NSString` method `stringWithFormat`), but rather simply `WHERE name=%@`. 509 | 510 | */ 511 | 512 | - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... NS_FORMAT_FUNCTION(1,2); 513 | 514 | /** Execute select statement 515 | 516 | Executing queries returns an `` object if successful, and `nil` upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the `` and `` methods to determine why a query failed. 517 | 518 | In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[FMResultSet next]>`) from one record to the other. 519 | 520 | @param sql The SELECT statement to be performed, with optional `?` placeholders. 521 | 522 | @param arguments A `NSArray` of objects to be used when binding values to the `?` placeholders in the SQL statement. 523 | 524 | @return A `` for the result set upon success; `nil` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 525 | 526 | @see FMResultSet 527 | @see [`FMResultSet next`](<[FMResultSet next]>) 528 | */ 529 | 530 | - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments; 531 | 532 | /** Execute select statement 533 | 534 | Executing queries returns an `` object if successful, and `nil` upon failure. Like executing updates, there is a variant that accepts an `NSError **` parameter. Otherwise you should use the `` and `` methods to determine why a query failed. 535 | 536 | In order to iterate through the results of your query, you use a `while()` loop. You also need to "step" (via `<[FMResultSet next]>`) from one record to the other. 537 | 538 | @param sql The SELECT statement to be performed, with optional `?` placeholders. 539 | 540 | @param arguments A `NSDictionary` of objects keyed by column names that will be used when binding values to the `?` placeholders in the SQL statement. 541 | 542 | @return A `` for the result set upon success; `nil` upon failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 543 | 544 | @see FMResultSet 545 | @see [`FMResultSet next`](<[FMResultSet next]>) 546 | */ 547 | 548 | - (FMResultSet *)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary *)arguments; 549 | 550 | 551 | // Documentation forthcoming. 552 | - (FMResultSet *)executeQuery:(NSString*)sql withVAList: (va_list)args; 553 | 554 | ///------------------- 555 | /// @name Transactions 556 | ///------------------- 557 | 558 | /** Begin a transaction 559 | 560 | @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 561 | 562 | @see commit 563 | @see rollback 564 | @see beginDeferredTransaction 565 | @see inTransaction 566 | */ 567 | 568 | - (BOOL)beginTransaction; 569 | 570 | /** Begin a deferred transaction 571 | 572 | @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 573 | 574 | @see commit 575 | @see rollback 576 | @see beginTransaction 577 | @see inTransaction 578 | */ 579 | 580 | - (BOOL)beginDeferredTransaction; 581 | 582 | /** Commit a transaction 583 | 584 | Commit a transaction that was initiated with either `` or with ``. 585 | 586 | @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 587 | 588 | @see beginTransaction 589 | @see beginDeferredTransaction 590 | @see rollback 591 | @see inTransaction 592 | */ 593 | 594 | - (BOOL)commit; 595 | 596 | /** Rollback a transaction 597 | 598 | Rollback a transaction that was initiated with either `` or with ``. 599 | 600 | @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 601 | 602 | @see beginTransaction 603 | @see beginDeferredTransaction 604 | @see commit 605 | @see inTransaction 606 | */ 607 | 608 | - (BOOL)rollback; 609 | 610 | /** Identify whether currently in a transaction or not 611 | 612 | @return `YES` if currently within transaction; `NO` if not. 613 | 614 | @see beginTransaction 615 | @see beginDeferredTransaction 616 | @see commit 617 | @see rollback 618 | */ 619 | 620 | - (BOOL)inTransaction; 621 | 622 | 623 | ///---------------------------------------- 624 | /// @name Cached statements and result sets 625 | ///---------------------------------------- 626 | 627 | /** Clear cached statements */ 628 | 629 | - (void)clearCachedStatements; 630 | 631 | /** Close all open result sets */ 632 | 633 | - (void)closeOpenResultSets; 634 | 635 | /** Whether database has any open result sets 636 | 637 | @return `YES` if there are open result sets; `NO` if not. 638 | */ 639 | 640 | - (BOOL)hasOpenResultSets; 641 | 642 | /** Return whether should cache statements or not 643 | 644 | @return `YES` if should cache statements; `NO` if not. 645 | */ 646 | 647 | - (BOOL)shouldCacheStatements; 648 | 649 | /** Set whether should cache statements or not 650 | 651 | @param value `YES` if should cache statements; `NO` if not. 652 | */ 653 | 654 | - (void)setShouldCacheStatements:(BOOL)value; 655 | 656 | 657 | ///------------------------- 658 | /// @name Encryption methods 659 | ///------------------------- 660 | 661 | /** Set encryption key. 662 | 663 | @param key The key to be used. 664 | 665 | @return `YES` if success, `NO` on error. 666 | 667 | @see http://www.sqlite-encrypt.com/develop-guide.htm 668 | 669 | @warning You need to have purchased the sqlite encryption extensions for this method to work. 670 | */ 671 | 672 | - (BOOL)setKey:(NSString*)key; 673 | 674 | /** Reset encryption key 675 | 676 | @param key The key to be used. 677 | 678 | @return `YES` if success, `NO` on error. 679 | 680 | @see http://www.sqlite-encrypt.com/develop-guide.htm 681 | 682 | @warning You need to have purchased the sqlite encryption extensions for this method to work. 683 | */ 684 | 685 | - (BOOL)rekey:(NSString*)key; 686 | 687 | /** Set encryption key using `keyData`. 688 | 689 | @param keyData The `NSData` to be used. 690 | 691 | @return `YES` if success, `NO` on error. 692 | 693 | @see http://www.sqlite-encrypt.com/develop-guide.htm 694 | 695 | @warning You need to have purchased the sqlite encryption extensions for this method to work. 696 | */ 697 | 698 | - (BOOL)setKeyWithData:(NSData *)keyData; 699 | 700 | /** Reset encryption key using `keyData`. 701 | 702 | @param keyData The `NSData` to be used. 703 | 704 | @return `YES` if success, `NO` on error. 705 | 706 | @see http://www.sqlite-encrypt.com/develop-guide.htm 707 | 708 | @warning You need to have purchased the sqlite encryption extensions for this method to work. 709 | */ 710 | 711 | - (BOOL)rekeyWithData:(NSData *)keyData; 712 | 713 | 714 | ///------------------------------ 715 | /// @name General inquiry methods 716 | ///------------------------------ 717 | 718 | /** The path of the database file 719 | 720 | @return path of database. 721 | 722 | */ 723 | 724 | - (NSString *)databasePath; 725 | 726 | /** The underlying SQLite handle 727 | 728 | @return The `sqlite3` pointer. 729 | 730 | */ 731 | 732 | - (void *)sqliteHandle; 733 | 734 | 735 | ///----------------------------- 736 | /// @name Retrieving error codes 737 | ///----------------------------- 738 | 739 | /** Last error message 740 | 741 | Returns the English-language text that describes the most recent failed SQLite API call associated with a database connection. If a prior API call failed but the most recent API call succeeded, this return value is undefined. 742 | 743 | @return `NSString` of the last error message. 744 | 745 | @see [sqlite3_errmsg()](http://sqlite.org/c3ref/errcode.html) 746 | @see lastErrorCode 747 | @see lastError 748 | 749 | */ 750 | 751 | - (NSString*)lastErrorMessage; 752 | 753 | /** Last error code 754 | 755 | Returns the numeric result code or extended result code for the most recent failed SQLite API call associated with a database connection. If a prior API call failed but the most recent API call succeeded, this return value is undefined. 756 | 757 | @return Integer value of the last error code. 758 | 759 | @see [sqlite3_errcode()](http://sqlite.org/c3ref/errcode.html) 760 | @see lastErrorMessage 761 | @see lastError 762 | 763 | */ 764 | 765 | - (int)lastErrorCode; 766 | 767 | /** Had error 768 | 769 | @return `YES` if there was an error, `NO` if no error. 770 | 771 | @see lastError 772 | @see lastErrorCode 773 | @see lastErrorMessage 774 | 775 | */ 776 | 777 | - (BOOL)hadError; 778 | 779 | /** Last error 780 | 781 | @return `NSError` representing the last error. 782 | 783 | @see lastErrorCode 784 | @see lastErrorMessage 785 | 786 | */ 787 | 788 | - (NSError*)lastError; 789 | 790 | 791 | // description forthcoming 792 | - (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeoutInSeconds; 793 | - (NSTimeInterval)maxBusyRetryTimeInterval; 794 | 795 | 796 | #if SQLITE_VERSION_NUMBER >= 3007000 797 | 798 | ///------------------ 799 | /// @name Save points 800 | ///------------------ 801 | 802 | /** Start save point 803 | 804 | @param name Name of save point. 805 | 806 | @param outErr A `NSError` object to receive any error object (if any). 807 | 808 | @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 809 | 810 | @see releaseSavePointWithName:error: 811 | @see rollbackToSavePointWithName:error: 812 | */ 813 | 814 | - (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr; 815 | 816 | /** Release save point 817 | 818 | @param name Name of save point. 819 | 820 | @param outErr A `NSError` object to receive any error object (if any). 821 | 822 | @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 823 | 824 | @see startSavePointWithName:error: 825 | @see rollbackToSavePointWithName:error: 826 | 827 | */ 828 | 829 | - (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr; 830 | 831 | /** Roll back to save point 832 | 833 | @param name Name of save point. 834 | @param outErr A `NSError` object to receive any error object (if any). 835 | 836 | @return `YES` on success; `NO` on failure. If failed, you can call ``, ``, or `` for diagnostic information regarding the failure. 837 | 838 | @see startSavePointWithName:error: 839 | @see releaseSavePointWithName:error: 840 | 841 | */ 842 | 843 | - (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr; 844 | 845 | /** Start save point 846 | 847 | @param block Block of code to perform from within save point. 848 | 849 | @return The NSError corresponding to the error, if any. If no error, returns `nil`. 850 | 851 | @see startSavePointWithName:error: 852 | @see releaseSavePointWithName:error: 853 | @see rollbackToSavePointWithName:error: 854 | 855 | */ 856 | 857 | - (NSError*)inSavePoint:(void (^)(BOOL *rollback))block; 858 | 859 | #endif 860 | 861 | ///---------------------------- 862 | /// @name SQLite library status 863 | ///---------------------------- 864 | 865 | /** Test to see if the library is threadsafe 866 | 867 | @return `NO` if and only if SQLite was compiled with mutexing code omitted due to the SQLITE_THREADSAFE compile-time option being set to 0. 868 | 869 | @see [sqlite3_threadsafe()](http://sqlite.org/c3ref/threadsafe.html) 870 | */ 871 | 872 | + (BOOL)isSQLiteThreadSafe; 873 | 874 | /** Run-time library version numbers 875 | 876 | @return The sqlite library version string. 877 | 878 | @see [sqlite3_libversion()](http://sqlite.org/c3ref/libversion.html) 879 | */ 880 | 881 | + (NSString*)sqliteLibVersion; 882 | 883 | 884 | + (NSString*)FMDBUserVersion; 885 | 886 | + (SInt32)FMDBVersion; 887 | 888 | 889 | ///------------------------ 890 | /// @name Make SQL function 891 | ///------------------------ 892 | 893 | /** Adds SQL functions or aggregates or to redefine the behavior of existing SQL functions or aggregates. 894 | 895 | For example: 896 | 897 | [queue inDatabase:^(FMDatabase *adb) { 898 | 899 | [adb executeUpdate:@"create table ftest (foo text)"]; 900 | [adb executeUpdate:@"insert into ftest values ('hello')"]; 901 | [adb executeUpdate:@"insert into ftest values ('hi')"]; 902 | [adb executeUpdate:@"insert into ftest values ('not h!')"]; 903 | [adb executeUpdate:@"insert into ftest values ('definitely not h!')"]; 904 | 905 | [adb makeFunctionNamed:@"StringStartsWithH" maximumArguments:1 withBlock:^(sqlite3_context *context, int aargc, sqlite3_value **aargv) { 906 | if (sqlite3_value_type(aargv[0]) == SQLITE_TEXT) { 907 | @autoreleasepool { 908 | const char *c = (const char *)sqlite3_value_text(aargv[0]); 909 | NSString *s = [NSString stringWithUTF8String:c]; 910 | sqlite3_result_int(context, [s hasPrefix:@"h"]); 911 | } 912 | } 913 | else { 914 | NSLog(@"Unknown formart for StringStartsWithH (%d) %s:%d", sqlite3_value_type(aargv[0]), __FUNCTION__, __LINE__); 915 | sqlite3_result_null(context); 916 | } 917 | }]; 918 | 919 | int rowCount = 0; 920 | FMResultSet *ars = [adb executeQuery:@"select * from ftest where StringStartsWithH(foo)"]; 921 | while ([ars next]) { 922 | rowCount++; 923 | NSLog(@"Does %@ start with 'h'?", [rs stringForColumnIndex:0]); 924 | } 925 | FMDBQuickCheck(rowCount == 2); 926 | }]; 927 | 928 | @param name Name of function 929 | 930 | @param count Maximum number of parameters 931 | 932 | @param block The block of code for the function 933 | 934 | @see [sqlite3_create_function()](http://sqlite.org/c3ref/create_function.html) 935 | */ 936 | 937 | - (void)makeFunctionNamed:(NSString*)name maximumArguments:(int)count withBlock:(void (^)(void *context, int argc, void **argv))block; 938 | 939 | 940 | ///--------------------- 941 | /// @name Date formatter 942 | ///--------------------- 943 | 944 | /** Generate an `NSDateFormatter` that won't be broken by permutations of timezones or locales. 945 | 946 | Use this method to generate values to set the dateFormat property. 947 | 948 | Example: 949 | 950 | myDB.dateFormat = [FMDatabase storeableDateFormat:@"yyyy-MM-dd HH:mm:ss"]; 951 | 952 | @param format A valid NSDateFormatter format string. 953 | 954 | @return A `NSDateFormatter` that can be used for converting dates to strings and vice versa. 955 | 956 | @see hasDateFormatter 957 | @see setDateFormat: 958 | @see dateFromString: 959 | @see stringFromDate: 960 | @see storeableDateFormat: 961 | 962 | @warning Note that `NSDateFormatter` is not thread-safe, so the formatter generated by this method should be assigned to only one FMDB instance and should not be used for other purposes. 963 | 964 | */ 965 | 966 | + (NSDateFormatter *)storeableDateFormat:(NSString *)format; 967 | 968 | /** Test whether the database has a date formatter assigned. 969 | 970 | @return `YES` if there is a date formatter; `NO` if not. 971 | 972 | @see hasDateFormatter 973 | @see setDateFormat: 974 | @see dateFromString: 975 | @see stringFromDate: 976 | @see storeableDateFormat: 977 | */ 978 | 979 | - (BOOL)hasDateFormatter; 980 | 981 | /** Set to a date formatter to use string dates with sqlite instead of the default UNIX timestamps. 982 | 983 | @param format Set to nil to use UNIX timestamps. Defaults to nil. Should be set using a formatter generated using FMDatabase::storeableDateFormat. 984 | 985 | @see hasDateFormatter 986 | @see setDateFormat: 987 | @see dateFromString: 988 | @see stringFromDate: 989 | @see storeableDateFormat: 990 | 991 | @warning Note there is no direct getter for the `NSDateFormatter`, and you should not use the formatter you pass to FMDB for other purposes, as `NSDateFormatter` is not thread-safe. 992 | */ 993 | 994 | - (void)setDateFormat:(NSDateFormatter *)format; 995 | 996 | /** Convert the supplied NSString to NSDate, using the current database formatter. 997 | 998 | @param s `NSString` to convert to `NSDate`. 999 | 1000 | @return The `NSDate` object; or `nil` if no formatter is set. 1001 | 1002 | @see hasDateFormatter 1003 | @see setDateFormat: 1004 | @see dateFromString: 1005 | @see stringFromDate: 1006 | @see storeableDateFormat: 1007 | */ 1008 | 1009 | - (NSDate *)dateFromString:(NSString *)s; 1010 | 1011 | /** Convert the supplied NSDate to NSString, using the current database formatter. 1012 | 1013 | @param date `NSDate` of date to convert to `NSString`. 1014 | 1015 | @return The `NSString` representation of the date; `nil` if no formatter is set. 1016 | 1017 | @see hasDateFormatter 1018 | @see setDateFormat: 1019 | @see dateFromString: 1020 | @see stringFromDate: 1021 | @see storeableDateFormat: 1022 | */ 1023 | 1024 | - (NSString *)stringFromDate:(NSDate *)date; 1025 | 1026 | @end 1027 | 1028 | 1029 | /** Objective-C wrapper for `sqlite3_stmt` 1030 | 1031 | This is a wrapper for a SQLite `sqlite3_stmt`. Generally when using FMDB you will not need to interact directly with `FMStatement`, but rather with `` and `` only. 1032 | 1033 | ### See also 1034 | 1035 | - `` 1036 | - `` 1037 | - [`sqlite3_stmt`](http://www.sqlite.org/c3ref/stmt.html) 1038 | */ 1039 | 1040 | @interface FMStatement : NSObject { 1041 | NSString *_query; 1042 | long _useCount; 1043 | BOOL _inUse; 1044 | } 1045 | 1046 | ///----------------- 1047 | /// @name Properties 1048 | ///----------------- 1049 | 1050 | /** Usage count */ 1051 | 1052 | @property (atomic, assign) long useCount; 1053 | 1054 | /** SQL statement */ 1055 | 1056 | @property (atomic, retain) NSString *query; 1057 | 1058 | /** SQLite sqlite3_stmt 1059 | 1060 | @see [`sqlite3_stmt`](http://www.sqlite.org/c3ref/stmt.html) 1061 | */ 1062 | 1063 | @property (atomic, assign) void *statement; 1064 | 1065 | /** Indication of whether the statement is in use */ 1066 | 1067 | @property (atomic, assign) BOOL inUse; 1068 | 1069 | ///---------------------------- 1070 | /// @name Closing and Resetting 1071 | ///---------------------------- 1072 | 1073 | /** Close statement */ 1074 | 1075 | - (void)close; 1076 | 1077 | /** Reset statement */ 1078 | 1079 | - (void)reset; 1080 | 1081 | @end 1082 | 1083 | #pragma clang diagnostic pop 1084 | 1085 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMDatabase.m: -------------------------------------------------------------------------------- 1 | #import "FMDatabase.h" 2 | #import "unistd.h" 3 | #import 4 | #import "sqlite3.h" 5 | 6 | @interface FMDatabase () { 7 | sqlite3* _db; 8 | } 9 | 10 | - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args; 11 | - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args; 12 | 13 | @end 14 | 15 | @implementation FMDatabase 16 | @synthesize cachedStatements=_cachedStatements; 17 | @synthesize logsErrors=_logsErrors; 18 | @synthesize crashOnErrors=_crashOnErrors; 19 | @synthesize checkedOut=_checkedOut; 20 | @synthesize traceExecution=_traceExecution; 21 | 22 | #pragma mark FMDatabase instantiation and deallocation 23 | 24 | + (instancetype)databaseWithPath:(NSString*)aPath { 25 | return FMDBReturnAutoreleased([[self alloc] initWithPath:aPath]); 26 | } 27 | 28 | - (instancetype)init { 29 | return [self initWithPath:nil]; 30 | } 31 | 32 | - (instancetype)initWithPath:(NSString*)aPath { 33 | 34 | assert(sqlite3_threadsafe()); // whoa there big boy- gotta make sure sqlite it happy with what we're going to do. 35 | 36 | self = [super init]; 37 | 38 | if (self) { 39 | _databasePath = [aPath copy]; 40 | _openResultSets = [[NSMutableSet alloc] init]; 41 | _db = nil; 42 | _logsErrors = YES; 43 | _crashOnErrors = NO; 44 | _maxBusyRetryTimeInterval = 2; 45 | } 46 | 47 | return self; 48 | } 49 | 50 | - (void)dealloc { 51 | [self close]; 52 | FMDBRelease(_openResultSets); 53 | FMDBRelease(_cachedStatements); 54 | FMDBRelease(_dateFormat); 55 | FMDBRelease(_databasePath); 56 | FMDBRelease(_openFunctions); 57 | 58 | #if ! __has_feature(objc_arc) 59 | [super dealloc]; 60 | #endif 61 | } 62 | 63 | - (NSString *)databasePath { 64 | return _databasePath; 65 | } 66 | 67 | + (NSString*)FMDBUserVersion { 68 | return @"2.5"; 69 | } 70 | 71 | // returns 0x0240 for version 2.4. This makes it super easy to do things like: 72 | // /* need to make sure to do X with FMDB version 2.4 or later */ 73 | // if ([FMDatabase FMDBVersion] >= 0x0240) { … } 74 | 75 | + (SInt32)FMDBVersion { 76 | 77 | // we go through these hoops so that we only have to change the version number in a single spot. 78 | static dispatch_once_t once; 79 | static SInt32 FMDBVersionVal = 0; 80 | 81 | dispatch_once(&once, ^{ 82 | NSString *prodVersion = [self FMDBUserVersion]; 83 | 84 | if ([[prodVersion componentsSeparatedByString:@"."] count] < 3) { 85 | prodVersion = [prodVersion stringByAppendingString:@".0"]; 86 | } 87 | 88 | NSString *junk = [prodVersion stringByReplacingOccurrencesOfString:@"." withString:@""]; 89 | 90 | char *e = nil; 91 | FMDBVersionVal = (int) strtoul([junk UTF8String], &e, 16); 92 | 93 | }); 94 | 95 | 96 | return FMDBVersionVal; 97 | } 98 | 99 | #pragma mark SQLite information 100 | 101 | + (NSString*)sqliteLibVersion { 102 | return [NSString stringWithFormat:@"%s", sqlite3_libversion()]; 103 | } 104 | 105 | + (BOOL)isSQLiteThreadSafe { 106 | // make sure to read the sqlite headers on this guy! 107 | return sqlite3_threadsafe() != 0; 108 | } 109 | 110 | - (void *)sqliteHandle { 111 | return _db; 112 | } 113 | 114 | - (const char*)sqlitePath { 115 | 116 | if (!_databasePath) { 117 | return ":memory:"; 118 | } 119 | 120 | if ([_databasePath length] == 0) { 121 | return ""; // this creates a temporary database (it's an sqlite thing). 122 | } 123 | 124 | return [_databasePath fileSystemRepresentation]; 125 | 126 | } 127 | 128 | #pragma mark Open and close database 129 | 130 | - (BOOL)open { 131 | if (_db) { 132 | return YES; 133 | } 134 | 135 | int err = sqlite3_open([self sqlitePath], &_db ); 136 | if(err != SQLITE_OK) { 137 | NSLog(@"error opening!: %d", err); 138 | return NO; 139 | } 140 | 141 | if (_maxBusyRetryTimeInterval > 0.0) { 142 | // set the handler 143 | [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; 144 | } 145 | 146 | 147 | return YES; 148 | } 149 | 150 | #if SQLITE_VERSION_NUMBER >= 3005000 151 | - (BOOL)openWithFlags:(int)flags { 152 | return [self openWithFlags:flags vfs:nil]; 153 | } 154 | - (BOOL)openWithFlags:(int)flags vfs:(NSString *)vfsName; { 155 | if (_db) { 156 | return YES; 157 | } 158 | 159 | int err = sqlite3_open_v2([self sqlitePath], &_db, flags, [vfsName UTF8String]); 160 | if(err != SQLITE_OK) { 161 | NSLog(@"error opening!: %d", err); 162 | return NO; 163 | } 164 | 165 | if (_maxBusyRetryTimeInterval > 0.0) { 166 | // set the handler 167 | [self setMaxBusyRetryTimeInterval:_maxBusyRetryTimeInterval]; 168 | } 169 | 170 | return YES; 171 | } 172 | #endif 173 | 174 | 175 | - (BOOL)close { 176 | 177 | [self clearCachedStatements]; 178 | [self closeOpenResultSets]; 179 | 180 | if (!_db) { 181 | return YES; 182 | } 183 | 184 | int rc; 185 | BOOL retry; 186 | BOOL triedFinalizingOpenStatements = NO; 187 | 188 | do { 189 | retry = NO; 190 | rc = sqlite3_close(_db); 191 | if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { 192 | if (!triedFinalizingOpenStatements) { 193 | triedFinalizingOpenStatements = YES; 194 | sqlite3_stmt *pStmt; 195 | while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) { 196 | NSLog(@"Closing leaked statement"); 197 | sqlite3_finalize(pStmt); 198 | retry = YES; 199 | } 200 | } 201 | } 202 | else if (SQLITE_OK != rc) { 203 | NSLog(@"error closing!: %d", rc); 204 | } 205 | } 206 | while (retry); 207 | 208 | _db = nil; 209 | return YES; 210 | } 211 | 212 | #pragma mark Busy handler routines 213 | 214 | // NOTE: appledoc seems to choke on this function for some reason; 215 | // so when generating documentation, you might want to ignore the 216 | // .m files so that it only documents the public interfaces outlined 217 | // in the .h files. 218 | // 219 | // This is a known appledoc bug that it has problems with C functions 220 | // within a class implementation, but for some reason, only this 221 | // C function causes problems; the rest don't. Anyway, ignoring the .m 222 | // files with appledoc will prevent this problem from occurring. 223 | 224 | static int FMDBDatabaseBusyHandler(void *f, int count) { 225 | FMDatabase *self = (__bridge FMDatabase*)f; 226 | 227 | if (count == 0) { 228 | self->_startBusyRetryTime = [NSDate timeIntervalSinceReferenceDate]; 229 | return 1; 230 | } 231 | 232 | NSTimeInterval delta = [NSDate timeIntervalSinceReferenceDate] - (self->_startBusyRetryTime); 233 | 234 | if (delta < [self maxBusyRetryTimeInterval]) { 235 | int requestedSleepInMillseconds = (int) arc4random_uniform(50) + 50; 236 | int actualSleepInMilliseconds = sqlite3_sleep(requestedSleepInMillseconds); 237 | if (actualSleepInMilliseconds != requestedSleepInMillseconds) { 238 | NSLog(@"WARNING: Requested sleep of %i milliseconds, but SQLite returned %i. Maybe SQLite wasn't built with HAVE_USLEEP=1?", requestedSleepInMillseconds, actualSleepInMilliseconds); 239 | } 240 | return 1; 241 | } 242 | 243 | return 0; 244 | } 245 | 246 | - (void)setMaxBusyRetryTimeInterval:(NSTimeInterval)timeout { 247 | 248 | _maxBusyRetryTimeInterval = timeout; 249 | 250 | if (!_db) { 251 | return; 252 | } 253 | 254 | if (timeout > 0) { 255 | sqlite3_busy_handler(_db, &FMDBDatabaseBusyHandler, (__bridge void *)(self)); 256 | } 257 | else { 258 | // turn it off otherwise 259 | sqlite3_busy_handler(_db, nil, nil); 260 | } 261 | } 262 | 263 | - (NSTimeInterval)maxBusyRetryTimeInterval { 264 | return _maxBusyRetryTimeInterval; 265 | } 266 | 267 | 268 | // we no longer make busyRetryTimeout public 269 | // but for folks who don't bother noticing that the interface to FMDatabase changed, 270 | // we'll still implement the method so they don't get surprise crashes 271 | - (int)busyRetryTimeout { 272 | NSLog(@"%s:%d", __FUNCTION__, __LINE__); 273 | NSLog(@"FMDB: busyRetryTimeout no longer works, please use maxBusyRetryTimeInterval"); 274 | return -1; 275 | } 276 | 277 | - (void)setBusyRetryTimeout:(int)i { 278 | NSLog(@"%s:%d", __FUNCTION__, __LINE__); 279 | NSLog(@"FMDB: setBusyRetryTimeout does nothing, please use setMaxBusyRetryTimeInterval:"); 280 | } 281 | 282 | #pragma mark Result set functions 283 | 284 | - (BOOL)hasOpenResultSets { 285 | return [_openResultSets count] > 0; 286 | } 287 | 288 | - (void)closeOpenResultSets { 289 | 290 | //Copy the set so we don't get mutation errors 291 | NSSet *openSetCopy = FMDBReturnAutoreleased([_openResultSets copy]); 292 | for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) { 293 | FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue]; 294 | 295 | [rs setParentDB:nil]; 296 | [rs close]; 297 | 298 | [_openResultSets removeObject:rsInWrappedInATastyValueMeal]; 299 | } 300 | } 301 | 302 | - (void)resultSetDidClose:(FMResultSet *)resultSet { 303 | NSValue *setValue = [NSValue valueWithNonretainedObject:resultSet]; 304 | 305 | [_openResultSets removeObject:setValue]; 306 | } 307 | 308 | #pragma mark Cached statements 309 | 310 | - (void)clearCachedStatements { 311 | 312 | for (NSMutableSet *statements in [_cachedStatements objectEnumerator]) { 313 | [statements makeObjectsPerformSelector:@selector(close)]; 314 | } 315 | 316 | [_cachedStatements removeAllObjects]; 317 | } 318 | 319 | - (FMStatement*)cachedStatementForQuery:(NSString*)query { 320 | 321 | NSMutableSet* statements = [_cachedStatements objectForKey:query]; 322 | 323 | return [[statements objectsPassingTest:^BOOL(FMStatement* statement, BOOL *stop) { 324 | 325 | *stop = ![statement inUse]; 326 | return *stop; 327 | 328 | }] anyObject]; 329 | } 330 | 331 | 332 | - (void)setCachedStatement:(FMStatement*)statement forQuery:(NSString*)query { 333 | 334 | query = [query copy]; // in case we got handed in a mutable string... 335 | [statement setQuery:query]; 336 | 337 | NSMutableSet* statements = [_cachedStatements objectForKey:query]; 338 | if (!statements) { 339 | statements = [NSMutableSet set]; 340 | } 341 | 342 | [statements addObject:statement]; 343 | 344 | [_cachedStatements setObject:statements forKey:query]; 345 | 346 | FMDBRelease(query); 347 | } 348 | 349 | #pragma mark Key routines 350 | 351 | - (BOOL)rekey:(NSString*)key { 352 | NSData *keyData = [NSData dataWithBytes:(void *)[key UTF8String] length:(NSUInteger)strlen([key UTF8String])]; 353 | 354 | return [self rekeyWithData:keyData]; 355 | } 356 | 357 | - (BOOL)rekeyWithData:(NSData *)keyData { 358 | #ifdef SQLITE_HAS_CODEC 359 | if (!keyData) { 360 | return NO; 361 | } 362 | 363 | int rc = sqlite3_rekey(_db, [keyData bytes], (int)[keyData length]); 364 | 365 | if (rc != SQLITE_OK) { 366 | NSLog(@"error on rekey: %d", rc); 367 | NSLog(@"%@", [self lastErrorMessage]); 368 | } 369 | 370 | return (rc == SQLITE_OK); 371 | #else 372 | return NO; 373 | #endif 374 | } 375 | 376 | - (BOOL)setKey:(NSString*)key { 377 | NSData *keyData = [NSData dataWithBytes:[key UTF8String] length:(NSUInteger)strlen([key UTF8String])]; 378 | 379 | return [self setKeyWithData:keyData]; 380 | } 381 | 382 | - (BOOL)setKeyWithData:(NSData *)keyData { 383 | #ifdef SQLITE_HAS_CODEC 384 | if (!keyData) { 385 | return NO; 386 | } 387 | 388 | int rc = sqlite3_key(_db, [keyData bytes], (int)[keyData length]); 389 | 390 | return (rc == SQLITE_OK); 391 | #else 392 | return NO; 393 | #endif 394 | } 395 | 396 | #pragma mark Date routines 397 | 398 | + (NSDateFormatter *)storeableDateFormat:(NSString *)format { 399 | 400 | NSDateFormatter *result = FMDBReturnAutoreleased([[NSDateFormatter alloc] init]); 401 | result.dateFormat = format; 402 | result.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; 403 | result.locale = FMDBReturnAutoreleased([[NSLocale alloc] initWithLocaleIdentifier:@"en_US"]); 404 | return result; 405 | } 406 | 407 | 408 | - (BOOL)hasDateFormatter { 409 | return _dateFormat != nil; 410 | } 411 | 412 | - (void)setDateFormat:(NSDateFormatter *)format { 413 | FMDBAutorelease(_dateFormat); 414 | _dateFormat = FMDBReturnRetained(format); 415 | } 416 | 417 | - (NSDate *)dateFromString:(NSString *)s { 418 | return [_dateFormat dateFromString:s]; 419 | } 420 | 421 | - (NSString *)stringFromDate:(NSDate *)date { 422 | return [_dateFormat stringFromDate:date]; 423 | } 424 | 425 | #pragma mark State of database 426 | 427 | - (BOOL)goodConnection { 428 | 429 | if (!_db) { 430 | return NO; 431 | } 432 | 433 | FMResultSet *rs = [self executeQuery:@"select name from sqlite_master where type='table'"]; 434 | 435 | if (rs) { 436 | [rs close]; 437 | return YES; 438 | } 439 | 440 | return NO; 441 | } 442 | 443 | - (void)warnInUse { 444 | NSLog(@"The FMDatabase %@ is currently in use.", self); 445 | 446 | #ifndef NS_BLOCK_ASSERTIONS 447 | if (_crashOnErrors) { 448 | NSAssert(false, @"The FMDatabase %@ is currently in use.", self); 449 | abort(); 450 | } 451 | #endif 452 | } 453 | 454 | - (BOOL)databaseExists { 455 | 456 | if (!_db) { 457 | 458 | NSLog(@"The FMDatabase %@ is not open.", self); 459 | 460 | #ifndef NS_BLOCK_ASSERTIONS 461 | if (_crashOnErrors) { 462 | NSAssert(false, @"The FMDatabase %@ is not open.", self); 463 | abort(); 464 | } 465 | #endif 466 | 467 | return NO; 468 | } 469 | 470 | return YES; 471 | } 472 | 473 | #pragma mark Error routines 474 | 475 | - (NSString*)lastErrorMessage { 476 | return [NSString stringWithUTF8String:sqlite3_errmsg(_db)]; 477 | } 478 | 479 | - (BOOL)hadError { 480 | int lastErrCode = [self lastErrorCode]; 481 | 482 | return (lastErrCode > SQLITE_OK && lastErrCode < SQLITE_ROW); 483 | } 484 | 485 | - (int)lastErrorCode { 486 | return sqlite3_errcode(_db); 487 | } 488 | 489 | - (NSError*)errorWithMessage:(NSString*)message { 490 | NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:message forKey:NSLocalizedDescriptionKey]; 491 | 492 | return [NSError errorWithDomain:@"FMDatabase" code:sqlite3_errcode(_db) userInfo:errorMessage]; 493 | } 494 | 495 | - (NSError*)lastError { 496 | return [self errorWithMessage:[self lastErrorMessage]]; 497 | } 498 | 499 | #pragma mark Update information routines 500 | 501 | - (sqlite_int64)lastInsertRowId { 502 | 503 | if (_isExecutingStatement) { 504 | [self warnInUse]; 505 | return NO; 506 | } 507 | 508 | _isExecutingStatement = YES; 509 | 510 | sqlite_int64 ret = sqlite3_last_insert_rowid(_db); 511 | 512 | _isExecutingStatement = NO; 513 | 514 | return ret; 515 | } 516 | 517 | - (int)changes { 518 | if (_isExecutingStatement) { 519 | [self warnInUse]; 520 | return 0; 521 | } 522 | 523 | _isExecutingStatement = YES; 524 | 525 | int ret = sqlite3_changes(_db); 526 | 527 | _isExecutingStatement = NO; 528 | 529 | return ret; 530 | } 531 | 532 | #pragma mark SQL manipulation 533 | 534 | - (void)bindObject:(id)obj toColumn:(int)idx inStatement:(sqlite3_stmt*)pStmt { 535 | 536 | if ((!obj) || ((NSNull *)obj == [NSNull null])) { 537 | sqlite3_bind_null(pStmt, idx); 538 | } 539 | 540 | // FIXME - someday check the return codes on these binds. 541 | else if ([obj isKindOfClass:[NSData class]]) { 542 | const void *bytes = [obj bytes]; 543 | if (!bytes) { 544 | // it's an empty NSData object, aka [NSData data]. 545 | // Don't pass a NULL pointer, or sqlite will bind a SQL null instead of a blob. 546 | bytes = ""; 547 | } 548 | sqlite3_bind_blob(pStmt, idx, bytes, (int)[obj length], SQLITE_STATIC); 549 | } 550 | else if ([obj isKindOfClass:[NSDate class]]) { 551 | if (self.hasDateFormatter) 552 | sqlite3_bind_text(pStmt, idx, [[self stringFromDate:obj] UTF8String], -1, SQLITE_STATIC); 553 | else 554 | sqlite3_bind_double(pStmt, idx, [obj timeIntervalSince1970]); 555 | } 556 | else if ([obj isKindOfClass:[NSNumber class]]) { 557 | 558 | if (strcmp([obj objCType], @encode(char)) == 0) { 559 | sqlite3_bind_int(pStmt, idx, [obj charValue]); 560 | } 561 | else if (strcmp([obj objCType], @encode(unsigned char)) == 0) { 562 | sqlite3_bind_int(pStmt, idx, [obj unsignedCharValue]); 563 | } 564 | else if (strcmp([obj objCType], @encode(short)) == 0) { 565 | sqlite3_bind_int(pStmt, idx, [obj shortValue]); 566 | } 567 | else if (strcmp([obj objCType], @encode(unsigned short)) == 0) { 568 | sqlite3_bind_int(pStmt, idx, [obj unsignedShortValue]); 569 | } 570 | else if (strcmp([obj objCType], @encode(int)) == 0) { 571 | sqlite3_bind_int(pStmt, idx, [obj intValue]); 572 | } 573 | else if (strcmp([obj objCType], @encode(unsigned int)) == 0) { 574 | sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedIntValue]); 575 | } 576 | else if (strcmp([obj objCType], @encode(long)) == 0) { 577 | sqlite3_bind_int64(pStmt, idx, [obj longValue]); 578 | } 579 | else if (strcmp([obj objCType], @encode(unsigned long)) == 0) { 580 | sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongValue]); 581 | } 582 | else if (strcmp([obj objCType], @encode(long long)) == 0) { 583 | sqlite3_bind_int64(pStmt, idx, [obj longLongValue]); 584 | } 585 | else if (strcmp([obj objCType], @encode(unsigned long long)) == 0) { 586 | sqlite3_bind_int64(pStmt, idx, (long long)[obj unsignedLongLongValue]); 587 | } 588 | else if (strcmp([obj objCType], @encode(float)) == 0) { 589 | sqlite3_bind_double(pStmt, idx, [obj floatValue]); 590 | } 591 | else if (strcmp([obj objCType], @encode(double)) == 0) { 592 | sqlite3_bind_double(pStmt, idx, [obj doubleValue]); 593 | } 594 | else if (strcmp([obj objCType], @encode(BOOL)) == 0) { 595 | sqlite3_bind_int(pStmt, idx, ([obj boolValue] ? 1 : 0)); 596 | } 597 | else { 598 | sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC); 599 | } 600 | } 601 | else { 602 | sqlite3_bind_text(pStmt, idx, [[obj description] UTF8String], -1, SQLITE_STATIC); 603 | } 604 | } 605 | 606 | - (void)extractSQL:(NSString *)sql argumentsList:(va_list)args intoString:(NSMutableString *)cleanedSQL arguments:(NSMutableArray *)arguments { 607 | 608 | NSUInteger length = [sql length]; 609 | unichar last = '\0'; 610 | for (NSUInteger i = 0; i < length; ++i) { 611 | id arg = nil; 612 | unichar current = [sql characterAtIndex:i]; 613 | unichar add = current; 614 | if (last == '%') { 615 | switch (current) { 616 | case '@': 617 | arg = va_arg(args, id); 618 | break; 619 | case 'c': 620 | // warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int' 621 | arg = [NSString stringWithFormat:@"%c", va_arg(args, int)]; 622 | break; 623 | case 's': 624 | arg = [NSString stringWithUTF8String:va_arg(args, char*)]; 625 | break; 626 | case 'd': 627 | case 'D': 628 | case 'i': 629 | arg = [NSNumber numberWithInt:va_arg(args, int)]; 630 | break; 631 | case 'u': 632 | case 'U': 633 | arg = [NSNumber numberWithUnsignedInt:va_arg(args, unsigned int)]; 634 | break; 635 | case 'h': 636 | i++; 637 | if (i < length && [sql characterAtIndex:i] == 'i') { 638 | // warning: second argument to 'va_arg' is of promotable type 'short'; this va_arg has undefined behavior because arguments will be promoted to 'int' 639 | arg = [NSNumber numberWithShort:(short)(va_arg(args, int))]; 640 | } 641 | else if (i < length && [sql characterAtIndex:i] == 'u') { 642 | // warning: second argument to 'va_arg' is of promotable type 'unsigned short'; this va_arg has undefined behavior because arguments will be promoted to 'int' 643 | arg = [NSNumber numberWithUnsignedShort:(unsigned short)(va_arg(args, uint))]; 644 | } 645 | else { 646 | i--; 647 | } 648 | break; 649 | case 'q': 650 | i++; 651 | if (i < length && [sql characterAtIndex:i] == 'i') { 652 | arg = [NSNumber numberWithLongLong:va_arg(args, long long)]; 653 | } 654 | else if (i < length && [sql characterAtIndex:i] == 'u') { 655 | arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)]; 656 | } 657 | else { 658 | i--; 659 | } 660 | break; 661 | case 'f': 662 | arg = [NSNumber numberWithDouble:va_arg(args, double)]; 663 | break; 664 | case 'g': 665 | // warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double' 666 | arg = [NSNumber numberWithFloat:(float)(va_arg(args, double))]; 667 | break; 668 | case 'l': 669 | i++; 670 | if (i < length) { 671 | unichar next = [sql characterAtIndex:i]; 672 | if (next == 'l') { 673 | i++; 674 | if (i < length && [sql characterAtIndex:i] == 'd') { 675 | //%lld 676 | arg = [NSNumber numberWithLongLong:va_arg(args, long long)]; 677 | } 678 | else if (i < length && [sql characterAtIndex:i] == 'u') { 679 | //%llu 680 | arg = [NSNumber numberWithUnsignedLongLong:va_arg(args, unsigned long long)]; 681 | } 682 | else { 683 | i--; 684 | } 685 | } 686 | else if (next == 'd') { 687 | //%ld 688 | arg = [NSNumber numberWithLong:va_arg(args, long)]; 689 | } 690 | else if (next == 'u') { 691 | //%lu 692 | arg = [NSNumber numberWithUnsignedLong:va_arg(args, unsigned long)]; 693 | } 694 | else { 695 | i--; 696 | } 697 | } 698 | else { 699 | i--; 700 | } 701 | break; 702 | default: 703 | // something else that we can't interpret. just pass it on through like normal 704 | break; 705 | } 706 | } 707 | else if (current == '%') { 708 | // percent sign; skip this character 709 | add = '\0'; 710 | } 711 | 712 | if (arg != nil) { 713 | [cleanedSQL appendString:@"?"]; 714 | [arguments addObject:arg]; 715 | } 716 | else if (add == (unichar)'@' && last == (unichar) '%') { 717 | [cleanedSQL appendFormat:@"NULL"]; 718 | } 719 | else if (add != '\0') { 720 | [cleanedSQL appendFormat:@"%C", add]; 721 | } 722 | last = current; 723 | } 724 | } 725 | 726 | #pragma mark Execute queries 727 | 728 | - (FMResultSet *)executeQuery:(NSString *)sql withParameterDictionary:(NSDictionary *)arguments { 729 | return [self executeQuery:sql withArgumentsInArray:nil orDictionary:arguments orVAList:nil]; 730 | } 731 | 732 | - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { 733 | 734 | if (![self databaseExists]) { 735 | return 0x00; 736 | } 737 | 738 | if (_isExecutingStatement) { 739 | [self warnInUse]; 740 | return 0x00; 741 | } 742 | 743 | _isExecutingStatement = YES; 744 | 745 | int rc = 0x00; 746 | sqlite3_stmt *pStmt = 0x00; 747 | FMStatement *statement = 0x00; 748 | FMResultSet *rs = 0x00; 749 | 750 | if (_traceExecution && sql) { 751 | NSLog(@"%@ executeQuery: %@", self, sql); 752 | } 753 | 754 | if (_shouldCacheStatements) { 755 | statement = [self cachedStatementForQuery:sql]; 756 | pStmt = statement ? [statement statement] : 0x00; 757 | [statement reset]; 758 | } 759 | 760 | if (!pStmt) { 761 | 762 | rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0); 763 | 764 | if (SQLITE_OK != rc) { 765 | if (_logsErrors) { 766 | NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); 767 | NSLog(@"DB Query: %@", sql); 768 | NSLog(@"DB Path: %@", _databasePath); 769 | } 770 | 771 | if (_crashOnErrors) { 772 | NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); 773 | abort(); 774 | } 775 | 776 | sqlite3_finalize(pStmt); 777 | _isExecutingStatement = NO; 778 | return nil; 779 | } 780 | } 781 | 782 | id obj; 783 | int idx = 0; 784 | int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!) 785 | 786 | // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support 787 | if (dictionaryArgs) { 788 | 789 | for (NSString *dictionaryKey in [dictionaryArgs allKeys]) { 790 | 791 | // Prefix the key with a colon. 792 | NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey]; 793 | 794 | if (_traceExecution) { 795 | NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]); 796 | } 797 | 798 | // Get the index for the parameter name. 799 | int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]); 800 | 801 | FMDBRelease(parameterName); 802 | 803 | if (namedIdx > 0) { 804 | // Standard binding from here. 805 | [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt]; 806 | // increment the binding count, so our check below works out 807 | idx++; 808 | } 809 | else { 810 | NSLog(@"Could not find index for %@", dictionaryKey); 811 | } 812 | } 813 | } 814 | else { 815 | 816 | while (idx < queryCount) { 817 | 818 | if (arrayArgs && idx < (int)[arrayArgs count]) { 819 | obj = [arrayArgs objectAtIndex:(NSUInteger)idx]; 820 | } 821 | else if (args) { 822 | obj = va_arg(args, id); 823 | } 824 | else { 825 | //We ran out of arguments 826 | break; 827 | } 828 | 829 | if (_traceExecution) { 830 | if ([obj isKindOfClass:[NSData class]]) { 831 | NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]); 832 | } 833 | else { 834 | NSLog(@"obj: %@", obj); 835 | } 836 | } 837 | 838 | idx++; 839 | 840 | [self bindObject:obj toColumn:idx inStatement:pStmt]; 841 | } 842 | } 843 | 844 | if (idx != queryCount) { 845 | NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)"); 846 | sqlite3_finalize(pStmt); 847 | _isExecutingStatement = NO; 848 | return nil; 849 | } 850 | 851 | FMDBRetain(statement); // to balance the release below 852 | 853 | if (!statement) { 854 | statement = [[FMStatement alloc] init]; 855 | [statement setStatement:pStmt]; 856 | 857 | if (_shouldCacheStatements && sql) { 858 | [self setCachedStatement:statement forQuery:sql]; 859 | } 860 | } 861 | 862 | // the statement gets closed in rs's dealloc or [rs close]; 863 | rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self]; 864 | [rs setQuery:sql]; 865 | 866 | NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs]; 867 | [_openResultSets addObject:openResultSet]; 868 | 869 | [statement setUseCount:[statement useCount] + 1]; 870 | 871 | FMDBRelease(statement); 872 | 873 | _isExecutingStatement = NO; 874 | 875 | return rs; 876 | } 877 | 878 | - (FMResultSet *)executeQuery:(NSString*)sql, ... { 879 | va_list args; 880 | va_start(args, sql); 881 | 882 | id result = [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args]; 883 | 884 | va_end(args); 885 | return result; 886 | } 887 | 888 | - (FMResultSet *)executeQueryWithFormat:(NSString*)format, ... { 889 | va_list args; 890 | va_start(args, format); 891 | 892 | NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]]; 893 | NSMutableArray *arguments = [NSMutableArray array]; 894 | [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; 895 | 896 | va_end(args); 897 | 898 | return [self executeQuery:sql withArgumentsInArray:arguments]; 899 | } 900 | 901 | - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray *)arguments { 902 | return [self executeQuery:sql withArgumentsInArray:arguments orDictionary:nil orVAList:nil]; 903 | } 904 | 905 | - (FMResultSet *)executeQuery:(NSString*)sql withVAList:(va_list)args { 906 | return [self executeQuery:sql withArgumentsInArray:nil orDictionary:nil orVAList:args]; 907 | } 908 | 909 | #pragma mark Execute updates 910 | 911 | - (BOOL)executeUpdate:(NSString*)sql error:(NSError**)outErr withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args { 912 | 913 | if (![self databaseExists]) { 914 | return NO; 915 | } 916 | 917 | if (_isExecutingStatement) { 918 | [self warnInUse]; 919 | return NO; 920 | } 921 | 922 | _isExecutingStatement = YES; 923 | 924 | int rc = 0x00; 925 | sqlite3_stmt *pStmt = 0x00; 926 | FMStatement *cachedStmt = 0x00; 927 | 928 | if (_traceExecution && sql) { 929 | NSLog(@"%@ executeUpdate: %@", self, sql); 930 | } 931 | 932 | if (_shouldCacheStatements) { 933 | cachedStmt = [self cachedStatementForQuery:sql]; 934 | pStmt = cachedStmt ? [cachedStmt statement] : 0x00; 935 | [cachedStmt reset]; 936 | } 937 | 938 | if (!pStmt) { 939 | rc = sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pStmt, 0); 940 | 941 | if (SQLITE_OK != rc) { 942 | if (_logsErrors) { 943 | NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); 944 | NSLog(@"DB Query: %@", sql); 945 | NSLog(@"DB Path: %@", _databasePath); 946 | } 947 | 948 | if (_crashOnErrors) { 949 | NSAssert(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]); 950 | abort(); 951 | } 952 | 953 | sqlite3_finalize(pStmt); 954 | 955 | if (outErr) { 956 | *outErr = [self errorWithMessage:[NSString stringWithUTF8String:sqlite3_errmsg(_db)]]; 957 | } 958 | 959 | _isExecutingStatement = NO; 960 | return NO; 961 | } 962 | } 963 | 964 | id obj; 965 | int idx = 0; 966 | int queryCount = sqlite3_bind_parameter_count(pStmt); 967 | 968 | // If dictionaryArgs is passed in, that means we are using sqlite's named parameter support 969 | if (dictionaryArgs) { 970 | 971 | for (NSString *dictionaryKey in [dictionaryArgs allKeys]) { 972 | 973 | // Prefix the key with a colon. 974 | NSString *parameterName = [[NSString alloc] initWithFormat:@":%@", dictionaryKey]; 975 | 976 | if (_traceExecution) { 977 | NSLog(@"%@ = %@", parameterName, [dictionaryArgs objectForKey:dictionaryKey]); 978 | } 979 | // Get the index for the parameter name. 980 | int namedIdx = sqlite3_bind_parameter_index(pStmt, [parameterName UTF8String]); 981 | 982 | FMDBRelease(parameterName); 983 | 984 | if (namedIdx > 0) { 985 | // Standard binding from here. 986 | [self bindObject:[dictionaryArgs objectForKey:dictionaryKey] toColumn:namedIdx inStatement:pStmt]; 987 | 988 | // increment the binding count, so our check below works out 989 | idx++; 990 | } 991 | else { 992 | NSLog(@"Could not find index for %@", dictionaryKey); 993 | } 994 | } 995 | } 996 | else { 997 | 998 | while (idx < queryCount) { 999 | 1000 | if (arrayArgs && idx < (int)[arrayArgs count]) { 1001 | obj = [arrayArgs objectAtIndex:(NSUInteger)idx]; 1002 | } 1003 | else if (args) { 1004 | obj = va_arg(args, id); 1005 | } 1006 | else { 1007 | //We ran out of arguments 1008 | break; 1009 | } 1010 | 1011 | if (_traceExecution) { 1012 | if ([obj isKindOfClass:[NSData class]]) { 1013 | NSLog(@"data: %ld bytes", (unsigned long)[(NSData*)obj length]); 1014 | } 1015 | else { 1016 | NSLog(@"obj: %@", obj); 1017 | } 1018 | } 1019 | 1020 | idx++; 1021 | 1022 | [self bindObject:obj toColumn:idx inStatement:pStmt]; 1023 | } 1024 | } 1025 | 1026 | 1027 | if (idx != queryCount) { 1028 | NSLog(@"Error: the bind count (%d) is not correct for the # of variables in the query (%d) (%@) (executeUpdate)", idx, queryCount, sql); 1029 | sqlite3_finalize(pStmt); 1030 | _isExecutingStatement = NO; 1031 | return NO; 1032 | } 1033 | 1034 | /* Call sqlite3_step() to run the virtual machine. Since the SQL being 1035 | ** executed is not a SELECT statement, we assume no data will be returned. 1036 | */ 1037 | 1038 | rc = sqlite3_step(pStmt); 1039 | 1040 | if (SQLITE_DONE == rc) { 1041 | // all is well, let's return. 1042 | } 1043 | else if (SQLITE_ERROR == rc) { 1044 | if (_logsErrors) { 1045 | NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_ERROR", rc, sqlite3_errmsg(_db)); 1046 | NSLog(@"DB Query: %@", sql); 1047 | } 1048 | } 1049 | else if (SQLITE_MISUSE == rc) { 1050 | // uh oh. 1051 | if (_logsErrors) { 1052 | NSLog(@"Error calling sqlite3_step (%d: %s) SQLITE_MISUSE", rc, sqlite3_errmsg(_db)); 1053 | NSLog(@"DB Query: %@", sql); 1054 | } 1055 | } 1056 | else { 1057 | // wtf? 1058 | if (_logsErrors) { 1059 | NSLog(@"Unknown error calling sqlite3_step (%d: %s) eu", rc, sqlite3_errmsg(_db)); 1060 | NSLog(@"DB Query: %@", sql); 1061 | } 1062 | } 1063 | 1064 | if (rc == SQLITE_ROW) { 1065 | NSAssert(NO, @"A executeUpdate is being called with a query string '%@'", sql); 1066 | } 1067 | 1068 | if (_shouldCacheStatements && !cachedStmt) { 1069 | cachedStmt = [[FMStatement alloc] init]; 1070 | 1071 | [cachedStmt setStatement:pStmt]; 1072 | 1073 | [self setCachedStatement:cachedStmt forQuery:sql]; 1074 | 1075 | FMDBRelease(cachedStmt); 1076 | } 1077 | 1078 | int closeErrorCode; 1079 | 1080 | if (cachedStmt) { 1081 | [cachedStmt setUseCount:[cachedStmt useCount] + 1]; 1082 | closeErrorCode = sqlite3_reset(pStmt); 1083 | } 1084 | else { 1085 | /* Finalize the virtual machine. This releases all memory and other 1086 | ** resources allocated by the sqlite3_prepare() call above. 1087 | */ 1088 | closeErrorCode = sqlite3_finalize(pStmt); 1089 | } 1090 | 1091 | if (closeErrorCode != SQLITE_OK) { 1092 | if (_logsErrors) { 1093 | NSLog(@"Unknown error finalizing or resetting statement (%d: %s)", closeErrorCode, sqlite3_errmsg(_db)); 1094 | NSLog(@"DB Query: %@", sql); 1095 | } 1096 | } 1097 | 1098 | _isExecutingStatement = NO; 1099 | return (rc == SQLITE_DONE || rc == SQLITE_OK); 1100 | } 1101 | 1102 | 1103 | - (BOOL)executeUpdate:(NSString*)sql, ... { 1104 | va_list args; 1105 | va_start(args, sql); 1106 | 1107 | BOOL result = [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args]; 1108 | 1109 | va_end(args); 1110 | return result; 1111 | } 1112 | 1113 | - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments { 1114 | return [self executeUpdate:sql error:nil withArgumentsInArray:arguments orDictionary:nil orVAList:nil]; 1115 | } 1116 | 1117 | - (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments { 1118 | return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:arguments orVAList:nil]; 1119 | } 1120 | 1121 | - (BOOL)executeUpdate:(NSString*)sql withVAList:(va_list)args { 1122 | return [self executeUpdate:sql error:nil withArgumentsInArray:nil orDictionary:nil orVAList:args]; 1123 | } 1124 | 1125 | - (BOOL)executeUpdateWithFormat:(NSString*)format, ... { 1126 | va_list args; 1127 | va_start(args, format); 1128 | 1129 | NSMutableString *sql = [NSMutableString stringWithCapacity:[format length]]; 1130 | NSMutableArray *arguments = [NSMutableArray array]; 1131 | 1132 | [self extractSQL:format argumentsList:args intoString:sql arguments:arguments]; 1133 | 1134 | va_end(args); 1135 | 1136 | return [self executeUpdate:sql withArgumentsInArray:arguments]; 1137 | } 1138 | 1139 | 1140 | int FMDBExecuteBulkSQLCallback(void *theBlockAsVoid, int columns, char **values, char **names); // shhh clang. 1141 | int FMDBExecuteBulkSQLCallback(void *theBlockAsVoid, int columns, char **values, char **names) { 1142 | 1143 | if (!theBlockAsVoid) { 1144 | return SQLITE_OK; 1145 | } 1146 | 1147 | int (^execCallbackBlock)(NSDictionary *resultsDictionary) = (__bridge int (^)(NSDictionary *__strong))(theBlockAsVoid); 1148 | 1149 | NSMutableDictionary *dictionary = [NSMutableDictionary dictionaryWithCapacity:(NSUInteger)columns]; 1150 | 1151 | for (NSInteger i = 0; i < columns; i++) { 1152 | NSString *key = [NSString stringWithUTF8String:names[i]]; 1153 | id value = values[i] ? [NSString stringWithUTF8String:values[i]] : [NSNull null]; 1154 | [dictionary setObject:value forKey:key]; 1155 | } 1156 | 1157 | return execCallbackBlock(dictionary); 1158 | } 1159 | 1160 | - (BOOL)executeStatements:(NSString *)sql { 1161 | return [self executeStatements:sql withResultBlock:nil]; 1162 | } 1163 | 1164 | - (BOOL)executeStatements:(NSString *)sql withResultBlock:(FMDBExecuteStatementsCallbackBlock)block { 1165 | 1166 | int rc; 1167 | char *errmsg = nil; 1168 | 1169 | rc = sqlite3_exec([self sqliteHandle], [sql UTF8String], block ? FMDBExecuteBulkSQLCallback : nil, (__bridge void *)(block), &errmsg); 1170 | 1171 | if (errmsg && [self logsErrors]) { 1172 | NSLog(@"Error inserting batch: %s", errmsg); 1173 | sqlite3_free(errmsg); 1174 | } 1175 | 1176 | return (rc == SQLITE_OK); 1177 | } 1178 | 1179 | - (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ... { 1180 | 1181 | va_list args; 1182 | va_start(args, outErr); 1183 | 1184 | BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:args]; 1185 | 1186 | va_end(args); 1187 | return result; 1188 | } 1189 | 1190 | 1191 | #pragma clang diagnostic push 1192 | #pragma clang diagnostic ignored "-Wdeprecated-implementations" 1193 | - (BOOL)update:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ... { 1194 | va_list args; 1195 | va_start(args, outErr); 1196 | 1197 | BOOL result = [self executeUpdate:sql error:outErr withArgumentsInArray:nil orDictionary:nil orVAList:args]; 1198 | 1199 | va_end(args); 1200 | return result; 1201 | } 1202 | 1203 | #pragma clang diagnostic pop 1204 | 1205 | #pragma mark Transactions 1206 | 1207 | - (BOOL)rollback { 1208 | BOOL b = [self executeUpdate:@"rollback transaction"]; 1209 | 1210 | if (b) { 1211 | _inTransaction = NO; 1212 | } 1213 | 1214 | return b; 1215 | } 1216 | 1217 | - (BOOL)commit { 1218 | BOOL b = [self executeUpdate:@"commit transaction"]; 1219 | 1220 | if (b) { 1221 | _inTransaction = NO; 1222 | } 1223 | 1224 | return b; 1225 | } 1226 | 1227 | - (BOOL)beginDeferredTransaction { 1228 | 1229 | BOOL b = [self executeUpdate:@"begin deferred transaction"]; 1230 | if (b) { 1231 | _inTransaction = YES; 1232 | } 1233 | 1234 | return b; 1235 | } 1236 | 1237 | - (BOOL)beginTransaction { 1238 | 1239 | BOOL b = [self executeUpdate:@"begin exclusive transaction"]; 1240 | if (b) { 1241 | _inTransaction = YES; 1242 | } 1243 | 1244 | return b; 1245 | } 1246 | 1247 | - (BOOL)inTransaction { 1248 | return _inTransaction; 1249 | } 1250 | 1251 | #if SQLITE_VERSION_NUMBER >= 3007000 1252 | 1253 | static NSString *FMDBEscapeSavePointName(NSString *savepointName) { 1254 | return [savepointName stringByReplacingOccurrencesOfString:@"'" withString:@"''"]; 1255 | } 1256 | 1257 | - (BOOL)startSavePointWithName:(NSString*)name error:(NSError**)outErr { 1258 | 1259 | NSParameterAssert(name); 1260 | 1261 | NSString *sql = [NSString stringWithFormat:@"savepoint '%@';", FMDBEscapeSavePointName(name)]; 1262 | 1263 | if (![self executeUpdate:sql]) { 1264 | 1265 | if (outErr) { 1266 | *outErr = [self lastError]; 1267 | } 1268 | 1269 | return NO; 1270 | } 1271 | 1272 | return YES; 1273 | } 1274 | 1275 | - (BOOL)releaseSavePointWithName:(NSString*)name error:(NSError**)outErr { 1276 | 1277 | NSParameterAssert(name); 1278 | 1279 | NSString *sql = [NSString stringWithFormat:@"release savepoint '%@';", FMDBEscapeSavePointName(name)]; 1280 | BOOL worked = [self executeUpdate:sql]; 1281 | 1282 | if (!worked && outErr) { 1283 | *outErr = [self lastError]; 1284 | } 1285 | 1286 | return worked; 1287 | } 1288 | 1289 | - (BOOL)rollbackToSavePointWithName:(NSString*)name error:(NSError**)outErr { 1290 | 1291 | NSParameterAssert(name); 1292 | 1293 | NSString *sql = [NSString stringWithFormat:@"rollback transaction to savepoint '%@';", FMDBEscapeSavePointName(name)]; 1294 | BOOL worked = [self executeUpdate:sql]; 1295 | 1296 | if (!worked && outErr) { 1297 | *outErr = [self lastError]; 1298 | } 1299 | 1300 | return worked; 1301 | } 1302 | 1303 | - (NSError*)inSavePoint:(void (^)(BOOL *rollback))block { 1304 | static unsigned long savePointIdx = 0; 1305 | 1306 | NSString *name = [NSString stringWithFormat:@"dbSavePoint%ld", savePointIdx++]; 1307 | 1308 | BOOL shouldRollback = NO; 1309 | 1310 | NSError *err = 0x00; 1311 | 1312 | if (![self startSavePointWithName:name error:&err]) { 1313 | return err; 1314 | } 1315 | 1316 | if (block) { 1317 | block(&shouldRollback); 1318 | } 1319 | 1320 | if (shouldRollback) { 1321 | // We need to rollback and release this savepoint to remove it 1322 | [self rollbackToSavePointWithName:name error:&err]; 1323 | } 1324 | [self releaseSavePointWithName:name error:&err]; 1325 | 1326 | return err; 1327 | } 1328 | 1329 | #endif 1330 | 1331 | #pragma mark Cache statements 1332 | 1333 | - (BOOL)shouldCacheStatements { 1334 | return _shouldCacheStatements; 1335 | } 1336 | 1337 | - (void)setShouldCacheStatements:(BOOL)value { 1338 | 1339 | _shouldCacheStatements = value; 1340 | 1341 | if (_shouldCacheStatements && !_cachedStatements) { 1342 | [self setCachedStatements:[NSMutableDictionary dictionary]]; 1343 | } 1344 | 1345 | if (!_shouldCacheStatements) { 1346 | [self setCachedStatements:nil]; 1347 | } 1348 | } 1349 | 1350 | #pragma mark Callback function 1351 | 1352 | void FMDBBlockSQLiteCallBackFunction(sqlite3_context *context, int argc, sqlite3_value **argv); // -Wmissing-prototypes 1353 | void FMDBBlockSQLiteCallBackFunction(sqlite3_context *context, int argc, sqlite3_value **argv) { 1354 | #if ! __has_feature(objc_arc) 1355 | void (^block)(sqlite3_context *context, int argc, sqlite3_value **argv) = (id)sqlite3_user_data(context); 1356 | #else 1357 | void (^block)(sqlite3_context *context, int argc, sqlite3_value **argv) = (__bridge id)sqlite3_user_data(context); 1358 | #endif 1359 | if (block) { 1360 | block(context, argc, argv); 1361 | } 1362 | } 1363 | 1364 | 1365 | - (void)makeFunctionNamed:(NSString*)name maximumArguments:(int)count withBlock:(void (^)(void *context, int argc, void **argv))block { 1366 | 1367 | if (!_openFunctions) { 1368 | _openFunctions = [NSMutableSet new]; 1369 | } 1370 | 1371 | id b = FMDBReturnAutoreleased([block copy]); 1372 | 1373 | [_openFunctions addObject:b]; 1374 | 1375 | /* I tried adding custom functions to release the block when the connection is destroyed- but they seemed to never be called, so we use _openFunctions to store the values instead. */ 1376 | #if ! __has_feature(objc_arc) 1377 | sqlite3_create_function([self sqliteHandle], [name UTF8String], count, SQLITE_UTF8, (void*)b, &FMDBBlockSQLiteCallBackFunction, 0x00, 0x00); 1378 | #else 1379 | sqlite3_create_function([self sqliteHandle], [name UTF8String], count, SQLITE_UTF8, (__bridge void*)b, &FMDBBlockSQLiteCallBackFunction, 0x00, 0x00); 1380 | #endif 1381 | } 1382 | 1383 | @end 1384 | 1385 | 1386 | 1387 | @implementation FMStatement { 1388 | sqlite3_stmt *_statement; 1389 | } 1390 | 1391 | @synthesize statement=_statement; 1392 | @synthesize query=_query; 1393 | @synthesize useCount=_useCount; 1394 | @synthesize inUse=_inUse; 1395 | 1396 | - (void)dealloc { 1397 | [self close]; 1398 | FMDBRelease(_query); 1399 | #if ! __has_feature(objc_arc) 1400 | [super dealloc]; 1401 | #endif 1402 | } 1403 | 1404 | - (void)close { 1405 | if (_statement) { 1406 | sqlite3_finalize(_statement); 1407 | _statement = 0x00; 1408 | } 1409 | 1410 | _inUse = NO; 1411 | } 1412 | 1413 | - (void)reset { 1414 | if (_statement) { 1415 | sqlite3_reset(_statement); 1416 | } 1417 | 1418 | _inUse = NO; 1419 | } 1420 | 1421 | - (NSString*)description { 1422 | return [NSString stringWithFormat:@"%@ %ld hit(s) for query %@", [super description], _useCount, _query]; 1423 | } 1424 | 1425 | 1426 | @end 1427 | 1428 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMDatabaseAdditions.h: -------------------------------------------------------------------------------- 1 | // 2 | // FMDatabaseAdditions.h 3 | // fmdb 4 | // 5 | // Created by August Mueller on 10/30/05. 6 | // Copyright 2005 Flying Meat Inc.. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "FMDatabase.h" 11 | 12 | 13 | /** Category of additions for `` class. 14 | 15 | ### See also 16 | 17 | - `` 18 | */ 19 | 20 | @interface FMDatabase (FMDatabaseAdditions) 21 | 22 | ///---------------------------------------- 23 | /// @name Return results of SQL to variable 24 | ///---------------------------------------- 25 | 26 | /** Return `int` value for query 27 | 28 | @param query The SQL query to be performed. 29 | @param ... A list of parameters that will be bound to the `?` placeholders in the SQL query. 30 | 31 | @return `int` value. 32 | 33 | @note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project. 34 | */ 35 | 36 | - (int)intForQuery:(NSString*)query, ...; 37 | 38 | /** Return `long` value for query 39 | 40 | @param query The SQL query to be performed. 41 | @param ... A list of parameters that will be bound to the `?` placeholders in the SQL query. 42 | 43 | @return `long` value. 44 | 45 | @note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project. 46 | */ 47 | 48 | - (long)longForQuery:(NSString*)query, ...; 49 | 50 | /** Return `BOOL` value for query 51 | 52 | @param query The SQL query to be performed. 53 | @param ... A list of parameters that will be bound to the `?` placeholders in the SQL query. 54 | 55 | @return `BOOL` value. 56 | 57 | @note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project. 58 | */ 59 | 60 | - (BOOL)boolForQuery:(NSString*)query, ...; 61 | 62 | /** Return `double` value for query 63 | 64 | @param query The SQL query to be performed. 65 | @param ... A list of parameters that will be bound to the `?` placeholders in the SQL query. 66 | 67 | @return `double` value. 68 | 69 | @note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project. 70 | */ 71 | 72 | - (double)doubleForQuery:(NSString*)query, ...; 73 | 74 | /** Return `NSString` value for query 75 | 76 | @param query The SQL query to be performed. 77 | @param ... A list of parameters that will be bound to the `?` placeholders in the SQL query. 78 | 79 | @return `NSString` value. 80 | 81 | @note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project. 82 | */ 83 | 84 | - (NSString*)stringForQuery:(NSString*)query, ...; 85 | 86 | /** Return `NSData` value for query 87 | 88 | @param query The SQL query to be performed. 89 | @param ... A list of parameters that will be bound to the `?` placeholders in the SQL query. 90 | 91 | @return `NSData` value. 92 | 93 | @note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project. 94 | */ 95 | 96 | - (NSData*)dataForQuery:(NSString*)query, ...; 97 | 98 | /** Return `NSDate` value for query 99 | 100 | @param query The SQL query to be performed. 101 | @param ... A list of parameters that will be bound to the `?` placeholders in the SQL query. 102 | 103 | @return `NSDate` value. 104 | 105 | @note To use this method from Swift, you must include `FMDatabaseAdditionsVariadic.swift` in your project. 106 | */ 107 | 108 | - (NSDate*)dateForQuery:(NSString*)query, ...; 109 | 110 | 111 | // Notice that there's no dataNoCopyForQuery:. 112 | // That would be a bad idea, because we close out the result set, and then what 113 | // happens to the data that we just didn't copy? Who knows, not I. 114 | 115 | 116 | ///-------------------------------- 117 | /// @name Schema related operations 118 | ///-------------------------------- 119 | 120 | /** Does table exist in database? 121 | 122 | @param tableName The name of the table being looked for. 123 | 124 | @return `YES` if table found; `NO` if not found. 125 | */ 126 | 127 | - (BOOL)tableExists:(NSString*)tableName; 128 | 129 | /** The schema of the database. 130 | 131 | This will be the schema for the entire database. For each entity, each row of the result set will include the following fields: 132 | 133 | - `type` - The type of entity (e.g. table, index, view, or trigger) 134 | - `name` - The name of the object 135 | - `tbl_name` - The name of the table to which the object references 136 | - `rootpage` - The page number of the root b-tree page for tables and indices 137 | - `sql` - The SQL that created the entity 138 | 139 | @return `FMResultSet` of schema; `nil` on error. 140 | 141 | @see [SQLite File Format](http://www.sqlite.org/fileformat.html) 142 | */ 143 | 144 | - (FMResultSet*)getSchema; 145 | 146 | /** The schema of the database. 147 | 148 | This will be the schema for a particular table as report by SQLite `PRAGMA`, for example: 149 | 150 | PRAGMA table_info('employees') 151 | 152 | This will report: 153 | 154 | - `cid` - The column ID number 155 | - `name` - The name of the column 156 | - `type` - The data type specified for the column 157 | - `notnull` - whether the field is defined as NOT NULL (i.e. values required) 158 | - `dflt_value` - The default value for the column 159 | - `pk` - Whether the field is part of the primary key of the table 160 | 161 | @param tableName The name of the table for whom the schema will be returned. 162 | 163 | @return `FMResultSet` of schema; `nil` on error. 164 | 165 | @see [table_info](http://www.sqlite.org/pragma.html#pragma_table_info) 166 | */ 167 | 168 | - (FMResultSet*)getTableSchema:(NSString*)tableName; 169 | 170 | /** Test to see if particular column exists for particular table in database 171 | 172 | @param columnName The name of the column. 173 | 174 | @param tableName The name of the table. 175 | 176 | @return `YES` if column exists in table in question; `NO` otherwise. 177 | */ 178 | 179 | - (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName; 180 | 181 | /** Test to see if particular column exists for particular table in database 182 | 183 | @param columnName The name of the column. 184 | 185 | @param tableName The name of the table. 186 | 187 | @return `YES` if column exists in table in question; `NO` otherwise. 188 | 189 | @see columnExists:inTableWithName: 190 | 191 | @warning Deprecated - use `` instead. 192 | */ 193 | 194 | - (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated)); 195 | 196 | 197 | /** Validate SQL statement 198 | 199 | This validates SQL statement by performing `sqlite3_prepare_v2`, but not returning the results, but instead immediately calling `sqlite3_finalize`. 200 | 201 | @param sql The SQL statement being validated. 202 | 203 | @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. 204 | 205 | @return `YES` if validation succeeded without incident; `NO` otherwise. 206 | 207 | */ 208 | 209 | - (BOOL)validateSQL:(NSString*)sql error:(NSError**)error; 210 | 211 | 212 | #if SQLITE_VERSION_NUMBER >= 3007017 213 | 214 | ///----------------------------------- 215 | /// @name Application identifier tasks 216 | ///----------------------------------- 217 | 218 | /** Retrieve application ID 219 | 220 | @return The `uint32_t` numeric value of the application ID. 221 | 222 | @see setApplicationID: 223 | */ 224 | 225 | - (uint32_t)applicationID; 226 | 227 | /** Set the application ID 228 | 229 | @param appID The `uint32_t` numeric value of the application ID. 230 | 231 | @see applicationID 232 | */ 233 | 234 | - (void)setApplicationID:(uint32_t)appID; 235 | 236 | #if TARGET_OS_MAC && !TARGET_OS_IPHONE 237 | /** Retrieve application ID string 238 | 239 | @return The `NSString` value of the application ID. 240 | 241 | @see setApplicationIDString: 242 | */ 243 | 244 | 245 | - (NSString*)applicationIDString; 246 | 247 | /** Set the application ID string 248 | 249 | @param string The `NSString` value of the application ID. 250 | 251 | @see applicationIDString 252 | */ 253 | 254 | - (void)setApplicationIDString:(NSString*)string; 255 | #endif 256 | 257 | #endif 258 | 259 | ///----------------------------------- 260 | /// @name user version identifier tasks 261 | ///----------------------------------- 262 | 263 | /** Retrieve user version 264 | 265 | @return The `uint32_t` numeric value of the user version. 266 | 267 | @see setUserVersion: 268 | */ 269 | 270 | - (uint32_t)userVersion; 271 | 272 | /** Set the user-version 273 | 274 | @param version The `uint32_t` numeric value of the user version. 275 | 276 | @see userVersion 277 | */ 278 | 279 | - (void)setUserVersion:(uint32_t)version; 280 | 281 | @end 282 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMDatabaseAdditions.m: -------------------------------------------------------------------------------- 1 | // 2 | // FMDatabaseAdditions.m 3 | // fmdb 4 | // 5 | // Created by August Mueller on 10/30/05. 6 | // Copyright 2005 Flying Meat Inc.. All rights reserved. 7 | // 8 | 9 | #import "FMDatabase.h" 10 | #import "FMDatabaseAdditions.h" 11 | #import "TargetConditionals.h" 12 | #import "sqlite3.h" 13 | 14 | @interface FMDatabase (PrivateStuff) 15 | - (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orDictionary:(NSDictionary *)dictionaryArgs orVAList:(va_list)args; 16 | @end 17 | 18 | @implementation FMDatabase (FMDatabaseAdditions) 19 | 20 | #define RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(type, sel) \ 21 | va_list args; \ 22 | va_start(args, query); \ 23 | FMResultSet *resultSet = [self executeQuery:query withArgumentsInArray:0x00 orDictionary:0x00 orVAList:args]; \ 24 | va_end(args); \ 25 | if (![resultSet next]) { return (type)0; } \ 26 | type ret = [resultSet sel:0]; \ 27 | [resultSet close]; \ 28 | [resultSet setParentDB:nil]; \ 29 | return ret; 30 | 31 | 32 | - (NSString*)stringForQuery:(NSString*)query, ... { 33 | RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSString *, stringForColumnIndex); 34 | } 35 | 36 | - (int)intForQuery:(NSString*)query, ... { 37 | RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(int, intForColumnIndex); 38 | } 39 | 40 | - (long)longForQuery:(NSString*)query, ... { 41 | RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(long, longForColumnIndex); 42 | } 43 | 44 | - (BOOL)boolForQuery:(NSString*)query, ... { 45 | RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(BOOL, boolForColumnIndex); 46 | } 47 | 48 | - (double)doubleForQuery:(NSString*)query, ... { 49 | RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(double, doubleForColumnIndex); 50 | } 51 | 52 | - (NSData*)dataForQuery:(NSString*)query, ... { 53 | RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSData *, dataForColumnIndex); 54 | } 55 | 56 | - (NSDate*)dateForQuery:(NSString*)query, ... { 57 | RETURN_RESULT_FOR_QUERY_WITH_SELECTOR(NSDate *, dateForColumnIndex); 58 | } 59 | 60 | 61 | - (BOOL)tableExists:(NSString*)tableName { 62 | 63 | tableName = [tableName lowercaseString]; 64 | 65 | FMResultSet *rs = [self executeQuery:@"select [sql] from sqlite_master where [type] = 'table' and lower(name) = ?", tableName]; 66 | 67 | //if at least one next exists, table exists 68 | BOOL returnBool = [rs next]; 69 | 70 | //close and free object 71 | [rs close]; 72 | 73 | return returnBool; 74 | } 75 | 76 | /* 77 | get table with list of tables: result columns: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING] 78 | check if table exist in database (patch from OZLB) 79 | */ 80 | - (FMResultSet*)getSchema { 81 | 82 | //result columns: type[STRING], name[STRING],tbl_name[STRING],rootpage[INTEGER],sql[STRING] 83 | 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"]; 84 | 85 | return rs; 86 | } 87 | 88 | /* 89 | get table schema: result columns: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER] 90 | */ 91 | - (FMResultSet*)getTableSchema:(NSString*)tableName { 92 | 93 | //result columns: cid[INTEGER], name,type [STRING], notnull[INTEGER], dflt_value[],pk[INTEGER] 94 | FMResultSet *rs = [self executeQuery:[NSString stringWithFormat: @"pragma table_info('%@')", tableName]]; 95 | 96 | return rs; 97 | } 98 | 99 | - (BOOL)columnExists:(NSString*)columnName inTableWithName:(NSString*)tableName { 100 | 101 | BOOL returnBool = NO; 102 | 103 | tableName = [tableName lowercaseString]; 104 | columnName = [columnName lowercaseString]; 105 | 106 | FMResultSet *rs = [self getTableSchema:tableName]; 107 | 108 | //check if column is present in table schema 109 | while ([rs next]) { 110 | if ([[[rs stringForColumn:@"name"] lowercaseString] isEqualToString:columnName]) { 111 | returnBool = YES; 112 | break; 113 | } 114 | } 115 | 116 | //If this is not done FMDatabase instance stays out of pool 117 | [rs close]; 118 | 119 | return returnBool; 120 | } 121 | 122 | 123 | #if SQLITE_VERSION_NUMBER >= 3007017 124 | 125 | - (uint32_t)applicationID { 126 | 127 | uint32_t r = 0; 128 | 129 | FMResultSet *rs = [self executeQuery:@"pragma application_id"]; 130 | 131 | if ([rs next]) { 132 | r = (uint32_t)[rs longLongIntForColumnIndex:0]; 133 | } 134 | 135 | [rs close]; 136 | 137 | return r; 138 | } 139 | 140 | - (void)setApplicationID:(uint32_t)appID { 141 | NSString *query = [NSString stringWithFormat:@"pragma application_id=%d", appID]; 142 | FMResultSet *rs = [self executeQuery:query]; 143 | [rs next]; 144 | [rs close]; 145 | } 146 | 147 | 148 | #if TARGET_OS_MAC && !TARGET_OS_IPHONE 149 | - (NSString*)applicationIDString { 150 | NSString *s = NSFileTypeForHFSTypeCode([self applicationID]); 151 | 152 | assert([s length] == 6); 153 | 154 | s = [s substringWithRange:NSMakeRange(1, 4)]; 155 | 156 | 157 | return s; 158 | 159 | } 160 | 161 | - (void)setApplicationIDString:(NSString*)s { 162 | 163 | if ([s length] != 4) { 164 | NSLog(@"setApplicationIDString: string passed is not exactly 4 chars long. (was %ld)", [s length]); 165 | } 166 | 167 | [self setApplicationID:NSHFSTypeCodeFromFileType([NSString stringWithFormat:@"'%@'", s])]; 168 | } 169 | 170 | 171 | #endif 172 | 173 | #endif 174 | 175 | - (uint32_t)userVersion { 176 | uint32_t r = 0; 177 | 178 | FMResultSet *rs = [self executeQuery:@"pragma user_version"]; 179 | 180 | if ([rs next]) { 181 | r = (uint32_t)[rs longLongIntForColumnIndex:0]; 182 | } 183 | 184 | [rs close]; 185 | return r; 186 | } 187 | 188 | - (void)setUserVersion:(uint32_t)version { 189 | NSString *query = [NSString stringWithFormat:@"pragma user_version = %d", version]; 190 | FMResultSet *rs = [self executeQuery:query]; 191 | [rs next]; 192 | [rs close]; 193 | } 194 | 195 | #pragma clang diagnostic push 196 | #pragma clang diagnostic ignored "-Wdeprecated-implementations" 197 | 198 | - (BOOL)columnExists:(NSString*)tableName columnName:(NSString*)columnName __attribute__ ((deprecated)) { 199 | return [self columnExists:columnName inTableWithName:tableName]; 200 | } 201 | 202 | #pragma clang diagnostic pop 203 | 204 | 205 | - (BOOL)validateSQL:(NSString*)sql error:(NSError**)error { 206 | sqlite3_stmt *pStmt = NULL; 207 | BOOL validationSucceeded = YES; 208 | 209 | int rc = sqlite3_prepare_v2([self sqliteHandle], [sql UTF8String], -1, &pStmt, 0); 210 | if (rc != SQLITE_OK) { 211 | validationSucceeded = NO; 212 | if (error) { 213 | *error = [NSError errorWithDomain:NSCocoaErrorDomain 214 | code:[self lastErrorCode] 215 | userInfo:[NSDictionary dictionaryWithObject:[self lastErrorMessage] 216 | forKey:NSLocalizedDescriptionKey]]; 217 | } 218 | } 219 | 220 | sqlite3_finalize(pStmt); 221 | 222 | return validationSucceeded; 223 | } 224 | 225 | @end 226 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMResultSet+RSExtras.h: -------------------------------------------------------------------------------- 1 | // 2 | // FMResultSet+RSExtras.h 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 2/19/13. 6 | // Copyright (c) 2013 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | 10 | #import "FMResultSet.h" 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface FMResultSet (RSExtras) 15 | 16 | 17 | - (NSArray *)rs_arrayForSingleColumnResultSet; // Doesn't handle dates. 18 | 19 | - (NSSet *)rs_setForSingleColumnResultSet; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMResultSet+RSExtras.m: -------------------------------------------------------------------------------- 1 | // 2 | // FMResultSet+RSExtras.m 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 2/19/13. 6 | // Copyright (c) 2013 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "FMResultSet+RSExtras.h" 10 | 11 | 12 | @implementation FMResultSet (RSExtras) 13 | 14 | 15 | - (id)valueForKey:(NSString *)key { 16 | 17 | if ([key containsString:@"Date"] || [key containsString:@"date"]) { 18 | return [self dateForColumn:key]; 19 | } 20 | 21 | return [self objectForColumnName:key]; 22 | } 23 | 24 | 25 | - (NSArray *)rs_arrayForSingleColumnResultSet { 26 | 27 | NSMutableArray *results = [NSMutableArray new]; 28 | 29 | while ([self next]) { 30 | id oneObject = [self objectForColumnIndex:0]; 31 | [results addObject:oneObject]; 32 | } 33 | 34 | return [results copy]; 35 | } 36 | 37 | 38 | - (NSSet *)rs_setForSingleColumnResultSet { 39 | 40 | NSMutableSet *results = [NSMutableSet new]; 41 | 42 | while ([self next]) { 43 | id oneObject = [self objectForColumnIndex:0]; 44 | [results addObject:oneObject]; 45 | } 46 | 47 | return [results copy]; 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMResultSet.h: -------------------------------------------------------------------------------- 1 | #import 2 | //#import "sqlite3.h" 3 | 4 | #ifndef __has_feature // Optional. 5 | #define __has_feature(x) 0 // Compatibility with non-clang compilers. 6 | #endif 7 | 8 | #ifndef NS_RETURNS_NOT_RETAINED 9 | #if __has_feature(attribute_ns_returns_not_retained) 10 | #define NS_RETURNS_NOT_RETAINED __attribute__((ns_returns_not_retained)) 11 | #else 12 | #define NS_RETURNS_NOT_RETAINED 13 | #endif 14 | #endif 15 | 16 | @class FMDatabase; 17 | @class FMStatement; 18 | 19 | /** Represents the results of executing a query on an ``. 20 | 21 | ### See also 22 | 23 | - `` 24 | */ 25 | 26 | @interface FMResultSet : NSObject { 27 | FMDatabase *_parentDB; 28 | FMStatement *_statement; 29 | 30 | NSString *_query; 31 | NSMutableDictionary *_columnNameToIndexMap; 32 | } 33 | 34 | ///----------------- 35 | /// @name Properties 36 | ///----------------- 37 | 38 | /** Executed query */ 39 | 40 | @property (atomic, retain) NSString *query; 41 | 42 | /** `NSMutableDictionary` mapping column names to numeric index */ 43 | 44 | @property (readonly) NSMutableDictionary *columnNameToIndexMap; 45 | 46 | /** `FMStatement` used by result set. */ 47 | 48 | @property (atomic, retain) FMStatement *statement; 49 | 50 | ///------------------------------------ 51 | /// @name Creating and closing database 52 | ///------------------------------------ 53 | 54 | /** Create result set from `` 55 | 56 | @param statement A `` to be performed 57 | 58 | @param aDB A `` to be used 59 | 60 | @return A `FMResultSet` on success; `nil` on failure 61 | */ 62 | 63 | + (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB; 64 | 65 | /** Close result set */ 66 | 67 | - (void)close; 68 | 69 | - (void)setParentDB:(FMDatabase *)newDb; 70 | 71 | ///--------------------------------------- 72 | /// @name Iterating through the result set 73 | ///--------------------------------------- 74 | 75 | /** Retrieve next row for result set. 76 | 77 | You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one. 78 | 79 | @return `YES` if row successfully retrieved; `NO` if end of result set reached 80 | 81 | @see hasAnotherRow 82 | */ 83 | 84 | - (BOOL)next; 85 | 86 | /** Retrieve next row for result set. 87 | 88 | You must always invoke `next` or `nextWithError` before attempting to access the values returned in a query, even if you're only expecting one. 89 | 90 | @param outErr A 'NSError' object to receive any error object (if any). 91 | 92 | @return 'YES' if row successfully retrieved; 'NO' if end of result set reached 93 | 94 | @see hasAnotherRow 95 | */ 96 | 97 | - (BOOL)nextWithError:(NSError **)outErr; 98 | 99 | /** Did the last call to `` succeed in retrieving another row? 100 | 101 | @return `YES` if the last call to `` succeeded in retrieving another record; `NO` if not. 102 | 103 | @see next 104 | 105 | @warning The `hasAnotherRow` method must follow a call to ``. 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. 106 | */ 107 | 108 | - (BOOL)hasAnotherRow; 109 | 110 | ///--------------------------------------------- 111 | /// @name Retrieving information from result set 112 | ///--------------------------------------------- 113 | 114 | /** How many columns in result set 115 | 116 | @return Integer value of the number of columns. 117 | */ 118 | 119 | - (int)columnCount; 120 | 121 | /** Column index for column name 122 | 123 | @param columnName `NSString` value of the name of the column. 124 | 125 | @return Zero-based index for column. 126 | */ 127 | 128 | - (int)columnIndexForName:(NSString*)columnName; 129 | 130 | /** Column name for column index 131 | 132 | @param columnIdx Zero-based index for column. 133 | 134 | @return columnName `NSString` value of the name of the column. 135 | */ 136 | 137 | - (NSString*)columnNameForIndex:(int)columnIdx; 138 | 139 | /** Result set integer value for column. 140 | 141 | @param columnName `NSString` value of the name of the column. 142 | 143 | @return `int` value of the result set's column. 144 | */ 145 | 146 | - (int)intForColumn:(NSString*)columnName; 147 | 148 | /** Result set integer value for column. 149 | 150 | @param columnIdx Zero-based index for column. 151 | 152 | @return `int` value of the result set's column. 153 | */ 154 | 155 | - (int)intForColumnIndex:(int)columnIdx; 156 | 157 | /** Result set `long` value for column. 158 | 159 | @param columnName `NSString` value of the name of the column. 160 | 161 | @return `long` value of the result set's column. 162 | */ 163 | 164 | - (long)longForColumn:(NSString*)columnName; 165 | 166 | /** Result set long value for column. 167 | 168 | @param columnIdx Zero-based index for column. 169 | 170 | @return `long` value of the result set's column. 171 | */ 172 | 173 | - (long)longForColumnIndex:(int)columnIdx; 174 | 175 | /** Result set `long long int` value for column. 176 | 177 | @param columnName `NSString` value of the name of the column. 178 | 179 | @return `long long int` value of the result set's column. 180 | */ 181 | 182 | - (long long int)longLongIntForColumn:(NSString*)columnName; 183 | 184 | /** Result set `long long int` value for column. 185 | 186 | @param columnIdx Zero-based index for column. 187 | 188 | @return `long long int` value of the result set's column. 189 | */ 190 | 191 | - (long long int)longLongIntForColumnIndex:(int)columnIdx; 192 | 193 | /** Result set `unsigned long long int` value for column. 194 | 195 | @param columnName `NSString` value of the name of the column. 196 | 197 | @return `unsigned long long int` value of the result set's column. 198 | */ 199 | 200 | - (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName; 201 | 202 | /** Result set `unsigned long long int` value for column. 203 | 204 | @param columnIdx Zero-based index for column. 205 | 206 | @return `unsigned long long int` value of the result set's column. 207 | */ 208 | 209 | - (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx; 210 | 211 | /** Result set `BOOL` value for column. 212 | 213 | @param columnName `NSString` value of the name of the column. 214 | 215 | @return `BOOL` value of the result set's column. 216 | */ 217 | 218 | - (BOOL)boolForColumn:(NSString*)columnName; 219 | 220 | /** Result set `BOOL` value for column. 221 | 222 | @param columnIdx Zero-based index for column. 223 | 224 | @return `BOOL` value of the result set's column. 225 | */ 226 | 227 | - (BOOL)boolForColumnIndex:(int)columnIdx; 228 | 229 | /** Result set `double` value for column. 230 | 231 | @param columnName `NSString` value of the name of the column. 232 | 233 | @return `double` value of the result set's column. 234 | 235 | */ 236 | 237 | - (double)doubleForColumn:(NSString*)columnName; 238 | 239 | /** Result set `double` value for column. 240 | 241 | @param columnIdx Zero-based index for column. 242 | 243 | @return `double` value of the result set's column. 244 | 245 | */ 246 | 247 | - (double)doubleForColumnIndex:(int)columnIdx; 248 | 249 | /** Result set `NSString` value for column. 250 | 251 | @param columnName `NSString` value of the name of the column. 252 | 253 | @return `NSString` value of the result set's column. 254 | 255 | */ 256 | 257 | - (NSString*)stringForColumn:(NSString*)columnName; 258 | 259 | /** Result set `NSString` value for column. 260 | 261 | @param columnIdx Zero-based index for column. 262 | 263 | @return `NSString` value of the result set's column. 264 | */ 265 | 266 | - (NSString*)stringForColumnIndex:(int)columnIdx; 267 | 268 | /** Result set `NSDate` value for column. 269 | 270 | @param columnName `NSString` value of the name of the column. 271 | 272 | @return `NSDate` value of the result set's column. 273 | */ 274 | 275 | - (NSDate*)dateForColumn:(NSString*)columnName; 276 | 277 | /** Result set `NSDate` value for column. 278 | 279 | @param columnIdx Zero-based index for column. 280 | 281 | @return `NSDate` value of the result set's column. 282 | 283 | */ 284 | 285 | - (NSDate*)dateForColumnIndex:(int)columnIdx; 286 | 287 | /** Result set `NSData` value for column. 288 | 289 | This is useful when storing binary data in table (such as image or the like). 290 | 291 | @param columnName `NSString` value of the name of the column. 292 | 293 | @return `NSData` value of the result set's column. 294 | 295 | */ 296 | 297 | - (NSData*)dataForColumn:(NSString*)columnName; 298 | 299 | /** Result set `NSData` value for column. 300 | 301 | @param columnIdx Zero-based index for column. 302 | 303 | @return `NSData` value of the result set's column. 304 | */ 305 | 306 | - (NSData*)dataForColumnIndex:(int)columnIdx; 307 | 308 | /** Result set `(const unsigned char *)` value for column. 309 | 310 | @param columnName `NSString` value of the name of the column. 311 | 312 | @return `(const unsigned char *)` value of the result set's column. 313 | */ 314 | 315 | - (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName; 316 | 317 | /** Result set `(const unsigned char *)` value for column. 318 | 319 | @param columnIdx Zero-based index for column. 320 | 321 | @return `(const unsigned char *)` value of the result set's column. 322 | */ 323 | 324 | - (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx; 325 | 326 | /** Result set object for column. 327 | 328 | @param columnName `NSString` value of the name of the column. 329 | 330 | @return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object. 331 | 332 | @see objectForKeyedSubscript: 333 | */ 334 | 335 | - (id)objectForColumnName:(NSString*)columnName; 336 | 337 | /** Result set object for column. 338 | 339 | @param columnIdx Zero-based index for column. 340 | 341 | @return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object. 342 | 343 | @see objectAtIndexedSubscript: 344 | */ 345 | 346 | - (id)objectForColumnIndex:(int)columnIdx; 347 | 348 | /** Result set object for column. 349 | 350 | 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: 351 | 352 | id result = rs[@"employee_name"]; 353 | 354 | This simplified syntax is equivalent to calling: 355 | 356 | id result = [rs objectForKeyedSubscript:@"employee_name"]; 357 | 358 | which is, it turns out, equivalent to calling: 359 | 360 | id result = [rs objectForColumnName:@"employee_name"]; 361 | 362 | @param columnName `NSString` value of the name of the column. 363 | 364 | @return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object. 365 | */ 366 | 367 | - (id)objectForKeyedSubscript:(NSString *)columnName; 368 | 369 | /** Result set object for column. 370 | 371 | 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: 372 | 373 | id result = rs[0]; 374 | 375 | This simplified syntax is equivalent to calling: 376 | 377 | id result = [rs objectForKeyedSubscript:0]; 378 | 379 | which is, it turns out, equivalent to calling: 380 | 381 | id result = [rs objectForColumnName:0]; 382 | 383 | @param columnIdx Zero-based index for column. 384 | 385 | @return Either `NSNumber`, `NSString`, `NSData`, or `NSNull`. If the column was `NULL`, this returns `[NSNull null]` object. 386 | */ 387 | 388 | - (id)objectAtIndexedSubscript:(int)columnIdx; 389 | 390 | /** Result set `NSData` value for column. 391 | 392 | @param columnName `NSString` value of the name of the column. 393 | 394 | @return `NSData` value of the result set's column. 395 | 396 | @warning If you are going to use this data after you iterate over the next row, or after you close the 397 | result set, make sure to make a copy of the data first (or just use ``/``) 398 | If you don't, you're going to be in a world of hurt when you try and use the data. 399 | 400 | */ 401 | 402 | - (NSData*)dataNoCopyForColumn:(NSString*)columnName NS_RETURNS_NOT_RETAINED; 403 | 404 | /** Result set `NSData` value for column. 405 | 406 | @param columnIdx Zero-based index for column. 407 | 408 | @return `NSData` value of the result set's column. 409 | 410 | @warning If you are going to use this data after you iterate over the next row, or after you close the 411 | result set, make sure to make a copy of the data first (or just use ``/``) 412 | If you don't, you're going to be in a world of hurt when you try and use the data. 413 | 414 | */ 415 | 416 | - (NSData*)dataNoCopyForColumnIndex:(int)columnIdx NS_RETURNS_NOT_RETAINED; 417 | 418 | /** Is the column `NULL`? 419 | 420 | @param columnIdx Zero-based index for column. 421 | 422 | @return `YES` if column is `NULL`; `NO` if not `NULL`. 423 | */ 424 | 425 | - (BOOL)columnIndexIsNull:(int)columnIdx; 426 | 427 | /** Is the column `NULL`? 428 | 429 | @param columnName `NSString` value of the name of the column. 430 | 431 | @return `YES` if column is `NULL`; `NO` if not `NULL`. 432 | */ 433 | 434 | - (BOOL)columnIsNull:(NSString*)columnName; 435 | 436 | 437 | /** Returns a dictionary of the row results mapped to case sensitive keys of the column names. 438 | 439 | @returns `NSDictionary` of the row results. 440 | 441 | @warning The keys to the dictionary are case sensitive of the column names. 442 | */ 443 | 444 | - (NSDictionary*)resultDictionary; 445 | 446 | /** Returns a dictionary of the row results 447 | 448 | @see resultDictionary 449 | 450 | @warning **Deprecated**: Please use `` instead. Also, beware that `` is case sensitive! 451 | */ 452 | 453 | - (NSDictionary*)resultDict __attribute__ ((deprecated)); 454 | 455 | ///----------------------------- 456 | /// @name Key value coding magic 457 | ///----------------------------- 458 | 459 | /** Performs `setValue` to yield support for key value observing. 460 | 461 | @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. 462 | 463 | */ 464 | 465 | - (void)kvcMagic:(id)object; 466 | 467 | 468 | @end 469 | 470 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/FMResultSet.m: -------------------------------------------------------------------------------- 1 | #import "FMResultSet.h" 2 | #import "FMDatabase.h" 3 | #import "unistd.h" 4 | #import "sqlite3.h" 5 | 6 | @interface FMDatabase () 7 | - (void)resultSetDidClose:(FMResultSet *)resultSet; 8 | @end 9 | 10 | @interface FMResultSet () 11 | @property (nonatomic, readonly) NSDictionary *columnNameToIndexMapNonLowercased; 12 | @end 13 | 14 | @implementation FMResultSet 15 | @synthesize query=_query; 16 | @synthesize statement=_statement; 17 | @synthesize columnNameToIndexMapNonLowercased = _columnNameToIndexMapNonLowercased; 18 | 19 | + (instancetype)resultSetWithStatement:(FMStatement *)statement usingParentDatabase:(FMDatabase*)aDB { 20 | 21 | FMResultSet *rs = [[FMResultSet alloc] init]; 22 | 23 | [rs setStatement:statement]; 24 | [rs setParentDB:aDB]; 25 | 26 | NSParameterAssert(![statement inUse]); 27 | [statement setInUse:YES]; // weak reference 28 | 29 | return FMDBReturnAutoreleased(rs); 30 | } 31 | 32 | - (void)dealloc { 33 | [self close]; 34 | 35 | FMDBRelease(_query); 36 | _query = nil; 37 | 38 | FMDBRelease(_columnNameToIndexMap); 39 | _columnNameToIndexMap = nil; 40 | 41 | #if ! __has_feature(objc_arc) 42 | [super dealloc]; 43 | #endif 44 | } 45 | 46 | - (void)close { 47 | [_statement reset]; 48 | FMDBRelease(_statement); 49 | _statement = nil; 50 | 51 | // we don't need this anymore... (i think) 52 | //[_parentDB setInUse:NO]; 53 | [_parentDB resultSetDidClose:self]; 54 | _parentDB = nil; 55 | } 56 | 57 | - (int)columnCount { 58 | return sqlite3_column_count([_statement statement]); 59 | } 60 | 61 | - (NSMutableDictionary *)columnNameToIndexMap { 62 | if (!_columnNameToIndexMap) { 63 | NSDictionary *nonLowercasedMap = self.columnNameToIndexMapNonLowercased; 64 | NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:nonLowercasedMap.count]; 65 | for (NSString *key in nonLowercasedMap.allKeys) { 66 | [d setObject:nonLowercasedMap[key] forKey:[self _lowercaseString:key]]; 67 | } 68 | _columnNameToIndexMap = d; 69 | } 70 | return _columnNameToIndexMap; 71 | } 72 | 73 | - (NSDictionary *)columnNameToIndexMapNonLowercased { 74 | if (!_columnNameToIndexMapNonLowercased) { 75 | int columnCount = sqlite3_column_count([_statement statement]); 76 | NSMutableDictionary *d = [[NSMutableDictionary alloc] initWithCapacity:(NSUInteger)columnCount]; 77 | int columnIdx = 0; 78 | for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { 79 | [d setObject:[NSNumber numberWithInt:columnIdx] 80 | forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]]; 81 | } 82 | _columnNameToIndexMapNonLowercased = d; 83 | } 84 | return _columnNameToIndexMapNonLowercased; 85 | } 86 | 87 | - (void)kvcMagic:(id)object { 88 | 89 | int columnCount = sqlite3_column_count([_statement statement]); 90 | 91 | int columnIdx = 0; 92 | for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { 93 | 94 | const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx); 95 | 96 | // check for a null row 97 | if (c) { 98 | NSString *s = [NSString stringWithUTF8String:c]; 99 | 100 | [object setValue:s forKey:[NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]]; 101 | } 102 | } 103 | } 104 | 105 | #pragma clang diagnostic push 106 | #pragma clang diagnostic ignored "-Wdeprecated-implementations" 107 | 108 | - (NSDictionary*)resultDict { 109 | 110 | NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]); 111 | 112 | if (num_cols > 0) { 113 | NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols]; 114 | 115 | NSEnumerator *columnNames = [[self columnNameToIndexMap] keyEnumerator]; 116 | NSString *columnName = nil; 117 | while ((columnName = [columnNames nextObject])) { 118 | id objectValue = [self objectForColumnName:columnName]; 119 | [dict setObject:objectValue forKey:columnName]; 120 | } 121 | 122 | return FMDBReturnAutoreleased([dict copy]); 123 | } 124 | else { 125 | NSLog(@"Warning: There seem to be no columns in this set."); 126 | } 127 | 128 | return nil; 129 | } 130 | 131 | #pragma clang diagnostic pop 132 | 133 | - (NSDictionary*)resultDictionary { 134 | 135 | NSUInteger num_cols = (NSUInteger)sqlite3_data_count([_statement statement]); 136 | 137 | if (num_cols > 0) { 138 | NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols]; 139 | 140 | int columnCount = sqlite3_column_count([_statement statement]); 141 | 142 | int columnIdx = 0; 143 | for (columnIdx = 0; columnIdx < columnCount; columnIdx++) { 144 | 145 | NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name([_statement statement], columnIdx)]; 146 | id objectValue = [self objectForColumnIndex:columnIdx]; 147 | [dict setObject:objectValue forKey:columnName]; 148 | } 149 | 150 | return dict; 151 | } 152 | else { 153 | NSLog(@"Warning: There seem to be no columns in this set."); 154 | } 155 | 156 | return nil; 157 | } 158 | 159 | 160 | 161 | 162 | - (BOOL)next { 163 | return [self nextWithError:nil]; 164 | } 165 | 166 | - (BOOL)nextWithError:(NSError **)outErr { 167 | 168 | int rc = sqlite3_step([_statement statement]); 169 | 170 | if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) { 171 | NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [_parentDB databasePath]); 172 | NSLog(@"Database busy"); 173 | if (outErr) { 174 | *outErr = [_parentDB lastError]; 175 | } 176 | } 177 | else if (SQLITE_DONE == rc || SQLITE_ROW == rc) { 178 | // all is well, let's return. 179 | } 180 | else if (SQLITE_ERROR == rc) { 181 | NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); 182 | if (outErr) { 183 | *outErr = [_parentDB lastError]; 184 | } 185 | } 186 | else if (SQLITE_MISUSE == rc) { 187 | // uh oh. 188 | NSLog(@"Error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); 189 | if (outErr) { 190 | if (_parentDB) { 191 | *outErr = [_parentDB lastError]; 192 | } 193 | else { 194 | // If 'next' or 'nextWithError' is called after the result set is closed, 195 | // we need to return the appropriate error. 196 | NSDictionary* errorMessage = [NSDictionary dictionaryWithObject:@"parentDB does not exist" forKey:NSLocalizedDescriptionKey]; 197 | *outErr = [NSError errorWithDomain:@"FMDatabase" code:SQLITE_MISUSE userInfo:errorMessage]; 198 | } 199 | 200 | } 201 | } 202 | else { 203 | // wtf? 204 | NSLog(@"Unknown error calling sqlite3_step (%d: %s) rs", rc, sqlite3_errmsg([_parentDB sqliteHandle])); 205 | if (outErr) { 206 | *outErr = [_parentDB lastError]; 207 | } 208 | } 209 | 210 | 211 | if (rc != SQLITE_ROW) { 212 | [self close]; 213 | } 214 | 215 | return (rc == SQLITE_ROW); 216 | } 217 | 218 | - (BOOL)hasAnotherRow { 219 | return sqlite3_errcode([_parentDB sqliteHandle]) == SQLITE_ROW; 220 | } 221 | 222 | - (int)columnIndexForName:(NSString*)columnName { 223 | NSNumber *n = self.columnNameToIndexMapNonLowercased[columnName]; 224 | if (!n) { 225 | columnName = [self _lowercaseString:columnName]; 226 | n = [[self columnNameToIndexMap] objectForKey:columnName]; 227 | } 228 | 229 | if (n) { 230 | return [n intValue]; 231 | } 232 | 233 | NSLog(@"Warning: I could not find the column named '%@'.", columnName); 234 | 235 | return -1; 236 | } 237 | 238 | 239 | 240 | - (int)intForColumn:(NSString*)columnName { 241 | return [self intForColumnIndex:[self columnIndexForName:columnName]]; 242 | } 243 | 244 | - (int)intForColumnIndex:(int)columnIdx { 245 | return sqlite3_column_int([_statement statement], columnIdx); 246 | } 247 | 248 | - (long)longForColumn:(NSString*)columnName { 249 | return [self longForColumnIndex:[self columnIndexForName:columnName]]; 250 | } 251 | 252 | - (long)longForColumnIndex:(int)columnIdx { 253 | return (long)sqlite3_column_int64([_statement statement], columnIdx); 254 | } 255 | 256 | - (long long int)longLongIntForColumn:(NSString*)columnName { 257 | return [self longLongIntForColumnIndex:[self columnIndexForName:columnName]]; 258 | } 259 | 260 | - (long long int)longLongIntForColumnIndex:(int)columnIdx { 261 | return sqlite3_column_int64([_statement statement], columnIdx); 262 | } 263 | 264 | - (unsigned long long int)unsignedLongLongIntForColumn:(NSString*)columnName { 265 | return [self unsignedLongLongIntForColumnIndex:[self columnIndexForName:columnName]]; 266 | } 267 | 268 | - (unsigned long long int)unsignedLongLongIntForColumnIndex:(int)columnIdx { 269 | return (unsigned long long int)[self longLongIntForColumnIndex:columnIdx]; 270 | } 271 | 272 | - (BOOL)boolForColumn:(NSString*)columnName { 273 | return [self boolForColumnIndex:[self columnIndexForName:columnName]]; 274 | } 275 | 276 | - (BOOL)boolForColumnIndex:(int)columnIdx { 277 | return ([self intForColumnIndex:columnIdx] != 0); 278 | } 279 | 280 | - (double)doubleForColumn:(NSString*)columnName { 281 | return [self doubleForColumnIndex:[self columnIndexForName:columnName]]; 282 | } 283 | 284 | - (double)doubleForColumnIndex:(int)columnIdx { 285 | return sqlite3_column_double([_statement statement], columnIdx); 286 | } 287 | 288 | - (NSString*)stringForColumnIndex:(int)columnIdx { 289 | 290 | if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { 291 | return nil; 292 | } 293 | 294 | const char *c = (const char *)sqlite3_column_text([_statement statement], columnIdx); 295 | 296 | if (!c) { 297 | // null row. 298 | return nil; 299 | } 300 | 301 | return [NSString stringWithUTF8String:c]; 302 | } 303 | 304 | - (NSString*)stringForColumn:(NSString*)columnName { 305 | return [self stringForColumnIndex:[self columnIndexForName:columnName]]; 306 | } 307 | 308 | - (NSDate*)dateForColumn:(NSString*)columnName { 309 | return [self dateForColumnIndex:[self columnIndexForName:columnName]]; 310 | } 311 | 312 | - (NSDate*)dateForColumnIndex:(int)columnIdx { 313 | 314 | if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { 315 | return nil; 316 | } 317 | 318 | return [_parentDB hasDateFormatter] ? [_parentDB dateFromString:[self stringForColumnIndex:columnIdx]] : [NSDate dateWithTimeIntervalSince1970:[self doubleForColumnIndex:columnIdx]]; 319 | } 320 | 321 | 322 | - (NSData*)dataForColumn:(NSString*)columnName { 323 | return [self dataForColumnIndex:[self columnIndexForName:columnName]]; 324 | } 325 | 326 | - (NSData*)dataForColumnIndex:(int)columnIdx { 327 | 328 | if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { 329 | return nil; 330 | } 331 | 332 | const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx); 333 | int dataSize = sqlite3_column_bytes([_statement statement], columnIdx); 334 | 335 | if (dataBuffer == NULL) { 336 | return nil; 337 | } 338 | 339 | return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize]; 340 | } 341 | 342 | 343 | - (NSData*)dataNoCopyForColumn:(NSString*)columnName { 344 | return [self dataNoCopyForColumnIndex:[self columnIndexForName:columnName]]; 345 | } 346 | 347 | - (NSData*)dataNoCopyForColumnIndex:(int)columnIdx { 348 | 349 | if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { 350 | return nil; 351 | } 352 | 353 | const char *dataBuffer = sqlite3_column_blob([_statement statement], columnIdx); 354 | int dataSize = sqlite3_column_bytes([_statement statement], columnIdx); 355 | 356 | NSData *data = [NSData dataWithBytesNoCopy:(void *)dataBuffer length:(NSUInteger)dataSize freeWhenDone:NO]; 357 | 358 | return data; 359 | } 360 | 361 | 362 | - (BOOL)columnIndexIsNull:(int)columnIdx { 363 | return sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL; 364 | } 365 | 366 | - (BOOL)columnIsNull:(NSString*)columnName { 367 | return [self columnIndexIsNull:[self columnIndexForName:columnName]]; 368 | } 369 | 370 | - (const unsigned char *)UTF8StringForColumnIndex:(int)columnIdx { 371 | 372 | if (sqlite3_column_type([_statement statement], columnIdx) == SQLITE_NULL || (columnIdx < 0)) { 373 | return nil; 374 | } 375 | 376 | return sqlite3_column_text([_statement statement], columnIdx); 377 | } 378 | 379 | - (const unsigned char *)UTF8StringForColumnName:(NSString*)columnName { 380 | return [self UTF8StringForColumnIndex:[self columnIndexForName:columnName]]; 381 | } 382 | 383 | - (id)objectForColumnIndex:(int)columnIdx { 384 | int columnType = sqlite3_column_type([_statement statement], columnIdx); 385 | 386 | id returnValue = nil; 387 | 388 | if (columnType == SQLITE_INTEGER) { 389 | returnValue = [NSNumber numberWithLongLong:[self longLongIntForColumnIndex:columnIdx]]; 390 | } 391 | else if (columnType == SQLITE_FLOAT) { 392 | returnValue = [NSNumber numberWithDouble:[self doubleForColumnIndex:columnIdx]]; 393 | } 394 | else if (columnType == SQLITE_BLOB) { 395 | returnValue = [self dataForColumnIndex:columnIdx]; 396 | } 397 | else { 398 | //default to a string for everything else 399 | returnValue = [self stringForColumnIndex:columnIdx]; 400 | } 401 | 402 | if (returnValue == nil) { 403 | returnValue = [NSNull null]; 404 | } 405 | 406 | return returnValue; 407 | } 408 | 409 | - (id)objectForColumnName:(NSString*)columnName { 410 | return [self objectForColumnIndex:[self columnIndexForName:columnName]]; 411 | } 412 | 413 | // returns autoreleased NSString containing the name of the column in the result set 414 | - (NSString*)columnNameForIndex:(int)columnIdx { 415 | return [NSString stringWithUTF8String: sqlite3_column_name([_statement statement], columnIdx)]; 416 | } 417 | 418 | - (void)setParentDB:(FMDatabase *)newDb { 419 | _parentDB = newDb; 420 | } 421 | 422 | - (id)objectAtIndexedSubscript:(int)columnIdx { 423 | return [self objectForColumnIndex:columnIdx]; 424 | } 425 | 426 | - (id)objectForKeyedSubscript:(NSString *)columnName { 427 | return [self objectForColumnName:columnName]; 428 | } 429 | 430 | // Brent 22 Feb. 2019: Calls to lowerCaseString show up in Instruments too much. 431 | // Given that the amount of column names in a given app is going to be pretty small, 432 | // we can just cache the lowercase versions 433 | - (NSString *)_lowercaseString:(NSString *)s { 434 | static NSLock *lock = nil; 435 | static NSMutableDictionary *lowercaseStringCache = nil; 436 | 437 | static dispatch_once_t onceToken; 438 | dispatch_once(&onceToken, ^{ 439 | lock = [[NSLock alloc] init]; 440 | lowercaseStringCache = [[NSMutableDictionary alloc] init]; 441 | }); 442 | 443 | [lock lock]; 444 | NSString *lowercaseString = lowercaseStringCache[s]; 445 | if (lowercaseString == nil) { 446 | lowercaseString = s.lowercaseString; 447 | lowercaseStringCache[s] = lowercaseString; 448 | } 449 | [lock unlock]; 450 | 451 | return lowercaseString; 452 | } 453 | 454 | @end 455 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/NSString+RSDatabase.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RSDatabase.h 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 3/27/15. 6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | @import Foundation; 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface NSString (QSDatabase) 14 | 15 | 16 | /*Returns @"(?, ?, ?)" -- where number of ? spots is specified by numberOfValues. 17 | numberOfValues should be greater than 0. Triggers an NSParameterAssert if not.*/ 18 | 19 | + (nullable NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues; 20 | 21 | 22 | /*Returns @"(someColumn, anotherColumm, thirdColumn)" -- using passed-in keys. 23 | It's essential that you trust keys. They must not be user input. 24 | Triggers an NSParameterAssert if keys are empty.*/ 25 | 26 | + (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys; 27 | 28 | 29 | /*Returns @"key1=?, key2=?" using passed-in keys. Keys must be trusted.*/ 30 | 31 | + (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys; 32 | 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/NSString+RSDatabase.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+RSDatabase.m 3 | // RSDatabase 4 | // 5 | // Created by Brent Simmons on 3/27/15. 6 | // Copyright (c) 2015 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | #import "NSString+RSDatabase.h" 10 | 11 | 12 | @implementation NSString (RSDatabase) 13 | 14 | 15 | + (NSString *)rs_SQLValueListWithPlaceholders:(NSUInteger)numberOfValues { 16 | 17 | // @"(?, ?, ?)" 18 | 19 | NSParameterAssert(numberOfValues > 0); 20 | if (numberOfValues < 1) { 21 | return nil; 22 | } 23 | 24 | static NSMutableDictionary *cache = nil; 25 | static dispatch_once_t onceToken; 26 | static NSLock *lock = nil; 27 | dispatch_once(&onceToken, ^{ 28 | lock = [[NSLock alloc] init]; 29 | cache = [NSMutableDictionary new]; 30 | }); 31 | 32 | [lock lock]; 33 | NSNumber *cacheKey = @(numberOfValues); 34 | NSString *cachedString = cache[cacheKey]; 35 | if (cachedString) { 36 | [lock unlock]; 37 | return cachedString; 38 | } 39 | 40 | NSMutableString *s = [[NSMutableString alloc] initWithString:@"("]; 41 | NSUInteger i = 0; 42 | 43 | for (i = 0; i < numberOfValues; i++) { 44 | 45 | [s appendString:@"?"]; 46 | BOOL isLast = (i == (numberOfValues - 1)); 47 | if (!isLast) { 48 | [s appendString:@", "]; 49 | } 50 | } 51 | 52 | [s appendString:@")"]; 53 | 54 | cache[cacheKey] = s; 55 | [lock unlock]; 56 | 57 | return s; 58 | } 59 | 60 | 61 | + (NSString *)rs_SQLKeysListWithArray:(NSArray *)keys { 62 | 63 | NSParameterAssert(keys.count > 0); 64 | 65 | static NSMutableDictionary *cache = nil; 66 | static dispatch_once_t onceToken; 67 | static NSLock *lock = nil; 68 | dispatch_once(&onceToken, ^{ 69 | lock = [[NSLock alloc] init]; 70 | cache = [NSMutableDictionary new]; 71 | }); 72 | 73 | [lock lock]; 74 | NSArray *cacheKey = keys; 75 | NSString *cachedString = cache[cacheKey]; 76 | if (cachedString) { 77 | [lock unlock]; 78 | return cachedString; 79 | } 80 | 81 | NSString *s = [NSString stringWithFormat:@"(%@)", [keys componentsJoinedByString:@", "]]; 82 | 83 | cache[cacheKey] = s; 84 | [lock unlock]; 85 | 86 | return s; 87 | } 88 | 89 | 90 | + (NSString *)rs_SQLKeyPlaceholderPairsWithKeys:(NSArray *)keys { 91 | 92 | // key1=?, key2=? 93 | 94 | NSParameterAssert(keys.count > 0); 95 | 96 | static NSMutableDictionary *cache = nil; 97 | static dispatch_once_t onceToken; 98 | static NSLock *lock = nil; 99 | dispatch_once(&onceToken, ^{ 100 | lock = [[NSLock alloc] init]; 101 | cache = [NSMutableDictionary new]; 102 | }); 103 | 104 | [lock lock]; 105 | NSArray *cacheKey = keys; 106 | NSString *cachedString = cache[cacheKey]; 107 | if (cachedString) { 108 | [lock unlock]; 109 | return cachedString; 110 | } 111 | 112 | NSMutableString *s = [NSMutableString stringWithString:@""]; 113 | 114 | NSUInteger i = 0; 115 | NSUInteger numberOfKeys = [keys count]; 116 | 117 | for (i = 0; i < numberOfKeys; i++) { 118 | 119 | NSString *oneKey = keys[i]; 120 | [s appendString:oneKey]; 121 | [s appendString:@"=?"]; 122 | BOOL isLast = (i == (numberOfKeys - 1)); 123 | if (!isLast) { 124 | [s appendString:@", "]; 125 | } 126 | } 127 | 128 | cache[cacheKey] = s; 129 | [lock unlock]; 130 | 131 | return s; 132 | } 133 | 134 | 135 | @end 136 | -------------------------------------------------------------------------------- /Sources/RSDatabaseObjC/include/RSDatabaseObjC.h: -------------------------------------------------------------------------------- 1 | // RSDatabaseObjC 2 | 3 | // FMDB 4 | 5 | #import "../FMDatabase.h" 6 | #import "../FMDatabaseAdditions.h" 7 | #import "../FMResultSet.h" 8 | 9 | // Categories 10 | 11 | #import "../FMDatabase+RSExtras.h" 12 | #import "../FMResultSet+RSExtras.h" 13 | #import "../NSString+RSDatabase.h" 14 | -------------------------------------------------------------------------------- /Tests/RSDatabaseTests/ODBTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ODBTests.swift 3 | // RSDatabaseTests 4 | // 5 | // Created by Brent Simmons on 8/27/18. 6 | // Copyright © 2018 Ranchero Software, LLC. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | import RSDatabase 11 | 12 | class ODBTests: XCTestCase { 13 | 14 | func testODBCreation() { 15 | let odb = genericTestODB() 16 | closeAndDelete(odb) 17 | } 18 | 19 | func testSimpleBoolStorage() { 20 | let odb = genericTestODB() 21 | let path = ODBPath.path(["testBool"]) 22 | path.setRawValue(true, odb: odb) 23 | 24 | XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) 25 | closeAndDelete(odb) 26 | } 27 | 28 | func testSimpleIntStorage() { 29 | let odb = genericTestODB() 30 | let path = ODBPath.path(["TestInt"]) 31 | let intValue = 3487456 32 | path.setRawValue(intValue, odb: odb) 33 | 34 | XCTAssertEqual(path.rawValue(with: odb) as! Int, intValue) 35 | closeAndDelete(odb) 36 | } 37 | 38 | func testSimpleDoubleStorage() { 39 | let odb = genericTestODB() 40 | let path = ODBPath.path(["TestDouble"]) 41 | let doubleValue = 3498.45745 42 | path.setRawValue(doubleValue, odb: odb) 43 | 44 | XCTAssertEqual(path.rawValue(with: odb) as! Double, doubleValue) 45 | closeAndDelete(odb) 46 | } 47 | 48 | func testReadSimpleBoolPerformance() { 49 | let odb = genericTestODB() 50 | let path = ODBPath.path(["TestBool"]) 51 | path.setRawValue(true, odb: odb) 52 | XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) 53 | 54 | self.measure { 55 | let _ = path.rawValue(with: odb) 56 | } 57 | closeAndDelete(odb) 58 | } 59 | 60 | func testSetSimpleUnchangingBoolPerformance() { 61 | let odb = genericTestODB() 62 | let path = ODBPath.path(["TestBool"]) 63 | self.measure { 64 | path.setRawValue(true, odb: odb) 65 | } 66 | closeAndDelete(odb) 67 | } 68 | 69 | func testReadAndCloseAndReadSimpleBool() { 70 | let f = pathForTestFile("testReadAndCloseAndReadSimpleBool.odb") 71 | var odb = ODB(filepath: f) 72 | let path = ODBPath.path(["testBool"]) 73 | path.setRawValue(true, odb: odb) 74 | 75 | XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) 76 | odb.close() 77 | 78 | odb = ODB(filepath: f) 79 | XCTAssertEqual(path.rawValue(with: odb) as! Bool, true) 80 | closeAndDelete(odb) 81 | } 82 | 83 | func testReplaceSimpleObject() { 84 | let odb = genericTestODB() 85 | let path = ODBPath.path(["TestValue"]) 86 | let intValue = 3487456 87 | path.setRawValue(intValue, odb: odb) 88 | 89 | XCTAssertEqual(path.rawValue(with: odb) as! Int, intValue) 90 | 91 | let stringValue = "test string value" 92 | path.setRawValue(stringValue, odb: odb) 93 | XCTAssertEqual(path.rawValue(with: odb) as! String, stringValue) 94 | 95 | closeAndDelete(odb) 96 | } 97 | 98 | func testEnsureTable() { 99 | let odb = genericTestODB() 100 | let path = ODBPath.path(["A", "B", "C", "D"]) 101 | let _ = path.ensureTable(with: odb) 102 | closeAndDelete(odb) 103 | } 104 | 105 | func testEnsureTablePerformance() { 106 | let odb = genericTestODB() 107 | let path = ODBPath.path(["A", "B", "C", "D"]) 108 | 109 | self.measure { 110 | let _ = path.ensureTable(with: odb) 111 | } 112 | 113 | closeAndDelete(odb) 114 | } 115 | 116 | func testStoreDateInSubtable() { 117 | let odb = genericTestODB() 118 | let path = ODBPath.path(["A", "B", "C", "D"]) 119 | path.ensureTable(with: odb) 120 | 121 | let d = Date() 122 | let datePath = path + "TestValue" 123 | datePath.setRawValue(d, odb: odb) 124 | XCTAssertEqual(datePath.rawValue(with: odb) as! Date, d) 125 | closeAndDelete(odb) 126 | } 127 | } 128 | 129 | private extension ODBTests { 130 | 131 | func tempFolderPath() -> String { 132 | return FileManager.default.temporaryDirectory.path 133 | } 134 | 135 | func pathForTestFile(_ name: String) -> String { 136 | let folder = tempFolderPath() 137 | return (folder as NSString).appendingPathComponent(name) 138 | } 139 | 140 | static var databaseFileID = 0; 141 | 142 | func pathForGenericTestFile() -> String { 143 | ODBTests.databaseFileID += 1 144 | return pathForTestFile("Test\(ODBTests.databaseFileID).odb") 145 | } 146 | 147 | func genericTestODB() -> ODB { 148 | let f = pathForGenericTestFile() 149 | return ODB(filepath: f) 150 | } 151 | 152 | func closeAndDelete(_ odb: ODB) { 153 | odb.close() 154 | try! FileManager.default.removeItem(atPath: odb.filepath) 155 | } 156 | } 157 | --------------------------------------------------------------------------------