├── LICENSE ├── README.md └── SwiftData.swift /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ryan Fowler 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | SwiftData 2 | ========= 3 | 4 | Working with SQLite is a pain in Swift - that's where SwiftData comes in. 5 | 6 | SwiftData is a simple and effective wrapper around the SQLite3 C API written completely in Swift. 7 | 8 | 9 | ##Features 10 | 11 | - Execute SQL statements directly 12 | - Bind objects conveniently to a string of SQL 13 | - Queries return an easy to use array 14 | - Support for transactions and savepoints 15 | - Inline error handling 16 | - Completely thread-safe by default 17 | - Convenience functions for common tasks, including table and index creation/deletion/querying 18 | - Database connection operations (opening/closing) are automatically handled 19 | 20 | 21 | ##Installation 22 | 23 | Currently, it's as easy as adding the file 'SwiftData.swift' as a git submodule, and dragging it into your project. 24 | Ensure that you've added 'libsqlite3.dylib' as a linked framework and that you've added `#import "sqlite3.h"` to your Briding-Header.h file. 25 | 26 | 27 | ##System Requirements 28 | 29 | Xcode Version: 30 | 31 | - Xcode 6 32 | 33 | Can be used in applications with operating systems: 34 | 35 | - iOS 7.0+ 36 | - Mac OS X 10.9+ 37 | 38 | 39 | ##Usage 40 | 41 | This section runs through some sample usage of SwiftData. 42 | 43 | The full API documentation can be found [here](http://ryanfowler.github.io/SwiftData) 44 | 45 | 46 | ###Table Creation 47 | 48 | By default, SwiftData creates and uses a database called 'SwiftData.sqlite' in the 'Documents' folder of the application. 49 | 50 | To create a table in the database, you may use the convenience function: 51 | 52 | ```swift 53 | if let err = SD.createTable("Cities", withColumnNamesAndTypes: ["Name": .StringVal, "Population": .IntVal, "IsWarm": .BoolVal, "FoundedIn": .DateVal]) { 54 | //there was an error during this function, handle it here 55 | } else { 56 | //no error, the table was created successfully 57 | } 58 | ``` 59 | 60 | Similar convenience functions are provided for: 61 | 62 | - deleting a table: 63 | ```swift 64 | let err = SD.deleteTable("TableName") 65 | ``` 66 | - finding all existing tables in the database: 67 | ```swift 68 | let (tables, err) = SD.existingTables() 69 | ``` 70 | 71 | Alternatively, a table could be created using a SQL statement directly, as shown in the 'Execute A Change' section below. 72 | 73 | 74 | ================= 75 | ###Execute A Change 76 | 77 | The `SD.executeChange()` function can be used to execute any non-query SQL statement (e.g. INSERT, UPDATE, DELETE, CREATE, etc.). 78 | 79 | To create a table using this function, you could use the following: 80 | 81 | ```swift 82 | if let err = SD.executeChange("CREATE TABLE Cities (ID INTEGER PRIMARY KEY AUTOINCREMENT, Name TEXT, Population INTEGER, IsWarm BOOLEAN, FoundedIn DATE)") { 83 | //there was an error during this function, handle it here 84 | } else { 85 | //no error, the table was created successfully 86 | } 87 | ``` 88 | 89 | The table created by this function call is the equivalent of the convenience function used in the earlier section. 90 | 91 | Now that we've created our table, "Cities", we can insert a row into it like so: 92 | 93 | ```swift 94 | if let err = SD.executeChange("INSERT INTO Cities (Name, Population, IsWarm, FoundedIn) VALUES ('Toronto', 2615060, 0, '1793-08-27')") { 95 | //there was an error during the insert, handle it here 96 | } else { 97 | //no error, the row was inserted successfully 98 | } 99 | ``` 100 | 101 | #####Binding Values 102 | 103 | Alternatively, we could insert a row with object binding: 104 | 105 | ```swift 106 | //from user input 107 | let name: String = //user input 108 | let population: Int = //user input 109 | let isWarm: Bool = //user input 110 | let foundedIn: NSDate = //user input 111 | 112 | if let err = SD.executeChange("INSERT INTO Cities (Name, Population, IsWarm, FoundedIn) VALUES (?, ?, ?, ?)", withArgs: [name, population, isWarm, foundedIn]) { 113 | //there was an error during the insert, handle it here 114 | } else { 115 | //no error, the row was inserted successfully 116 | } 117 | ``` 118 | 119 | The provided objects will be escaped and will bind to the '?' characters (in order) in the string of SQL. 120 | 121 | Be aware that although this uses similar syntax to prepared statements, it actually uses the public function: 122 | ```swift 123 | let escValue = SD.escapeValue(object) 124 | ``` 125 | to escape objects internally, which you may also use yourself. 126 | This means that the objects will attempt to bind to *ALL* '?'s in the string of SQL, including those in strings and comments. 127 | 128 | The objects are escaped and will bind to a SQL string in the following manner: 129 | 130 | - A String object is escaped and surrounded by single quotes (e.g. 'sample string') 131 | - An Int object is left untouched (e.g. 10) 132 | - A Double object is left untouched (e.g. 10.8) 133 | - A Bool object is converted to 0 for false, or 1 for true (e.g. 1) 134 | - An NSDate object is converted to a string with format 'yyyy-MM-dd HH:mm:ss' and surrounded by single quotes (e.g. '2014-08-26 10:30:28') 135 | - An NSData object is prefaced with an 'X' and converted to a hexadecimal string surrounded by single quotes (e.g. X'1956a76c') 136 | - A UIImage object is saved to disk, and the ID for retrieval is saved as a string surrounded by single quotes (e.g. 'a98af5ca-7700-4abc-97fb-60737a7b6583') 137 | 138 | All other object types will bind to the SQL string as 'NULL', and a warning message will be printed to the console. 139 | 140 | #####Binding Identifiers 141 | 142 | If an identifier (e.g. table or column name) is provided by the user and needs to be escaped, you can use the characters 'i?' to bind the objects like so: 143 | 144 | ```swift 145 | //from user input 146 | let columnName1 = //user input 147 | let columnName2 = //user input 148 | 149 | if let err = SD.executeChange("INSERT INTO Cities (Name, Population, i?, i?) VALUES (?, ?, ?, ?)", withArgs: [columnName1, columnName2, name, population, isWarm, foundedIn]) { 150 | //there was an error during the insert, handle it here 151 | } else { 152 | //no error, the row was inserted successfully 153 | } 154 | ``` 155 | 156 | The objects 'columnName1' and 'columnName2' will bind to the characters 'i?' in the string of SQL as identifiers. Double quotes will be placed around each identifier. 157 | You may escape an identifier string yourself using the function: 158 | ```swift 159 | let escIdentifier = SD.escapeIdentifier(identifier) 160 | ``` 161 | Objects provided to bind as identifiers must be of type String. 162 | 163 | 164 | ==================== 165 | ###Execute A Query 166 | 167 | Now that our table has some data, we can query it: 168 | 169 | ```swift 170 | let (resultSet, err) = SD.executeQuery("SELECT * FROM Cities") 171 | if err != nil { 172 | //there was an error during the query, handle it here 173 | } else { 174 | for row in resultSet { 175 | if let name = row["Name"]?.asString() { 176 | println("The City name is: \(name)") 177 | } 178 | if let population = row["Population"]?.asInt() { 179 | println("The population is: \(population)") 180 | } 181 | if let isWarm = row["IsWarm"]?.asBool() { 182 | if isWarm { 183 | println("The city is warm") 184 | } else { 185 | println("The city is cold") 186 | } 187 | } 188 | if let foundedIn = row["FoundedIn"]?.asDate() { 189 | println("The city was founded in: \(foundedIn)") 190 | } 191 | } 192 | } 193 | ``` 194 | 195 | A query function returns a tuple of: 196 | 197 | - the result set as an Array of SDRow objects 198 | - the error code as an Optional Int 199 | 200 | An SDRow contains a number of corresponding SDColumn objects. 201 | The values for each column can be obtained by using the column name in subscript format, much like a Dictionary. 202 | In order to obtain the column value in the correct data type, you may use the convenience functions: 203 | 204 | - asString() 205 | - asInt() 206 | - asDouble() 207 | - asBool() 208 | - asDate() 209 | - asData() 210 | - asAnyObject() 211 | - asUIImage() 212 | 213 | If one of the above functions is not used, the value will be an SDColumn object. 214 | 215 | For example, if you want the string value for the column "Name": 216 | 217 | ```swift 218 | if let name = row["Name"]?.asString() { 219 | //the value for column "Name" exists as a String 220 | } else 221 | //the value is nil, cannot be cast as a String, or the column requested does not exist 222 | } 223 | ``` 224 | 225 | You may also execute a query using object binding, similar to the row insert example in an earlier section: 226 | 227 | ```swift 228 | let (resultSet, err) = SD.executeQuery("SELECT * FROM Cities WHERE Name = ?", withArgs: ["Toronto"]) 229 | if err != nil { 230 | //there was an error during the query, handle it here 231 | } else { 232 | for row in resultSet { 233 | if let name = row["Name"]?.asString() { 234 | println("The City name is: \(name)") //should be "Toronto" 235 | } 236 | if let population = row["Population"]?.asInt() { 237 | println("The population is: \(population)") 238 | } 239 | if let isWarm = row["IsWarm"]?.asBool() { 240 | if isWarm { 241 | println("The city is warm") 242 | } else { 243 | println("The city is cold") 244 | } 245 | } 246 | if let foundedIn = row["FoundedIn"]?.asDate() { 247 | println("The city was founded in: \(foundedIn)") 248 | } 249 | } 250 | } 251 | ``` 252 | 253 | The same binding rules apply as described in the 'Execute a Change' section. 254 | 255 | 256 | ================= 257 | ###Error Handling 258 | 259 | You have probably noticed that almost all SwiftData functions return an 'error' value. 260 | 261 | This error value is an Optional Int corresponding to the appropriate error message, which can be obtained by calling the function: 262 | 263 | ```swift 264 | let errMsg = SD.errorMessageForCode(err) 265 | ``` 266 | 267 | It is recommended to compare the error value with nil to see if there was an error during the operation, or if the operation was executed successfully. 268 | 269 | By default, error and warning messages are printed to the console when they are encountered. 270 | 271 | 272 | ================= 273 | ###Creating An Index 274 | 275 | To create an index, you may use the provided convenience function: 276 | 277 | ```swift 278 | if let err = SD.createIndex("NameIndex", onColumns: ["Name"], inTable: "Cities", isUnique: true) { 279 | //there was an error creating the index, handle it here 280 | } else { 281 | //the index was created successfully 282 | } 283 | ``` 284 | 285 | Similar convenience functions are provided for: 286 | - removing an index: 287 | ```swift 288 | let err = removeIndex("IndexName") 289 | ``` 290 | - finding all existing indexes: 291 | ```swift 292 | let (indexes, err) = existingIndexes() 293 | ``` 294 | - finding all indexes for a specified table: 295 | ```swift 296 | let (indexes, err) = existingIndexesForTable("TableName") 297 | ``` 298 | 299 | 300 | ================= 301 | ###Custom Connection 302 | 303 | You may create a custom connection to the database and execute a number of functions within a provided closure. 304 | An example of this can be seen below: 305 | 306 | ```swift 307 | let task: ()->Void = { 308 | if let err = SD.executeChange("INSERT INTO Cities VALUES ('Vancouver', 603502, 1, '1886-04-06')") { 309 | println("Error inserting city") 310 | } 311 | if let err = SD.createIndex(name: "NameIndex", onColumns: ["Name"], inTable: "Cities", isUnique: true) { 312 | println("Index creation failed") 313 | } 314 | } 315 | 316 | if let err = SD.executeWithConnection(.ReadWrite, task) { 317 | //there was an error opening or closing the custom connection 318 | } else { 319 | //no error, the closure was executed 320 | } 321 | ``` 322 | 323 | The available custom connection flags are: 324 | 325 | - .ReadOnly (SQLITE_OPEN_READONLY) 326 | - .ReadWrite (SQLITE_OPEN_READWRITE) 327 | - .ReadWriteCreate (SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE) 328 | 329 | All operations that occur within the provided closure are executed on the single custom connection. 330 | 331 | For more information, see the SQLite documentation for [opening a new database connection](http://www.sqlite.org/c3ref/open.html). 332 | 333 | 334 | ================= 335 | ###Transactions and Savepoints 336 | 337 | If we wanted to execute the above closure `task: ()->Void` inside an exclusive transaction, it could be done like so: 338 | 339 | ```swift 340 | if let err = transaction(task) { 341 | //there was an error starting, closing, committing, or rolling back the transaction as per the error code 342 | } else { 343 | //the transaction was executed without errors 344 | } 345 | ``` 346 | 347 | Similarly, a savepoint could be executed like so: 348 | 349 | ```swift 350 | if let err = savepoint(task) { 351 | //there was an error starting, closing, releasing, or rolling back the savepoint as per the error code 352 | } else { 353 | //the savepoint was executed without errors 354 | } 355 | ``` 356 | 357 | It should be noted that transactions *cannot* be embedded into another transaction or savepoint. 358 | Unlike transactions, savepoints may be embedded into other savepoints or transactions. 359 | 360 | For more information, see the SQLite documentation for [transactions](http://sqlite.org/lang_transaction.html) and [savepoints](http://www.sqlite.org/lang_savepoint.html). 361 | 362 | 363 | ================= 364 | ###Using UIImages 365 | 366 | Convenience functions are provided for working with UIImages. 367 | 368 | To easily save a UIImage to disk and insert the corresponding ID into the database: 369 | 370 | ```swift 371 | let image = UIImage(named:"SampleImage") 372 | if let imageID = SD.saveUIImage(image) { 373 | if let err = SD.executeChange("INSERT INTO SampleImageTable (Name, Image) VALUES (?, ?)", withArgs: ["SampleImageName", imageID]) { 374 | //there was an error inserting the new row, handle it here 375 | } 376 | } else { 377 | //there was an error saving the image to disk 378 | } 379 | ``` 380 | 381 | Alternatively, object binding can also be used: 382 | 383 | ```swift 384 | let image = UIImage(named:"SampleImage") 385 | if let err = SD.executeChange("INSERT INTO SampleImageTable (Name, Image) VALUES (?, ?)", withArgs: ["SampleImageName", image]) { 386 | //there was an error inserting the new row, handle it here 387 | } else { 388 | //the image was saved to disk, and the ID was inserted into the database as a String 389 | } 390 | ``` 391 | 392 | In the examples above, a UIImage is saved to disk and the returned ID is inserted into the database as a String. 393 | In order to easily obtain the UIImage from the database, the function '.asUIImage()' called on an SDColumn object may be used: 394 | 395 | ```swift 396 | let (resultSet, err) = SD.executeQuery("SELECT * FROM SampleImageTable") 397 | if err != nil { 398 | //there was an error with the query, handle it here 399 | } else { 400 | for row in resultSet { 401 | if let image = row["Image"]?.asUIImage() { 402 | //'image' contains the UIImage with the ID stored in this column 403 | } else { 404 | //the ID is invalid, or the image could not be initialized from the data at the specified path 405 | } 406 | } 407 | } 408 | ``` 409 | 410 | The '.asUIImage()' function obtains the ID as a String and returns the UIImage associated with this ID (or will return nil if the ID was invalid or a UIImage could not be initialized). 411 | 412 | If you would like to delete the photo, you may call the function: 413 | 414 | ```swift 415 | if SD.deleteUIImageWithID(imageID) { 416 | //image successfully deleted 417 | } else { 418 | //there was an error deleting the image with the specified ID 419 | } 420 | ``` 421 | 422 | This function should be called to delete the image with the specified ID from disk *before* the row containing the image ID is removed. 423 | Removing the row containing the image ID from the database does not delete the image stored on disk. 424 | 425 | 426 | ================= 427 | ###Thread Safety 428 | 429 | All SwiftData operations are placed on a custom serial queue and executed in a FIFO order. 430 | 431 | This means that you can access the SQLite database from multiple threads without the need to worry about causing errors. 432 | 433 | 434 | ##API Documentation 435 | 436 | Full API Documentation can be found [here](http://ryanfowler.github.io/SwiftData) 437 | 438 | 439 | ##Contact 440 | 441 | Please open an issue for any comments, concerns, ideas, or potential pull requests - all welcome :) 442 | 443 | 444 | ##License 445 | 446 | SwiftData is released under the MIT License. 447 | 448 | See the LICENSE file for full details. 449 | -------------------------------------------------------------------------------- /SwiftData.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SwiftData.swift 3 | // 4 | // Copyright (c) 2015 Ryan Fowler 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | 25 | import Foundation 26 | import UIKit 27 | 28 | 29 | // MARK: - SwiftData 30 | 31 | public struct SwiftData { 32 | 33 | 34 | // MARK: - Public SwiftData Functions 35 | 36 | 37 | // MARK: - Execute Statements 38 | 39 | /** 40 | Execute a non-query SQL statement (e.g. INSERT, UPDATE, DELETE, etc.) 41 | 42 | This function will execute the provided SQL and return an Int with the error code, or nil if there was no error. 43 | It is recommended to always verify that the return value is nil to ensure that the operation was successful. 44 | 45 | Possible errors returned by this function are: 46 | 47 | - SQLite errors (0 - 101) 48 | 49 | :param: sqlStr The non-query string of SQL to be executed (INSERT, UPDATE, DELETE, etc.) 50 | 51 | :returns: An Int with the error code, or nil if there was no error 52 | */ 53 | public static func executeChange(sqlStr: String) -> Int? { 54 | 55 | var error: Int? = nil 56 | let task: ()->Void = { 57 | if let err = SQLiteDB.sharedInstance.open() { 58 | error = err 59 | return 60 | } 61 | error = SQLiteDB.sharedInstance.executeChange(sqlStr) 62 | SQLiteDB.sharedInstance.close() 63 | } 64 | putOnThread(task) 65 | return error 66 | 67 | } 68 | 69 | /** 70 | Execute a non-query SQL statement (e.g. INSERT, UPDATE, DELETE, etc.) along with arguments to be bound to the characters "?" (for values) and "i?" (for identifiers e.g. table or column names). 71 | 72 | The objects in the provided array of arguments will be bound, in order, to the "i?" and "?" characters in the SQL string. 73 | The quantity of "i?"s and "?"s in the SQL string must be equal to the quantity of arguments provided. 74 | Objects that are to bind as an identifier ("i?") must be of type String. 75 | Identifiers should be bound and escaped if provided by the user. 76 | If "nil" is provided as an argument, the NULL value will be bound to the appropriate value in the SQL string. 77 | For more information on how the objects will be escaped, refer to the functions "escapeValue()" and "escapeIdentifier()". 78 | Note that the "escapeValue()" and "escapeIdentifier()" include the necessary quotations ' ' or " " to the arguments when being bound to the SQL. 79 | 80 | It is recommended to always verify that the return value is nil to ensure that the operation was successful. 81 | 82 | Possible errors returned by this function are: 83 | 84 | - SQLite errors (0 - 101) 85 | - binding errors (201 - 203) 86 | 87 | :param: sqlStr The non-query string of SQL to be executed (INSERT, UPDATE, DELETE, etc.) 88 | :param: withArgs An array of objects to bind to the "?" and "i?" characters in the sqlStr 89 | 90 | :returns: An Int with the error code, or nil if there was no error 91 | */ 92 | public static func executeChange(sqlStr: String, withArgs: [AnyObject]) -> Int? { 93 | 94 | var error: Int? = nil 95 | let task: ()->Void = { 96 | if let err = SQLiteDB.sharedInstance.open() { 97 | error = err 98 | return 99 | } 100 | error = SQLiteDB.sharedInstance.executeChange(sqlStr, withArgs: withArgs) 101 | SQLiteDB.sharedInstance.close() 102 | } 103 | putOnThread(task) 104 | return error 105 | 106 | } 107 | 108 | /** 109 | Execute multiple SQL statements (non-queries e.g. INSERT, UPDATE, DELETE, etc.) 110 | 111 | This function will execute each SQL statment in the provided array, in order, and return an Int with the error code, or nil if there was no error. 112 | 113 | Possible errors returned by this function are: 114 | 115 | - SQLite errors (0 - 101) 116 | 117 | :param: sqlArr An array of non-query strings of SQL to be executed (INSERT, UPDATE, DELETE, etc.) 118 | 119 | :returns: An Int with the error code, or nil if there was no error 120 | */ 121 | public static func executeMultipleChanges(sqlArr: [String]) -> Int? { 122 | 123 | var error: Int? = nil 124 | let task: ()->Void = { 125 | if let err = SQLiteDB.sharedInstance.open() { 126 | error = err 127 | return 128 | } 129 | for sqlStr in sqlArr { 130 | if let err = SQLiteDB.sharedInstance.executeChange(sqlStr) { 131 | SQLiteDB.sharedInstance.close() 132 | if let index = find(sqlArr, sqlStr) { 133 | println("Error occurred on array item: \(index) -> \"\(sqlStr)\"") 134 | } 135 | error = err 136 | return 137 | } 138 | } 139 | SQLiteDB.sharedInstance.close() 140 | } 141 | putOnThread(task) 142 | return error 143 | 144 | } 145 | 146 | /** 147 | Execute a SQLite query statement (e.g. SELECT) 148 | 149 | This function will execute the provided SQL and return a tuple of: 150 | - an Array of SDRow objects 151 | - an Int with the error code, or nil if there was no error 152 | 153 | The value for each column in an SDRow can be obtained using the column name in the subscript format similar to a Dictionary, along with the function to obtain the value in the appropriate type (.asString(), .asDate(), .asData(), .asInt(), .asDouble(), and .asBool()). 154 | Without the function call to return a specific type, the SDRow will return an object with type AnyObject. 155 | Note: NULL values in the SQLite database will be returned as 'nil'. 156 | 157 | Possible errors returned by this function are: 158 | 159 | - SQLite errors (0 - 101) 160 | 161 | :param: sqlStr The query String of SQL to be executed (e.g. SELECT) 162 | 163 | :returns: A tuple containing an Array of "SDRow"s, and an Int with the error code or nil if there was no error 164 | */ 165 | public static func executeQuery(sqlStr: String) -> (result: [SDRow], error: Int?) { 166 | 167 | var result = [SDRow] () 168 | var error: Int? = nil 169 | let task: ()->Void = { 170 | if let err = SQLiteDB.sharedInstance.open() { 171 | error = err 172 | return 173 | } 174 | (result, error) = SQLiteDB.sharedInstance.executeQuery(sqlStr) 175 | SQLiteDB.sharedInstance.close() 176 | } 177 | putOnThread(task) 178 | return (result, error) 179 | 180 | } 181 | 182 | /** 183 | Execute a SQL query statement (e.g. SELECT) with arguments to be bound to the characters "?" (for values) and "i?" (for identifiers e.g. table or column names). 184 | 185 | See the "executeChange(sqlStr: String, withArgs: [AnyObject?])" function for more information on the arguments provided and binding. 186 | 187 | See the "executeQuery(sqlStr: String)" function for more information on the return value. 188 | 189 | Possible errors returned by this function are: 190 | 191 | - SQLite errors (0 - 101) 192 | - binding errors (201 - 203) 193 | 194 | :param: sqlStr The query String of SQL to be executed (e.g. SELECT) 195 | :param: withArgs An array of objects that will be bound, in order, to the characters "?" (for values) and "i?" (for identifiers, e.g. table or column names) in the sqlStr. 196 | 197 | :returns: A tuple containing an Array of "SDRow"s, and an Int with the error code or nil if there was no error 198 | */ 199 | public static func executeQuery(sqlStr: String, withArgs: [AnyObject]) -> (result: [SDRow], error: Int?) { 200 | 201 | var result = [SDRow] () 202 | var error: Int? = nil 203 | let task: ()->Void = { 204 | if let err = SQLiteDB.sharedInstance.open() { 205 | error = err 206 | return 207 | } 208 | (result, error) = SQLiteDB.sharedInstance.executeQuery(sqlStr, withArgs: withArgs) 209 | SQLiteDB.sharedInstance.close() 210 | } 211 | putOnThread(task) 212 | return (result, error) 213 | 214 | } 215 | 216 | /** 217 | Execute functions in a closure on a single custom connection 218 | 219 | 220 | Note: This function cannot be nested within itself, or inside a transaction/savepoint. 221 | 222 | Possible errors returned by this function are: 223 | 224 | - custom connection errors (301 - 306) 225 | 226 | :param: flags The custom flag associated with the connection. Can be either: 227 | - .ReadOnly 228 | - .ReadWrite 229 | - .ReadWriteCreate 230 | 231 | :param: closure A closure containing functions that will be executed on the custom connection 232 | 233 | :returns: An Int with the error code, or nil if there was no error 234 | */ 235 | public static func executeWithConnection(flags: SD.Flags, closure: ()->Void) -> Int? { 236 | 237 | var error: Int? = nil 238 | let task: ()->Void = { 239 | if let err = SQLiteDB.sharedInstance.openWithFlags(flags.toSQL()) { 240 | error = err 241 | return 242 | } 243 | closure() 244 | if let err = SQLiteDB.sharedInstance.closeCustomConnection() { 245 | error = err 246 | return 247 | } 248 | } 249 | putOnThread(task) 250 | return error 251 | 252 | } 253 | 254 | 255 | // MARK: - Escaping Objects 256 | 257 | /** 258 | Escape an object to be inserted into a SQLite statement as a value 259 | 260 | NOTE: Supported object types are: String, Int, Double, Bool, NSData, NSDate, and nil. All other data types will return the String value "NULL", and a warning message will be printed. 261 | 262 | :param: obj The value to be escaped 263 | 264 | :returns: The escaped value as a String, ready to be inserted into a SQL statement. Note: Single quotes (') will be placed around the entire value, if necessary. 265 | */ 266 | public static func escapeValue(obj: AnyObject?) -> String { 267 | return SQLiteDB.sharedInstance.escapeValue(obj) 268 | } 269 | 270 | /** 271 | Escape a string to be inserted into a SQLite statement as an indentifier (e.g. table or column name) 272 | 273 | :param: obj The identifier to be escaped. NOTE: This object must be of type String. 274 | 275 | :returns: The escaped identifier as a String, ready to be inserted into a SQL statement. Note: Double quotes (") will be placed around the entire identifier. 276 | */ 277 | public static func escapeIdentifier(obj: String) -> String { 278 | return SQLiteDB.sharedInstance.escapeIdentifier(obj) 279 | } 280 | 281 | 282 | // MARK: - Tables 283 | 284 | /** 285 | Create A Table With The Provided Column Names and Types 286 | 287 | Note: The ID field is created automatically as "INTEGER PRIMARY KEY AUTOINCREMENT" 288 | 289 | Possible errors returned by this function are: 290 | 291 | - SQLite errors (0 - 101) 292 | 293 | :param: table The table name to be created 294 | :param: columnNamesAndTypes A dictionary where the key = column name, and the value = data type 295 | 296 | :returns: An Int with the error code, or nil if there was no error 297 | */ 298 | public static func createTable(table: String, withColumnNamesAndTypes values: [String: SwiftData.DataType]) -> Int? { 299 | 300 | var error: Int? = nil 301 | let task: ()->Void = { 302 | if let err = SQLiteDB.sharedInstance.open() { 303 | error = err 304 | return 305 | } 306 | error = SQLiteDB.sharedInstance.createSQLTable(table, withColumnsAndTypes: values) 307 | SQLiteDB.sharedInstance.close() 308 | } 309 | putOnThread(task) 310 | return error 311 | 312 | } 313 | 314 | /** 315 | Delete a SQLite table by name 316 | 317 | Possible errors returned by this function are: 318 | 319 | - SQLite errors (0 - 101) 320 | 321 | :param: table The table name to be deleted 322 | 323 | :returns: An Int with the error code, or nil if there was no error 324 | */ 325 | public static func deleteTable(table: String) -> Int? { 326 | 327 | var error: Int? = nil 328 | let task: ()->Void = { 329 | if let err = SQLiteDB.sharedInstance.open() { 330 | error = err 331 | return 332 | } 333 | error = SQLiteDB.sharedInstance.deleteSQLTable(table) 334 | SQLiteDB.sharedInstance.close() 335 | } 336 | putOnThread(task) 337 | return error 338 | 339 | } 340 | 341 | /** 342 | Obtain a list of the existing SQLite table names 343 | 344 | Possible errors returned by this function are: 345 | 346 | - SQLite errors (0 - 101) 347 | - Table query error (403) 348 | 349 | :returns: A tuple containing an Array of all existing SQLite table names, and an Int with the error code or nil if there was no error 350 | */ 351 | public static func existingTables() -> (result: [String], error: Int?) { 352 | 353 | var result = [String] () 354 | var error: Int? = nil 355 | let task: ()->Void = { 356 | if let err = SQLiteDB.sharedInstance.open() { 357 | error = err 358 | return 359 | } 360 | (result, error) = SQLiteDB.sharedInstance.existingTables() 361 | SQLiteDB.sharedInstance.close() 362 | } 363 | putOnThread(task) 364 | return (result, error) 365 | 366 | } 367 | 368 | 369 | // MARK: - Misc 370 | 371 | 372 | /** 373 | Obtain the error message relating to the provided error code 374 | 375 | :param: code The error code provided 376 | 377 | :returns: The error message relating to the provided error code 378 | */ 379 | public static func errorMessageForCode(code: Int) -> String { 380 | return SwiftData.SDError.errorMessageFromCode(code) 381 | } 382 | 383 | /** 384 | Obtain the database path 385 | 386 | :returns: The path to the SwiftData database 387 | */ 388 | public static func databasePath() -> String { 389 | return SQLiteDB.sharedInstance.dbPath 390 | } 391 | 392 | /** 393 | Obtain the last inserted row id 394 | 395 | Note: Care should be taken when the database is being accessed from multiple threads. The value could possibly return the last inserted row ID for another operation if another thread executes after your intended operation but before this function call. 396 | 397 | Possible errors returned by this function are: 398 | 399 | - SQLite errors (0 - 101) 400 | 401 | :returns: A tuple of he ID of the last successfully inserted row's, and an Int of the error code or nil if there was no error 402 | */ 403 | public static func lastInsertedRowID() -> (rowID: Int, error: Int?) { 404 | 405 | var result = 0 406 | var error: Int? = nil 407 | let task: ()->Void = { 408 | if let err = SQLiteDB.sharedInstance.open() { 409 | error = err 410 | return 411 | } 412 | result = SQLiteDB.sharedInstance.lastInsertedRowID() 413 | SQLiteDB.sharedInstance.close() 414 | } 415 | putOnThread(task) 416 | return (result, error) 417 | 418 | } 419 | 420 | /** 421 | Obtain the number of rows modified by the most recently completed SQLite statement (INSERT, UPDATE, or DELETE) 422 | 423 | Note: Care should be taken when the database is being accessed from multiple threads. The value could possibly return the number of rows modified for another operation if another thread executes after your intended operation but before this function call. 424 | 425 | Possible errors returned by this function are: 426 | 427 | - SQLite errors (0 - 101) 428 | 429 | :returns: A tuple of the number of rows modified by the most recently completed SQLite statement, and an Int with the error code or nil if there was no error 430 | */ 431 | public static func numberOfRowsModified() -> (rowID: Int, error: Int?) { 432 | 433 | var result = 0 434 | var error: Int? = nil 435 | let task: ()->Void = { 436 | if let err = SQLiteDB.sharedInstance.open() { 437 | error = err 438 | return 439 | } 440 | result = SQLiteDB.sharedInstance.numberOfRowsModified() 441 | SQLiteDB.sharedInstance.close() 442 | } 443 | putOnThread(task) 444 | return (result, error) 445 | 446 | } 447 | 448 | 449 | // MARK: - Indexes 450 | 451 | /** 452 | Create a SQLite index on the specified table and column(s) 453 | 454 | Possible errors returned by this function are: 455 | 456 | - SQLite errors (0 - 101) 457 | - Index error (401) 458 | 459 | :param: name The index name that is being created 460 | :param: onColumns An array of column names that the index will be applied to (must be one column or greater) 461 | :param: inTable The table name where the index is being created 462 | :param: isUnique True if the index should be unique, false if it should not be unique (defaults to false) 463 | 464 | :returns: An Int with the error code, or nil if there was no error 465 | */ 466 | public static func createIndex(#name: String, onColumns: [String], inTable: String, isUnique: Bool = false) -> Int? { 467 | 468 | var error: Int? = nil 469 | let task: ()->Void = { 470 | if let err = SQLiteDB.sharedInstance.open() { 471 | error = err 472 | return 473 | } 474 | error = SQLiteDB.sharedInstance.createIndex(name, columns: onColumns, table: inTable, unique: isUnique) 475 | SQLiteDB.sharedInstance.close() 476 | } 477 | putOnThread(task) 478 | return error 479 | 480 | } 481 | 482 | /** 483 | Remove a SQLite index by its name 484 | 485 | Possible errors returned by this function are: 486 | 487 | - SQLite errors (0 - 101) 488 | 489 | :param: indexName The name of the index to be removed 490 | 491 | :returns: An Int with the error code, or nil if there was no error 492 | */ 493 | public static func removeIndex(indexName: String) -> Int? { 494 | 495 | var error: Int? = nil 496 | let task: ()->Void = { 497 | if let err = SQLiteDB.sharedInstance.open() { 498 | error = err 499 | return 500 | } 501 | error = SQLiteDB.sharedInstance.removeIndex(indexName) 502 | SQLiteDB.sharedInstance.close() 503 | } 504 | putOnThread(task) 505 | return error 506 | 507 | } 508 | 509 | /** 510 | Obtain a list of all existing indexes 511 | 512 | Possible errors returned by this function are: 513 | 514 | - SQLite errors (0 - 101) 515 | - Index error (402) 516 | 517 | :returns: A tuple containing an Array of all existing index names on the SQLite database, and an Int with the error code or nil if there was no error 518 | */ 519 | public static func existingIndexes() -> (result: [String], error: Int?) { 520 | 521 | var result = [String] () 522 | var error: Int? = nil 523 | let task: ()->Void = { 524 | if let err = SQLiteDB.sharedInstance.open() { 525 | error = err 526 | return 527 | } 528 | (result, error) = SQLiteDB.sharedInstance.existingIndexes() 529 | SQLiteDB.sharedInstance.close() 530 | } 531 | putOnThread(task) 532 | return (result, error) 533 | 534 | } 535 | 536 | /** 537 | Obtain a list of all existing indexes on a specific table 538 | 539 | Possible errors returned by this function are: 540 | 541 | - SQLite errors (0 - 101) 542 | - Index error (402) 543 | 544 | :param: table The name of the table that is being queried for indexes 545 | 546 | :returns: A tuple containing an Array of all existing index names in the table, and an Int with the error code or nil if there was no error 547 | */ 548 | public static func existingIndexesForTable(table: String) -> (result: [String], error: Int?) { 549 | 550 | var result = [String] () 551 | var error: Int? = nil 552 | let task: ()->Void = { 553 | if let err = SQLiteDB.sharedInstance.open() { 554 | error = err 555 | return 556 | } 557 | (result, error) = SQLiteDB.sharedInstance.existingIndexesForTable(table) 558 | SQLiteDB.sharedInstance.close() 559 | } 560 | putOnThread(task) 561 | return (result, error) 562 | 563 | } 564 | 565 | 566 | // MARK: - Transactions and Savepoints 567 | 568 | /** 569 | Execute commands within a single exclusive transaction 570 | 571 | A connection to the database is opened and is not closed until the end of the transaction. A transaction cannot be embedded into another transaction or savepoint. 572 | 573 | Possible errors returned by this function are: 574 | 575 | - SQLite errors (0 - 101) 576 | - Transaction errors (501 - 502) 577 | 578 | :param: transactionClosure A closure containing commands that will execute as part of a single transaction. If the transactionClosure returns true, the changes made within the closure will be committed. If false, the changes will be rolled back and will not be saved. 579 | 580 | :returns: An Int with the error code, or nil if there was no error committing or rolling back the transaction 581 | */ 582 | public static func transaction(transactionClosure: ()->Bool) -> Int? { 583 | 584 | var error: Int? = nil 585 | let task: ()->Void = { 586 | if let err = SQLiteDB.sharedInstance.open() { 587 | error = err 588 | return 589 | } 590 | if let err = SQLiteDB.sharedInstance.beginTransaction() { 591 | SQLiteDB.sharedInstance.close() 592 | error = err 593 | return 594 | } 595 | if transactionClosure() { 596 | if let err = SQLiteDB.sharedInstance.commitTransaction() { 597 | error = err 598 | } 599 | } else { 600 | if let err = SQLiteDB.sharedInstance.rollbackTransaction() { 601 | error = err 602 | } 603 | } 604 | SQLiteDB.sharedInstance.close() 605 | } 606 | putOnThread(task) 607 | return error 608 | 609 | } 610 | 611 | /** 612 | Execute commands within a single savepoint 613 | 614 | A connection to the database is opened and is not closed until the end of the savepoint (or the end of the last savepoint, if embedded). 615 | 616 | NOTE: Unlike transactions, savepoints may be embedded into other savepoints or transactions. 617 | 618 | Possible errors returned by this function are: 619 | 620 | - SQLite errors (0 - 101) 621 | 622 | :param: savepointClosure A closure containing commands that will execute as part of a single savepoint. If the savepointClosure returns true, the changes made within the closure will be released. If false, the changes will be rolled back and will not be saved. 623 | 624 | :returns: An Int with the error code, or nil if there was no error releasing or rolling back the savepoint 625 | */ 626 | public static func savepoint(savepointClosure: ()->Bool) -> Int? { 627 | 628 | var error: Int? = nil 629 | let task: ()->Void = { 630 | if let err = SQLiteDB.sharedInstance.open() { 631 | error = err 632 | return 633 | } 634 | if let err = SQLiteDB.sharedInstance.beginSavepoint() { 635 | SQLiteDB.sharedInstance.close() 636 | error = err 637 | return 638 | } 639 | if savepointClosure() { 640 | if let err = SQLiteDB.sharedInstance.releaseSavepoint() { 641 | error = err 642 | } 643 | } else { 644 | if let err = SQLiteDB.sharedInstance.rollbackSavepoint() { 645 | println("Error rolling back to savepoint") 646 | --SQLiteDB.sharedInstance.savepointsOpen 647 | SQLiteDB.sharedInstance.close() 648 | error = err 649 | return 650 | } 651 | if let err = SQLiteDB.sharedInstance.releaseSavepoint() { 652 | error = err 653 | } 654 | } 655 | SQLiteDB.sharedInstance.close() 656 | } 657 | putOnThread(task) 658 | return error 659 | 660 | } 661 | 662 | /** 663 | Convenience function to save a UIImage to disk and return the ID 664 | 665 | :param: image The UIImage to be saved 666 | 667 | :returns: The ID of the saved image as a String, or nil if there was an error saving the image to disk 668 | */ 669 | public static func saveUIImage(image: UIImage) -> String? { 670 | 671 | let docsPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0] as String 672 | let imageDirPath = docsPath.stringByAppendingPathComponent("SwiftDataImages") 673 | if !NSFileManager.defaultManager().fileExistsAtPath(imageDirPath) { 674 | if !NSFileManager.defaultManager().createDirectoryAtPath(imageDirPath, withIntermediateDirectories: false, attributes: nil, error: nil) { 675 | println("Error creating SwiftData image folder") 676 | return nil 677 | } 678 | } 679 | let imageID = NSUUID().UUIDString 680 | let imagePath = imageDirPath.stringByAppendingPathComponent(imageID) 681 | let imageAsData = UIImagePNGRepresentation(image) 682 | if !imageAsData.writeToFile(imagePath, atomically: true) { 683 | println("Error saving image") 684 | return nil 685 | } 686 | return imageID 687 | 688 | } 689 | 690 | /** 691 | Convenience function to delete a UIImage with the specified ID 692 | 693 | :param: id The id of the UIImage 694 | 695 | :returns: True if the image was successfully deleted, or false if there was an error during the deletion 696 | */ 697 | public static func deleteUIImageWithID(id: String) -> Bool { 698 | 699 | let docsPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0] as String 700 | let imageDirPath = docsPath.stringByAppendingPathComponent("SwiftDataImages") 701 | let fullPath = imageDirPath.stringByAppendingPathComponent(id) 702 | return NSFileManager.defaultManager().removeItemAtPath(fullPath, error: nil) 703 | 704 | } 705 | 706 | 707 | // MARK: - SQLiteDB Class 708 | 709 | private class SQLiteDB { 710 | 711 | class var sharedInstance: SQLiteDB { 712 | struct Singleton { 713 | static let instance = SQLiteDB() 714 | } 715 | return Singleton.instance 716 | } 717 | var sqliteDB: COpaquePointer = nil 718 | var dbPath = SQLiteDB.createPath() 719 | var inTransaction = false 720 | var isConnected = false 721 | var openWithFlags = false 722 | var savepointsOpen = 0 723 | let queue = dispatch_queue_create("SwiftData.DatabaseQueue", DISPATCH_QUEUE_SERIAL) 724 | 725 | 726 | // MARK: - Database Handling Functions 727 | 728 | //open a connection to the sqlite3 database 729 | func open() -> Int? { 730 | 731 | if inTransaction || openWithFlags || savepointsOpen > 0 { 732 | return nil 733 | } 734 | if sqliteDB != nil || isConnected { 735 | return nil 736 | } 737 | let status = sqlite3_open(dbPath.cStringUsingEncoding(NSUTF8StringEncoding)!, &sqliteDB) 738 | if status != SQLITE_OK { 739 | println("SwiftData Error -> During: Opening Database") 740 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 741 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 742 | println(" -> Details: \(errMsg)") 743 | } 744 | return Int(status) 745 | } 746 | isConnected = true 747 | return nil 748 | 749 | } 750 | 751 | //open a connection to the sqlite3 database with flags 752 | func openWithFlags(flags: Int32) -> Int? { 753 | 754 | if inTransaction { 755 | println("SwiftData Error -> During: Opening Database with Flags") 756 | println(" -> Code: 302 - Cannot open a custom connection inside a transaction") 757 | return 302 758 | } 759 | if openWithFlags { 760 | println("SwiftData Error -> During: Opening Database with Flags") 761 | println(" -> Code: 301 - A custom connection is already open") 762 | return 301 763 | } 764 | if savepointsOpen > 0 { 765 | println("SwiftData Error -> During: Opening Database with Flags") 766 | println(" -> Code: 303 - Cannot open a custom connection inside a savepoint") 767 | return 303 768 | } 769 | if isConnected { 770 | println("SwiftData Error -> During: Opening Database with Flags") 771 | println(" -> Code: 301 - A custom connection is already open") 772 | return 301 773 | } 774 | let status = sqlite3_open_v2(dbPath.cStringUsingEncoding(NSUTF8StringEncoding)!, &sqliteDB, flags, nil) 775 | if status != SQLITE_OK { 776 | println("SwiftData Error -> During: Opening Database with Flags") 777 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 778 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 779 | println(" -> Details: \(errMsg)") 780 | } 781 | return Int(status) 782 | } 783 | isConnected = true 784 | openWithFlags = true 785 | return nil 786 | 787 | } 788 | 789 | //close the connection to to the sqlite3 database 790 | func close() { 791 | 792 | if inTransaction || openWithFlags || savepointsOpen > 0 { 793 | return 794 | } 795 | if sqliteDB == nil || !isConnected { 796 | return 797 | } 798 | let status = sqlite3_close(sqliteDB) 799 | if status != SQLITE_OK { 800 | println("SwiftData Error -> During: Closing Database") 801 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 802 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 803 | println(" -> Details: \(errMsg)") 804 | } 805 | } 806 | sqliteDB = nil 807 | isConnected = false 808 | 809 | } 810 | 811 | //close a custom connection to the sqlite3 database 812 | func closeCustomConnection() -> Int? { 813 | 814 | if inTransaction { 815 | println("SwiftData Error -> During: Closing Database with Flags") 816 | println(" -> Code: 305 - Cannot close a custom connection inside a transaction") 817 | return 305 818 | } 819 | if savepointsOpen > 0 { 820 | println("SwiftData Error -> During: Closing Database with Flags") 821 | println(" -> Code: 306 - Cannot close a custom connection inside a savepoint") 822 | return 306 823 | } 824 | if !openWithFlags { 825 | println("SwiftData Error -> During: Closing Database with Flags") 826 | println(" -> Code: 304 - A custom connection is not currently open") 827 | return 304 828 | } 829 | let status = sqlite3_close(sqliteDB) 830 | sqliteDB = nil 831 | isConnected = false 832 | openWithFlags = false 833 | if status != SQLITE_OK { 834 | println("SwiftData Error -> During: Closing Database with Flags") 835 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 836 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 837 | println(" -> Details: \(errMsg)") 838 | } 839 | return Int(status) 840 | } 841 | return nil 842 | 843 | } 844 | 845 | //create the database path 846 | class func createPath() -> String { 847 | 848 | let docsPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0] as String 849 | let databaseStr = "SwiftData.sqlite" 850 | let dbPath = docsPath.stringByAppendingPathComponent(databaseStr) 851 | return dbPath 852 | 853 | } 854 | 855 | //begin a transaction 856 | func beginTransaction() -> Int? { 857 | 858 | if savepointsOpen > 0 { 859 | println("SwiftData Error -> During: Beginning Transaction") 860 | println(" -> Code: 501 - Cannot begin a transaction within a savepoint") 861 | return 501 862 | } 863 | if inTransaction { 864 | println("SwiftData Error -> During: Beginning Transaction") 865 | println(" -> Code: 502 - Cannot begin a transaction within another transaction") 866 | return 502 867 | } 868 | if let error = executeChange("BEGIN EXCLUSIVE") { 869 | return error 870 | } 871 | inTransaction = true 872 | return nil 873 | 874 | } 875 | 876 | //rollback a transaction 877 | func rollbackTransaction() -> Int? { 878 | 879 | let error = executeChange("ROLLBACK") 880 | inTransaction = false 881 | return error 882 | 883 | } 884 | 885 | //commit a transaction 886 | func commitTransaction() -> Int? { 887 | 888 | let error = executeChange("COMMIT") 889 | inTransaction = false 890 | if let err = error { 891 | rollbackTransaction() 892 | return err 893 | } 894 | return nil 895 | 896 | } 897 | 898 | //begin a savepoint 899 | func beginSavepoint() -> Int? { 900 | 901 | if let error = executeChange("SAVEPOINT 'savepoint\(savepointsOpen + 1)'") { 902 | return error 903 | } 904 | ++savepointsOpen 905 | return nil 906 | 907 | } 908 | 909 | //rollback a savepoint 910 | func rollbackSavepoint() -> Int? { 911 | return executeChange("ROLLBACK TO 'savepoint\(savepointsOpen)'") 912 | } 913 | 914 | //release a savepoint 915 | func releaseSavepoint() -> Int? { 916 | 917 | let error = executeChange("RELEASE 'savepoint\(savepointsOpen)'") 918 | --savepointsOpen 919 | return error 920 | 921 | } 922 | 923 | //get last inserted row id 924 | func lastInsertedRowID() -> Int { 925 | let id = sqlite3_last_insert_rowid(sqliteDB) 926 | return Int(id) 927 | } 928 | 929 | //number of rows changed by last update 930 | func numberOfRowsModified() -> Int { 931 | return Int(sqlite3_changes(sqliteDB)) 932 | } 933 | 934 | //return value of column 935 | func getColumnValue(statement: COpaquePointer, index: Int32, type: String) -> AnyObject? { 936 | 937 | switch type { 938 | case "INT", "INTEGER", "TINYINT", "SMALLINT", "MEDIUMINT", "BIGINT", "UNSIGNED BIG INT", "INT2", "INT8": 939 | if sqlite3_column_type(statement, index) == SQLITE_NULL { 940 | return nil 941 | } 942 | return Int(sqlite3_column_int(statement, index)) 943 | case "CHARACTER(20)", "VARCHAR(255)", "VARYING CHARACTER(255)", "NCHAR(55)", "NATIVE CHARACTER", "NVARCHAR(100)", "TEXT", "CLOB": 944 | let text = UnsafePointer(sqlite3_column_text(statement, index)) 945 | return String.fromCString(text) 946 | case "BLOB", "NONE": 947 | let blob = sqlite3_column_blob(statement, index) 948 | if blob != nil { 949 | let size = sqlite3_column_bytes(statement, index) 950 | return NSData(bytes: blob, length: Int(size)) 951 | } 952 | return nil 953 | case "REAL", "DOUBLE", "DOUBLE PRECISION", "FLOAT", "NUMERIC", "DECIMAL(10,5)": 954 | if sqlite3_column_type(statement, index) == SQLITE_NULL { 955 | return nil 956 | } 957 | return Double(sqlite3_column_double(statement, index)) 958 | case "BOOLEAN": 959 | if sqlite3_column_type(statement, index) == SQLITE_NULL { 960 | return nil 961 | } 962 | return sqlite3_column_int(statement, index) != 0 963 | case "DATE", "DATETIME": 964 | let dateFormatter = NSDateFormatter() 965 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 966 | let text = UnsafePointer(sqlite3_column_text(statement, index)) 967 | if let string = String.fromCString(text) { 968 | return dateFormatter.dateFromString(string) 969 | } 970 | println("SwiftData Warning -> The text date at column: \(index) could not be cast as a String, returning nil") 971 | return nil 972 | default: 973 | println("SwiftData Warning -> Column: \(index) is of an unrecognized type, returning nil") 974 | return nil 975 | } 976 | 977 | } 978 | 979 | 980 | // MARK: SQLite Execution Functions 981 | 982 | //execute a SQLite update from a SQL String 983 | func executeChange(sqlStr: String, withArgs: [AnyObject]? = nil) -> Int? { 984 | 985 | var sql = sqlStr 986 | if let args = withArgs { 987 | let result = bind(args, toSQL: sql) 988 | if let error = result.error { 989 | return error 990 | } else { 991 | sql = result.string 992 | } 993 | } 994 | var pStmt: COpaquePointer = nil 995 | var status = sqlite3_prepare_v2(SQLiteDB.sharedInstance.sqliteDB, sql, -1, &pStmt, nil) 996 | if status != SQLITE_OK { 997 | println("SwiftData Error -> During: SQL Prepare") 998 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 999 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 1000 | println(" -> Details: \(errMsg)") 1001 | } 1002 | sqlite3_finalize(pStmt) 1003 | return Int(status) 1004 | } 1005 | status = sqlite3_step(pStmt) 1006 | if status != SQLITE_DONE && status != SQLITE_OK { 1007 | println("SwiftData Error -> During: SQL Step") 1008 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 1009 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 1010 | println(" -> Details: \(errMsg)") 1011 | } 1012 | sqlite3_finalize(pStmt) 1013 | return Int(status) 1014 | } 1015 | sqlite3_finalize(pStmt) 1016 | return nil 1017 | 1018 | } 1019 | 1020 | //execute a SQLite query from a SQL String 1021 | func executeQuery(sqlStr: String, withArgs: [AnyObject]? = nil) -> (result: [SDRow], error: Int?) { 1022 | 1023 | var resultSet = [SDRow]() 1024 | var sql = sqlStr 1025 | if let args = withArgs { 1026 | let result = bind(args, toSQL: sql) 1027 | if let err = result.error { 1028 | return (resultSet, err) 1029 | } else { 1030 | sql = result.string 1031 | } 1032 | } 1033 | var pStmt: COpaquePointer = nil 1034 | var status = sqlite3_prepare_v2(SQLiteDB.sharedInstance.sqliteDB, sql, -1, &pStmt, nil) 1035 | if status != SQLITE_OK { 1036 | println("SwiftData Error -> During: SQL Prepare") 1037 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 1038 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 1039 | println(" -> Details: \(errMsg)") 1040 | } 1041 | sqlite3_finalize(pStmt) 1042 | return (resultSet, Int(status)) 1043 | } 1044 | var columnCount: Int32 = 0 1045 | var next = true 1046 | while next { 1047 | status = sqlite3_step(pStmt) 1048 | if status == SQLITE_ROW { 1049 | columnCount = sqlite3_column_count(pStmt) 1050 | var row = SDRow() 1051 | for var i: Int32 = 0; i < columnCount; ++i { 1052 | let columnName = String.fromCString(sqlite3_column_name(pStmt, i))! 1053 | if let columnType = String.fromCString(sqlite3_column_decltype(pStmt, i))?.uppercaseString { 1054 | if let columnValue: AnyObject = getColumnValue(pStmt, index: i, type: columnType) { 1055 | row[columnName] = SDColumn(obj: columnValue) 1056 | } 1057 | } else { 1058 | var columnType = "" 1059 | switch sqlite3_column_type(pStmt, i) { 1060 | case SQLITE_INTEGER: 1061 | columnType = "INTEGER" 1062 | case SQLITE_FLOAT: 1063 | columnType = "FLOAT" 1064 | case SQLITE_TEXT: 1065 | columnType = "TEXT" 1066 | case SQLITE3_TEXT: 1067 | columnType = "TEXT" 1068 | case SQLITE_BLOB: 1069 | columnType = "BLOB" 1070 | case SQLITE_NULL: 1071 | columnType = "NULL" 1072 | default: 1073 | columnType = "NULL" 1074 | } 1075 | if let columnValue: AnyObject = getColumnValue(pStmt, index: i, type: columnType) { 1076 | row[columnName] = SDColumn(obj: columnValue) 1077 | } 1078 | } 1079 | } 1080 | resultSet.append(row) 1081 | } else if status == SQLITE_DONE { 1082 | next = false 1083 | } else { 1084 | println("SwiftData Error -> During: SQL Step") 1085 | println(" -> Code: \(status) - " + SDError.errorMessageFromCode(Int(status))) 1086 | if let errMsg = String.fromCString(sqlite3_errmsg(SQLiteDB.sharedInstance.sqliteDB)) { 1087 | println(" -> Details: \(errMsg)") 1088 | } 1089 | sqlite3_finalize(pStmt) 1090 | return (resultSet, Int(status)) 1091 | } 1092 | } 1093 | sqlite3_finalize(pStmt) 1094 | return (resultSet, nil) 1095 | 1096 | } 1097 | 1098 | } 1099 | 1100 | 1101 | // MARK: - SDRow 1102 | 1103 | public struct SDRow { 1104 | 1105 | var values = [String: SDColumn]() 1106 | public subscript(key: String) -> SDColumn? { 1107 | get { 1108 | return values[key] 1109 | } 1110 | set(newValue) { 1111 | values[key] = newValue 1112 | } 1113 | } 1114 | 1115 | } 1116 | 1117 | 1118 | // MARK: - SDColumn 1119 | 1120 | public struct SDColumn { 1121 | 1122 | var value: AnyObject 1123 | init(obj: AnyObject) { 1124 | value = obj 1125 | } 1126 | 1127 | //return value by type 1128 | 1129 | /** 1130 | Return the column value as a String 1131 | 1132 | :returns: An Optional String corresponding to the apprioriate column value. Will be nil if: the column name does not exist, the value cannot be cast as a String, or the value is NULL 1133 | */ 1134 | public func asString() -> String? { 1135 | return value as? String 1136 | } 1137 | 1138 | /** 1139 | Return the column value as an Int 1140 | 1141 | :returns: An Optional Int corresponding to the apprioriate column value. Will be nil if: the column name does not exist, the value cannot be cast as a Int, or the value is NULL 1142 | */ 1143 | public func asInt() -> Int? { 1144 | return value as? Int 1145 | } 1146 | 1147 | /** 1148 | Return the column value as a Double 1149 | 1150 | :returns: An Optional Double corresponding to the apprioriate column value. Will be nil if: the column name does not exist, the value cannot be cast as a Double, or the value is NULL 1151 | */ 1152 | public func asDouble() -> Double? { 1153 | return value as? Double 1154 | } 1155 | 1156 | /** 1157 | Return the column value as a Bool 1158 | 1159 | :returns: An Optional Bool corresponding to the apprioriate column value. Will be nil if: the column name does not exist, the value cannot be cast as a Bool, or the value is NULL 1160 | */ 1161 | public func asBool() -> Bool? { 1162 | return value as? Bool 1163 | } 1164 | 1165 | /** 1166 | Return the column value as NSData 1167 | 1168 | :returns: An Optional NSData object corresponding to the apprioriate column value. Will be nil if: the column name does not exist, the value cannot be cast as NSData, or the value is NULL 1169 | */ 1170 | public func asData() -> NSData? { 1171 | return value as? NSData 1172 | } 1173 | 1174 | /** 1175 | Return the column value as an NSDate 1176 | 1177 | :returns: An Optional NSDate corresponding to the apprioriate column value. Will be nil if: the column name does not exist, the value cannot be cast as an NSDate, or the value is NULL 1178 | */ 1179 | public func asDate() -> NSDate? { 1180 | return value as? NSDate 1181 | } 1182 | 1183 | /** 1184 | Return the column value as an AnyObject 1185 | 1186 | :returns: An Optional AnyObject corresponding to the apprioriate column value. Will be nil if: the column name does not exist, the value cannot be cast as an AnyObject, or the value is NULL 1187 | */ 1188 | public func asAnyObject() -> AnyObject? { 1189 | return value 1190 | } 1191 | 1192 | /** 1193 | Return the column value path as a UIImage 1194 | 1195 | :returns: An Optional UIImage corresponding to the path of the apprioriate column value. Will be nil if: the column name does not exist, the value of the specified path cannot be cast as a UIImage, or the value is NULL 1196 | */ 1197 | public func asUIImage() -> UIImage? { 1198 | 1199 | if let path = value as? String{ 1200 | let docsPath = NSSearchPathForDirectoriesInDomains(NSSearchPathDirectory.DocumentDirectory, NSSearchPathDomainMask.UserDomainMask, true)[0] as String 1201 | let imageDirPath = docsPath.stringByAppendingPathComponent("SwiftDataImages") 1202 | let fullPath = imageDirPath.stringByAppendingPathComponent(path) 1203 | if !NSFileManager.defaultManager().fileExistsAtPath(fullPath) { 1204 | println("SwiftData Error -> Invalid image ID provided") 1205 | return nil 1206 | } 1207 | if let imageAsData = NSData(contentsOfFile: fullPath) { 1208 | return UIImage(data: imageAsData) 1209 | } 1210 | } 1211 | return nil 1212 | 1213 | } 1214 | 1215 | } 1216 | 1217 | 1218 | // MARK: - Error Handling 1219 | 1220 | private struct SDError { 1221 | 1222 | } 1223 | 1224 | } 1225 | 1226 | 1227 | // MARK: - Threading 1228 | 1229 | extension SwiftData { 1230 | 1231 | private static func putOnThread(task: ()->Void) { 1232 | if SQLiteDB.sharedInstance.inTransaction || SQLiteDB.sharedInstance.savepointsOpen > 0 || SQLiteDB.sharedInstance.openWithFlags { 1233 | task() 1234 | } else { 1235 | dispatch_sync(SQLiteDB.sharedInstance.queue) { 1236 | task() 1237 | } 1238 | } 1239 | } 1240 | 1241 | } 1242 | 1243 | 1244 | // MARK: - Escaping And Binding Functions 1245 | 1246 | extension SwiftData.SQLiteDB { 1247 | 1248 | func bind(objects: [AnyObject], toSQL sql: String) -> (string: String, error: Int?) { 1249 | 1250 | var newSql = "" 1251 | var bindIndex = 0 1252 | var i = false 1253 | for char in sql { 1254 | if char == "?" { 1255 | if bindIndex > objects.count - 1 { 1256 | println("SwiftData Error -> During: Object Binding") 1257 | println(" -> Code: 201 - Not enough objects to bind provided") 1258 | return ("", 201) 1259 | } 1260 | var obj = "" 1261 | if i { 1262 | if let str = objects[bindIndex] as? String { 1263 | obj = escapeIdentifier(str) 1264 | } else { 1265 | println("SwiftData Error -> During: Object Binding") 1266 | println(" -> Code: 203 - Object to bind as identifier must be a String at array location: \(bindIndex)") 1267 | return ("", 203) 1268 | } 1269 | newSql = newSql.substringToIndex(newSql.endIndex.predecessor()) 1270 | } else { 1271 | obj = escapeValue(objects[bindIndex]) 1272 | } 1273 | newSql += obj 1274 | ++bindIndex 1275 | } else { 1276 | newSql.append(char) 1277 | } 1278 | if char == "i" { 1279 | i = true 1280 | } else if i { 1281 | i = false 1282 | } 1283 | } 1284 | if bindIndex != objects.count { 1285 | println("SwiftData Error -> During: Object Binding") 1286 | println(" -> Code: 202 - Too many objects to bind provided") 1287 | return ("", 202) 1288 | } 1289 | return (newSql, nil) 1290 | 1291 | } 1292 | 1293 | //return escaped String value of AnyObject 1294 | func escapeValue(obj: AnyObject?) -> String { 1295 | 1296 | if let obj: AnyObject = obj { 1297 | if obj is String { 1298 | return "'\(escapeStringValue(obj as String))'" 1299 | } 1300 | if obj is Double || obj is Int { 1301 | return "\(obj)" 1302 | } 1303 | if obj is Bool { 1304 | if obj as Bool { 1305 | return "1" 1306 | } else { 1307 | return "0" 1308 | } 1309 | } 1310 | if obj is NSData { 1311 | let str = "\(obj)" 1312 | var newStr = "" 1313 | for char in str { 1314 | if char != "<" && char != ">" && char != " " { 1315 | newStr.append(char) 1316 | } 1317 | } 1318 | return "X'\(newStr)'" 1319 | } 1320 | if obj is NSDate { 1321 | let dateFormatter = NSDateFormatter() 1322 | dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" 1323 | return "\(escapeValue(dateFormatter.stringFromDate(obj as NSDate)))" 1324 | } 1325 | if obj is UIImage { 1326 | if let imageID = SD.saveUIImage(obj as UIImage) { 1327 | return "'\(escapeStringValue(imageID))'" 1328 | } 1329 | println("SwiftData Warning -> Cannot save image, NULL will be inserted into the database") 1330 | return "NULL" 1331 | } 1332 | println("SwiftData Warning -> Object \"\(obj)\" is not a supported type and will be inserted into the database as NULL") 1333 | return "NULL" 1334 | } else { 1335 | return "NULL" 1336 | } 1337 | 1338 | } 1339 | 1340 | //return escaped String identifier 1341 | func escapeIdentifier(obj: String) -> String { 1342 | return "\"\(escapeStringIdentifier(obj))\"" 1343 | } 1344 | 1345 | 1346 | //escape string 1347 | func escapeStringValue(str: String) -> String { 1348 | var escapedStr = "" 1349 | for char in str { 1350 | if char == "'" { 1351 | escapedStr += "'" 1352 | } 1353 | escapedStr.append(char) 1354 | } 1355 | return escapedStr 1356 | } 1357 | 1358 | //escape string 1359 | func escapeStringIdentifier(str: String) -> String { 1360 | var escapedStr = "" 1361 | for char in str { 1362 | if char == "\"" { 1363 | escapedStr += "\"" 1364 | } 1365 | escapedStr.append(char) 1366 | } 1367 | return escapedStr 1368 | } 1369 | 1370 | } 1371 | 1372 | 1373 | // MARK: - SQL Creation Functions 1374 | 1375 | extension SwiftData { 1376 | 1377 | /** 1378 | Column Data Types 1379 | 1380 | :param: StringVal A column with type String, corresponds to SQLite type "TEXT" 1381 | :param: IntVal A column with type Int, corresponds to SQLite type "INTEGER" 1382 | :param: DoubleVal A column with type Double, corresponds to SQLite type "DOUBLE" 1383 | :param: BoolVal A column with type Bool, corresponds to SQLite type "BOOLEAN" 1384 | :param: DataVal A column with type NSdata, corresponds to SQLite type "BLOB" 1385 | :param: DateVal A column with type NSDate, corresponds to SQLite type "DATE" 1386 | :param: UIImageVal A column with type String (the path value of saved UIImage), corresponds to SQLite type "TEXT" 1387 | */ 1388 | public enum DataType { 1389 | 1390 | case StringVal 1391 | case IntVal 1392 | case DoubleVal 1393 | case BoolVal 1394 | case DataVal 1395 | case DateVal 1396 | case UIImageVal 1397 | 1398 | private func toSQL() -> String { 1399 | 1400 | switch self { 1401 | case .StringVal, .UIImageVal: 1402 | return "TEXT" 1403 | case .IntVal: 1404 | return "INTEGER" 1405 | case .DoubleVal: 1406 | return "DOUBLE" 1407 | case .BoolVal: 1408 | return "BOOLEAN" 1409 | case .DataVal: 1410 | return "BLOB" 1411 | case .DateVal: 1412 | return "DATE" 1413 | } 1414 | } 1415 | 1416 | } 1417 | 1418 | /** 1419 | Flags for custom connection to the SQLite database 1420 | 1421 | :param: ReadOnly Opens the SQLite database with the flag "SQLITE_OPEN_READONLY" 1422 | :param: ReadWrite Opens the SQLite database with the flag "SQLITE_OPEN_READWRITE" 1423 | :param: ReadWriteCreate Opens the SQLite database with the flag "SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE" 1424 | */ 1425 | public enum Flags { 1426 | 1427 | case ReadOnly 1428 | case ReadWrite 1429 | case ReadWriteCreate 1430 | 1431 | private func toSQL() -> Int32 { 1432 | 1433 | switch self { 1434 | case .ReadOnly: 1435 | return SQLITE_OPEN_READONLY 1436 | case .ReadWrite: 1437 | return SQLITE_OPEN_READWRITE 1438 | case .ReadWriteCreate: 1439 | return SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE 1440 | } 1441 | 1442 | } 1443 | 1444 | } 1445 | 1446 | } 1447 | 1448 | 1449 | extension SwiftData.SQLiteDB { 1450 | 1451 | //create a table 1452 | func createSQLTable(table: String, withColumnsAndTypes values: [String: SwiftData.DataType]) -> Int? { 1453 | 1454 | var sqlStr = "CREATE TABLE \(table) (ID INTEGER PRIMARY KEY AUTOINCREMENT, " 1455 | var firstRun = true 1456 | for value in values { 1457 | if firstRun { 1458 | sqlStr += "\(escapeIdentifier(value.0)) \(value.1.toSQL())" 1459 | firstRun = false 1460 | } else { 1461 | sqlStr += ", \(escapeIdentifier(value.0)) \(value.1.toSQL())" 1462 | } 1463 | } 1464 | sqlStr += ")" 1465 | return executeChange(sqlStr) 1466 | 1467 | } 1468 | 1469 | //delete a table 1470 | func deleteSQLTable(table: String) -> Int? { 1471 | let sqlStr = "DROP TABLE \(table)" 1472 | return executeChange(sqlStr) 1473 | } 1474 | 1475 | //get existing table names 1476 | func existingTables() -> (result: [String], error: Int?) { 1477 | let sqlStr = "SELECT name FROM sqlite_master WHERE type = 'table'" 1478 | var tableArr = [String]() 1479 | let results = executeQuery(sqlStr) 1480 | if let err = results.error { 1481 | return (tableArr, err) 1482 | } 1483 | for row in results.result { 1484 | if let table = row["name"]?.asString() { 1485 | tableArr.append(table) 1486 | } else { 1487 | println("SwiftData Error -> During: Finding Existing Tables") 1488 | println(" -> Code: 403 - Error extracting table names from sqlite_master") 1489 | return (tableArr, 403) 1490 | } 1491 | } 1492 | return (tableArr, nil) 1493 | } 1494 | 1495 | //create an index 1496 | func createIndex(name: String, columns: [String], table: String, unique: Bool) -> Int? { 1497 | 1498 | if columns.count < 1 { 1499 | println("SwiftData Error -> During: Creating Index") 1500 | println(" -> Code: 401 - At least one column name must be provided") 1501 | return 401 1502 | } 1503 | var sqlStr = "" 1504 | if unique { 1505 | sqlStr = "CREATE UNIQUE INDEX \(name) ON \(table) (" 1506 | } else { 1507 | sqlStr = "CREATE INDEX \(name) ON \(table) (" 1508 | } 1509 | var firstRun = true 1510 | for column in columns { 1511 | if firstRun { 1512 | sqlStr += column 1513 | firstRun = false 1514 | } else { 1515 | sqlStr += ", \(column)" 1516 | } 1517 | } 1518 | sqlStr += ")" 1519 | return executeChange(sqlStr) 1520 | 1521 | } 1522 | 1523 | //remove an index 1524 | func removeIndex(name: String) -> Int? { 1525 | let sqlStr = "DROP INDEX \(name)" 1526 | return executeChange(sqlStr) 1527 | } 1528 | 1529 | //obtain list of existing indexes 1530 | func existingIndexes() -> (result: [String], error: Int?) { 1531 | 1532 | let sqlStr = "SELECT name FROM sqlite_master WHERE type = 'index'" 1533 | var indexArr = [String]() 1534 | let results = executeQuery(sqlStr) 1535 | if let err = results.error { 1536 | return (indexArr, err) 1537 | } 1538 | for res in results.result { 1539 | if let index = res["name"]?.asString() { 1540 | indexArr.append(index) 1541 | } else { 1542 | println("SwiftData Error -> During: Finding Existing Indexes") 1543 | println(" -> Code: 402 - Error extracting index names from sqlite_master") 1544 | println("Error finding existing indexes -> Error extracting index names from sqlite_master") 1545 | return (indexArr, 402) 1546 | } 1547 | } 1548 | return (indexArr, nil) 1549 | 1550 | } 1551 | 1552 | //obtain list of existing indexes for a specific table 1553 | func existingIndexesForTable(table: String) -> (result: [String], error: Int?) { 1554 | 1555 | let sqlStr = "SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = '\(table)'" 1556 | var indexArr = [String]() 1557 | let results = executeQuery(sqlStr) 1558 | if let err = results.error { 1559 | return (indexArr, err) 1560 | } 1561 | for res in results.result { 1562 | if let index = res["name"]?.asString() { 1563 | indexArr.append(index) 1564 | } else { 1565 | println("SwiftData Error -> During: Finding Existing Indexes for a Table") 1566 | println(" -> Code: 402 - Error extracting index names from sqlite_master") 1567 | return (indexArr, 402) 1568 | } 1569 | } 1570 | return (indexArr, nil) 1571 | 1572 | } 1573 | 1574 | } 1575 | 1576 | 1577 | // MARK: - SDError Functions 1578 | 1579 | extension SwiftData.SDError { 1580 | 1581 | //get the error message from the error code 1582 | private static func errorMessageFromCode(errorCode: Int) -> String { 1583 | 1584 | switch errorCode { 1585 | 1586 | //no error 1587 | 1588 | case -1: 1589 | return "No error" 1590 | 1591 | //SQLite error codes and descriptions as per: http://www.sqlite.org/c3ref/c_abort.html 1592 | case 0: 1593 | return "Successful result" 1594 | case 1: 1595 | return "SQL error or missing database" 1596 | case 2: 1597 | return "Internal logic error in SQLite" 1598 | case 3: 1599 | return "Access permission denied" 1600 | case 4: 1601 | return "Callback routine requested an abort" 1602 | case 5: 1603 | return "The database file is locked" 1604 | case 6: 1605 | return "A table in the database is locked" 1606 | case 7: 1607 | return "A malloc() failed" 1608 | case 8: 1609 | return "Attempt to write a readonly database" 1610 | case 9: 1611 | return "Operation terminated by sqlite3_interrupt()" 1612 | case 10: 1613 | return "Some kind of disk I/O error occurred" 1614 | case 11: 1615 | return "The database disk image is malformed" 1616 | case 12: 1617 | return "Unknown opcode in sqlite3_file_control()" 1618 | case 13: 1619 | return "Insertion failed because database is full" 1620 | case 14: 1621 | return "Unable to open the database file" 1622 | case 15: 1623 | return "Database lock protocol error" 1624 | case 16: 1625 | return "Database is empty" 1626 | case 17: 1627 | return "The database schema changed" 1628 | case 18: 1629 | return "String or BLOB exceeds size limit" 1630 | case 19: 1631 | return "Abort due to constraint violation" 1632 | case 20: 1633 | return "Data type mismatch" 1634 | case 21: 1635 | return "Library used incorrectly" 1636 | case 22: 1637 | return "Uses OS features not supported on host" 1638 | case 23: 1639 | return "Authorization denied" 1640 | case 24: 1641 | return "Auxiliary database format error" 1642 | case 25: 1643 | return "2nd parameter to sqlite3_bind out of range" 1644 | case 26: 1645 | return "File opened that is not a database file" 1646 | case 27: 1647 | return "Notifications from sqlite3_log()" 1648 | case 28: 1649 | return "Warnings from sqlite3_log()" 1650 | case 100: 1651 | return "sqlite3_step() has another row ready" 1652 | case 101: 1653 | return "sqlite3_step() has finished executing" 1654 | 1655 | //custom SwiftData errors 1656 | 1657 | //->binding errors 1658 | 1659 | case 201: 1660 | return "Not enough objects to bind provided" 1661 | case 202: 1662 | return "Too many objects to bind provided" 1663 | case 203: 1664 | return "Object to bind as identifier must be a String" 1665 | 1666 | //->custom connection errors 1667 | 1668 | case 301: 1669 | return "A custom connection is already open" 1670 | case 302: 1671 | return "Cannot open a custom connection inside a transaction" 1672 | case 303: 1673 | return "Cannot open a custom connection inside a savepoint" 1674 | case 304: 1675 | return "A custom connection is not currently open" 1676 | case 305: 1677 | return "Cannot close a custom connection inside a transaction" 1678 | case 306: 1679 | return "Cannot close a custom connection inside a savepoint" 1680 | 1681 | //->index and table errors 1682 | 1683 | case 401: 1684 | return "At least one column name must be provided" 1685 | case 402: 1686 | return "Error extracting index names from sqlite_master" 1687 | case 403: 1688 | return "Error extracting table names from sqlite_master" 1689 | 1690 | //->transaction and savepoint errors 1691 | 1692 | case 501: 1693 | return "Cannot begin a transaction within a savepoint" 1694 | case 502: 1695 | return "Cannot begin a transaction within another transaction" 1696 | 1697 | //unknown error 1698 | 1699 | default: 1700 | //what the fuck happened?!? 1701 | return "Unknown error" 1702 | } 1703 | 1704 | } 1705 | 1706 | } 1707 | 1708 | public typealias SD = SwiftData --------------------------------------------------------------------------------