├── .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 |
--------------------------------------------------------------------------------