- Installation
- Getting Started
- Building Type-Safe SQL
- Creating a Table
- Inserting Rows
- Selecting Rows
- Updating Rows
- Deleting Rows
- Transactions and Savepoints
- Altering the Schema
- Custom Types
- Other Operators
- Core SQLite Functions
- Aggregate SQLite Functions
- Custom SQL Functions
- Custom Collations
- Full-text Search
- Executing Arbitrary SQL
- Logging
Note: SQLite.swift requires Swift 1.1 (and Xcode 6.1) or greater.
To install SQLite.swift as an Xcode sub-project:
-
Drag the SQLite.xcodeproj file into your own project. (Submodule, clone, or download the project first.)
-
In your target’s Build Phases, add SQLite to the Target Dependencies build phase.
-
Add SQLite.framework to the Link Binary With Libraries build phase.
-
Add SQLite.framework to a Copy Files build phase with a Frameworks destination. (Add a new build phase if need be.)
You should now be able to import SQLite from any of your target’s source files and begin using SQLite.swift.
To install SQLite.swift with SQLCipher support:
-
Make sure the sqlcipher working copy is checked out in Xcode. If sqlcipher.xcodeproj is unavailable (i.e., it appears red), go to the Source Control menu and select Check Out sqlcipher… from the sqlcipher menu item.
-
Follow the instructions above with the SQLiteCipher target, instead.
Note: By default, SQLCipher compiles without support for full-text search. If you intend to use FTS4, make sure you add the following to Other C Flags in the Build Settings of the sqlcipher target (in the sqlcipher.xcodeproj project):
-DSQLITE_ENABLE_FTS4-DSQLITE_ENABLE_FTS3_PARENTHESIS
It’s possible to use SQLite.swift in a target that doesn’t support frameworks, including iOS 7 apps and OS X command line tools, though it takes a little extra work.
-
In your target’s Build Phases, add libsqlite3.dylib to the Link Binary With Libraries build phase.
-
Copy the SQLite.swift source files (from its SQLite directory) into your Xcode project.
-
Remove
import sqlite3(and@import sqlite3;) from the SQLite.swift source files that call it. -
Add the following lines to your project’s bridging header (a file usually in the form of
$(TARGET_NAME)-Bridging-Header.h.#import <sqlite3.h> #import "SQLite-Bridging.h"
Note: Adding SQLite.swift source files directly to your application will both remove the
SQLitemodule namespace and expose internal functions and variables. Please report any namespace collisions and bugs you encounter.
To use SQLite.swift classes or structures in your target’s source file, first import the SQLite module.
import SQLiteDatabase connections are established using the Database class. A database is initialized with a path. SQLite will attempt to create the database file if it does not already exist.
let db = Database("path/to/db.sqlite3")On iOS, you can create a writable database in your app’s Documents directory.
let path = NSSearchPathForDirectoriesInDomains(
.DocumentDirectory, .UserDomainMask, true
).first as! String
let db = Database("\(path)/db.sqlite3")On OS X, you can use your app’s Application Support directory:
var path = NSSearchPathForDirectoriesInDomains(
.ApplicationSupportDirectory, .UserDomainMask, true
).first as! String + NSBundle.mainBundle().bundleIdentifier!
// create parent directory iff it doesn’t exist
NSFileManager.defaultManager().createDirectoryAtPath(
path, withIntermediateDirectories: true, attributes: nil, error: nil
)
let db = Database("\(path)/db.sqlite3")If you bundle a database with your app (i.e., you’ve copied a database file into your Xcode project and added it to your application target), you can establish a read-only connection to it.
let path = NSBundle.mainBundle().pathForResource("db", ofType: "sqlite3")!
let db = Database(path, readonly: true)Note: Signed applications cannot modify their bundle resources. If you bundle a database file with your app for the purpose of bootstrapping, copy it to a writable location before establishing a connection (see Read-Write Databases, above, for typical, writable locations).
If you omit the path, SQLite.swift will provision an in-memory database.
let db = Database() // equivalent to `Database(":memory:")`To create a temporary, disk-backed database, pass an empty file name.
let db = Database("")In-memory databases are automatically deleted when the database connection is closed.
Note: Every database comes equipped with its own serial queue for statement execution and can be safely accessed across threads. Threads that open transactions and savepoints, however, do not block other threads from executing statements within the transaction.
SQLite.swift comes with a typed expression layer that directly maps Swift types to their SQLite counterparts.
| Swift Type | SQLite Type |
|---|---|
Int64* |
INTEGER |
Double |
REAL |
String |
TEXT |
nil |
NULL |
SQLite.Blob† |
BLOB |
*While
Int64is the basic, raw type (to preserve 64-bit integers on 32-bit platforms),IntandBoolwork transparently.†SQLite.swift defines its own
Blobstructure, which safely wraps the underlying bytes.See Custom Types for more information about extending other classes and structures to work with SQLite.swift.
See Executing Arbitrary SQL to forego the typed layer and execute raw SQL, instead.
These expressions (in the form of the structure, Expression) build on one another and, with a query (Query), can create and execute SQL statements.
Expressions are generic structures associated with a type (built-in or custom), raw SQL, and (optionally) values to bind to that SQL. Typically, you will only explicitly create expressions to describe your columns, and typically only once per column.
let id = Expression<Int64>("id")
let email = Expression<String>("email")
let balance = Expression<Double>("balance")
let verified = Expression<Bool>("verified")Use optional generics for expressions that can evaluate to NULL.
let name = Expression<String?>("name")Note: The default
Expressioninitializer is for quoted identifiers (i.e., column names). To build a literal SQL expression, useinit(literal:).
Expressions can be combined with other expressions and types using filter operators and functions (as well as other non-filter operators and functions). These building blocks can create complex SQLite statements.
Queries are structures that reference a database and table name, and can be used to build a variety of statements using expressions. We can create a Query by subscripting a database connection with a table name.
let users = db["users"]Assuming the table exists, we can immediately insert, select, update, and delete rows.
We can run CREATE TABLE statements by calling the create(table:) function on a database connection. The following is a basic example of SQLite.swift code (using the expressions and query above) and the corresponding SQL it generates.
db.create(table: users) { t in // CREATE TABLE "users" (
t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL,
t.column(email, unique: true) // "email" TEXT UNIQUE NOT NULL,
t.column(name) // "name" TEXT
} // )Note:
Expression<T>structures (in this case, theidandNOT NULLconstraints automatically, whileExpression<T?>structures (name) do not.
The create(table:) function has several default parameters we can override.
-
temporaryadds aTEMPORARYclause to theCREATE TABLEstatement (to create a temporary table that will automatically drop when the database connection closes). Default:false.db.create(table: users, temporary: true) { t in /* ... */ } // CREATE TEMPORARY TABLE "users" -- ...
-
ifNotExistsadds anIF NOT EXISTSclause to theCREATE TABLEstatement (which will bail out gracefully if the table already exists). Default:false.db.create(table: users, ifNotExists: true) { t in /* ... */ } // CREATE TABLE "users" IF NOT EXISTS -- ...
The column function is used for a single column definition. It takes an expression describing the column name and type, and accepts several parameters that map to various column constraints and clauses.
-
primaryKeyadds aPRIMARY KEYconstraint to a single column.t.column(id, primaryKey: true) // "id" INTEGER PRIMARY KEY NOT NULL t.column(id, primaryKey: .Autoincrement) // "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL
Note: The
primaryKeyparameter cannot be used alongsidereferences. If you need to create a column that has a default value and is also a primary and/or foreign key, use theprimaryKeyandforeignKeyfunctions mentioned under Table Constraints.Primary keys cannot be optional (e.g.,
Expression<Int64?>).Only an
INTEGER PRIMARY KEYcan take.Autoincrement. -
uniqueadds aUNIQUEconstraint to the column. (See theuniquefunction under Table Constraints for uniqueness over multiple columns).t.column(email, unique: true) // "email" TEXT UNIQUE NOT NULL
-
checkattaches aCHECKconstraint to a column definition in the form of a boolean expression (Expression<Bool>). Boolean expressions can be easily built using filter operators and functions. (See also thecheckfunction under Table Constraints.)t.column(email, check: like("%@%", email)) // "email" TEXT NOT NULL CHECK ("email" LIKE '%@%')
-
defaultValueadds aDEFAULTclause to a column definition and only accepts a value (or expression) matching the column’s type. This value is used if none is explicitly provided during anINSERT.t.column(name, defaultValue: "Anonymous") // "name" TEXT DEFAULT 'Anonymous'
Note: The
defaultValueparameter cannot be used alongsideprimaryKeyandreferences. If you need to create a column that has a default value and is also a primary and/or foreign key, use theprimaryKeyandforeignKeyfunctions mentioned under Table Constraints. -
collateadds aCOLLATEclause toExpression<String>(andExpression<String?>) column definitions with a collating sequence defined in theCollationenumeration.t.column(email, collate: .Nocase) // "email" TEXT NOT NULL COLLATE "NOCASE" t.column(name, collate: .Rtrim) // "name" TEXT COLLATE "RTRIM"
-
referencesadds aREFERENCESclause toExpression<Int64>(andExpression<Int64?>) column definitions and accepts a table (Query) or namespaced column expression. (See theforeignKeyfunction under Table Constraints for non-integer foreign key support.)t.column(user_id, references: users[id]) // "user_id" INTEGER REFERENCES "users"("id") t.column(user_id, references: users) // "user_id" INTEGER REFERENCES "users" // -- assumes "users" has a PRIMARY KEY
Note: The
referencesparameter cannot be used alongsideprimaryKeyanddefaultValue. If you need to create a column that has a default value and is also a primary and/or foreign key, use theprimaryKeyandforeignKeyfunctions mentioned under Table Constraints.
Additional constraints may be provided outside the scope of a single column using the following functions.
-
primaryKeyadds aPRIMARY KEYconstraint to the table. Unlike the column constraint, above, it supports all SQLite types, ascending and descending orders, and composite (multiple column) keys.t.primaryKey(email.asc, name) // PRIMARY KEY("email" ASC, "name")
-
uniqueadds aUNIQUEconstraint to the table. Unlike the column constraint, above, it supports composite (multiple column) constraints.t.unique(local, domain) // UNIQUE("local", "domain")
-
checkadds aCHECKconstraint to the table in the form of a boolean expression (Expression<Bool>). Boolean expressions can be easily built using filter operators and functions. (See also thecheckparameter under Column Constraints.)t.check(balance >= 0) // CHECK ("balance" >= 0.0)
-
foreignKeyadds aFOREIGN KEYconstraint to the table. Unlike thereferencesconstraint, above, it supports all SQLite types, and bothON UPDATEandON DELETEactions, and composite (multiple column) keys.t.foreignKey(user_id, on: users[id], delete: .SetNull) // FOREIGN KEY("user_id") REFERENCES "users"("id") ON DELETE SET NULL
We can insert rows into a table by calling a query’s insert function with a list of setters, typically typed column expressions and values (which can also be expressions), each joined by the <- operator.
users.insert(email <- "alice@mac.com", name <- "Alice")?
// INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice')The insert function can return several different types that are useful in different contexts.
-
An
Int64?representing the inserted row’sROWID(ornilon failure), for simplicity.if let insertId = users.insert(email <- "alice@mac.com") { println("inserted id: \(insertId)") }
If a value is always expected, we can disambiguate with a
!.users.insert(email <- "alice@mac.com")!
-
A
Statement, for the transaction and savepoint helpers that take a list of statements.db.transaction() && users.insert(email <- "alice@mac.com") && users.insert(email <- "betty@mac.com") && db.commit() || db.rollback() // BEGIN DEFERRED TRANSACTION; // INSERT INTO "users" ("email") VALUES ('alice@mac.com'); // INSERT INTO "users" ("email") VALUES ('betty@mac.com'); // COMMIT TRANSACTION;
-
A tuple of the above
ROWIDand statement:(rowid: Int64?, statement: Statement), for flexibility.let (rowid, statement) = users.insert(email <- "alice@mac.com") if let rowid = rowid { println("inserted id: \(rowid)") } else if statement.failed { println("insertion failed: \(statement.reason)") }
The update and delete functions follow similar patterns.
Note: If
insertis called without any arguments, the statement will run with aDEFAULT VALUESclause. The table must not have any constraints that aren’t fulfilled by default values.timestamps.insert()! // INSERT INTO "timestamps" DEFAULT VALUES
SQLite.swift typically uses the <- operator to set values during inserts and updates.
views.update(count <- 0)
// UPDATE "views" SET "count" = 0 WHERE ("id" = 1)There are also a number of convenience setters that take the existing value into account using native Swift operators.
For example, to atomically increment a column, we can use ++:
views.update(count++) // equivalent to `views.update(count -> count + 1)`
// UPDATE "views" SET "count" = "count" + 1 WHERE ("id" = 1)To take an amount and “move” it via transaction, we can use -= and +=:
let amount = 100.0
db.transaction()
&& alice.update(balance -= amount)
&& betty.update(balance += amount)
&& db.commit() || db.rollback()
// BEGIN DEFERRED TRANSACTION;
// UPDATE "users" SET "balance" = "balance" - 100.0 WHERE ("id" = 1);
// UPDATE "users" SET "balance" = "balance" + 100.0 WHERE ("id" = 2);
// COMMIT TRANSACTION;| Operator | Types |
|---|---|
<- |
Value -> Value |
+= |
Number -> Number |
-= |
Number -> Number |
*= |
Number -> Number |
/= |
Number -> Number |
%= |
Int -> Int |
<<= |
Int -> Int |
>>= |
Int -> Int |
&= |
Int -> Int |
| ` | |
^= |
Int -> Int |
+= |
String -> String |
| Operator | Types |
|---|---|
++ |
Int -> Int |
-- |
Int -> Int |
Query structures are SELECT statements waiting to happen. They execute via iteration and other means of sequence access.
Queries execute lazily upon iteration. Each row is returned as a Row object, which can be subscripted with a column expression matching one of the columns returned.
for user in users {
println("id: \(user[id]), email: \(user[email]), name: \(user[name])")
// id: 1, email: alice@mac.com, name: Optional("Alice")
}
// SELECT * FROM "users"Expression<T> column values are automatically unwrapped (we’ve made a promise to the compiler that they’ll never be NULL), while Expression<T?> values remain wrapped.
We can pluck the first row by calling the first computed property on Query.
if let user = users.first { /* ... */ } // Row
// SELECT * FROM "users" LIMIT 1To collect all rows into an array, we can simply wrap the sequence (though this is not always the most memory-efficient idea).
let all = Array(users)
// SELECT * FROM "users"Query structures have a number of chainable functions that can be used (with expressions) to add and modify a number of clauses to the underlying statement.
let query = users.select(email) // SELECT "email" FROM "users"
.filter(name != nil) // WHERE "name" IS NOT NULL
.order(email.desc, name) // ORDER BY "email" DESC, "name"
.limit(5, offset: 1) // LIMIT 5 OFFSET 1By default, Query objects select every column of the result set (using SELECT *). We can use the select function with a list of expressions to return specific columns instead.
let query = users.select(id, email)
// SELECT "id", "email" FROM "users"We can join tables using a query’s join function.
users.join(posts, on: user_id == users[id])
// SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id")The join function takes a query object (for the table being joined on), a join condition (on), and is prefixed with an optional join type (default: .Inner). Join conditions can be built using filter operators and functions, generally require namespacing, and sometimes require aliasing.
When joining tables, column names can become ambiguous. E.g., both tables may have an id column.
let query = users.join(posts, on: user_id == id)
// assertion failure: ambiguous column 'id'We can disambiguate by namespacing id.
let query = users.join(posts, on: user_id == users[id])
// SELECT * FROM "users" INNER JOIN "posts" ON ("user_id" = "users"."id")Namespacing is achieved by subscripting a query with a column expression (e.g., users[id] above becomes users.id).
Note: We can namespace all of a table’s columns using
*.let query = users.select(users[*]) // SELECT "users".* FROM "users"
Occasionally, we need to join a table to itself, in which case we must alias the table with another name. We can achieve this using the query’s alias function.
let managers = users.alias("managers")
let query = users.join(managers, on: managers[id] == users[manager_id])
// SELECT * FROM "users"
// INNER JOIN ("users") AS "managers" ON ("managers"."id" = "users"."manager_id")If query results can have ambiguous column names, row values should be accessed with namespaced column expressions. In the above case, SELECT * immediately namespaces all columns of the result set.
let user = query.first!
user[id] // fatal error: ambiguous column 'id'
// (please disambiguate: ["users"."id", "managers"."id"])
user[users[id]] // returns "users"."id"
user[managers[id]] // returns "managers"."id"SQLite.swift filters rows using a query’s filter function with a boolean expression (Expression<Bool>).
users.filter(id == 1)
// SELECT * FROM "users" WHERE ("id" = 1)
users.filter(contains([1, 2, 3, 4, 5], id))
// SELECT * FROM "users" WHERE ("id" IN (1, 2, 3, 4, 5))
users.filter(like("%@mac.com", email))
// SELECT * FROM "users" WHERE ("email" LIKE '%@mac.com')
users.filter(verified && lower(name) == "alice")
// SELECT * FROM "users" WHERE ("verified" AND (lower("name") == 'alice'))
users.filter(verified || balance >= 10_000)
// SELECT * FROM "users" WHERE ("verified" OR ("balance" >= 10000.0))We can build our own boolean expressions by using one of the many filter operators and functions.
Note: SQLite.swift defines
filterinstead ofwherebecausewhereis a reserved keyword.
SQLite.swift defines a number of operators for building filtering predicates. Operators and functions work together in a type-safe manner, so attempting to equate or compare different types will prevent compilation.
| Swift | Types | SQLite |
|---|---|---|
== |
Equatable -> Bool |
=/IS* |
!= |
Equatable -> Bool |
!=/IS NOT* |
> |
Comparable -> Bool |
> |
>= |
Comparable -> Bool |
>= |
< |
Comparable -> Bool |
< |
<= |
Comparable -> Bool |
<= |
~= |
(Interval, Comparable) -> Bool |
BETWEEN |
&& |
Bool -> Bool |
AND |
| ` | ` |
*When comparing against
nil, SQLite.swift will useISandIS NOTaccordingly.
| Swift | Types | SQLite |
|---|---|---|
! |
Bool -> Bool |
NOT |
| Swift | Types | SQLite |
|---|---|---|
like |
String -> Bool |
LIKE |
glob |
String -> Bool |
GLOB |
match |
String -> Bool |
MATCH |
contains |
(Array<T>, T) -> Bool |
IN |
We can pre-sort returned rows using the query’s order function.
E.g., to return users sorted by email, then name, in ascending order:
users.order(email, name)
// SELECT * FROM "users" ORDER BY "email", "name"The order function takes a list of column expressions.
Expression objects have two computed properties to assist sorting: asc and desc. These properties append the expression with ASC and DESC to mark ascending and descending order respectively.
users.order(email.desc, name.asc)
// SELECT * FROM "users" ORDER BY "email" DESC, "name" ASCWe can limit and skip returned rows using a query’s limit function (and its optional offset parameter).
users.limit(5)
// SELECT * FROM "users" LIMIT 5
users.limit(5, offset: 5)
// SELECT * FROM "users" LIMIT 5 OFFSET 5Query structures come with a number of functions that quickly return aggregate values from the table. These mirror the core aggregate functions and are executed immediately against the query.
users.count
// SELECT count(*) FROM "users"Filtered queries will appropriately filter aggregate values.
users.filter(name != nil).count
// SELECT count(*) FROM "users" WHERE "name" IS NOT NULL-
countas a computed property (see examples above) returns the total number of rows matching the query.countas a function takes a column name and returns the total number of rows where that column is notNULL.users.count(name) // -> Int // SELECT count("name") FROM "users"
-
maxtakes a comparable column expression and returns the largest value if any exists.users.max(id) // -> Int64? // SELECT max("id") FROM "users"
-
mintakes a comparable column expression and returns the smallest value if any exists.users.min(id) // -> Int64? // SELECT min("id") FROM "users"
-
averagetakes a numeric column expression and returns the average row value (as aDouble) if any exists.users.average(balance) // -> Double? // SELECT avg("balance") FROM "users"
-
sumtakes a numeric column expression and returns the sum total of all rows if any exist.users.sum(balance) // -> Double? // SELECT sum("balance") FROM "users"
-
total, likesum, takes a numeric column expression and returns the sum total of all rows, but in this case always returns aDouble, and returns0.0for an empty query.users.total(balance) // -> Double // SELECT total("balance") FROM "users"
Note: Most of the above aggregate functions (except
maxandmin) can be called with adistinctparameter to aggregateDISTINCTvalues only.users.count(distinct: name) // SELECT count(DISTINCT "name") FROM "users"
We can update a table’s rows by calling a query’s update function with a list of setters, typically typed column expressions and values (which can also be expressions), each joined by the <- operator.
When an unscoped query calls update, it will update every row in the table.
users.update(email <- "alice@me.com")?
// UPDATE "users" SET "email" = 'alice@me.com'Be sure to scope UPDATE statements beforehand using the filter function.
let alice = users.filter(id == 1)
alice.update(email <- "alice@me.com")?
// UPDATE "users" SET "email" = 'alice@me.com' WHERE ("id" = 1)Like insert (and delete), update can return several different types that are useful in different contexts.
-
An
Int?representing the number of updated rows (ornilon failure), for simplicity.if alice.update(email <- "alice@me.com") > 0 { println("updated Alice") }
If a value is always expected, we can disambiguate with a
!.alice.update(email <- "alice@me.com")!
-
A
Statement, for the transaction and savepoint helpers that take a list of statements. -
A tuple of the above number of updated rows and statement:
(changes: Int?, Statement), for flexibility.
We can delete rows from a table by calling a query’s delete function.
When an unscoped query calls delete, it will delete every row in the table.
users.delete()?
// DELETE FROM "users"Be sure to scope DELETE statements beforehand using the filter function.
let alice = users.filter(id == 1)
alice.delete()?
// DELETE FROM "users" WHERE ("id" = 1)Like insert and update, delete can return several different types that are useful in different contexts.
-
An
Int?representing the number of deleted rows (ornilon failure), for simplicity.if alice.delete() > 0 { println("deleted Alice") }
If a value is always expected, we can disambiguate with a
!.alice.delete()!
-
A
Statement, for the transaction and savepoint helpers that take a list of statements. -
A tuple of the above number of deleted rows and statement:
(changes: Int?, Statement), for flexibility.
Using the transaction and savepoint functions, we can run a series of statements, committing the changes to the database if they all succeed. If a single statement fails, we can bail out early and roll back.
db.transaction()
&& users.insert(email <- "betty@icloud.com")
&& users.insert(email <- "cathy@icloud.com", manager_id <- db.lastInsertRowid)
&& db.commit() || db.rollback()Note: Each statement is captured in an auto-closure and won’t execute till the preceding statement succeeds. This means we can use the
lastInsertRowidproperty onDatabaseto reference the previous statement’s insertROWID.
SQLite.swift comes with several functions (in addition to create(table:)) for altering a database schema in a type-safe manner.
We can rename a table by calling the rename(table:to:) function on a database connection.
db.rename(users, to: "users_old")
// ALTER TABLE "users" RENAME TO "users_old"We can add columns to a table by calling alter function on a database connection. SQLite.swift enforces the same limited subset of ALTER TABLE that SQLite supports.
db.alter(table: users, add: suffix)
// ALTER TABLE "users" ADD COLUMN "suffix" TEXTThe alter function shares several of the same column function parameters used when creating tables.
-
checkattaches aCHECKconstraint to a column definition in the form of a boolean expression (Expression<Bool>). (See also thecheckfunction under Table Constraints.)let check = contains(["JR", "SR"], suffix) db.alter(table: users, add: suffix, check: check) // ALTER TABLE "users" // ADD COLUMN "suffix" TEXT CHECK ("suffix" IN ('JR', 'SR'))
-
defaultValueadds aDEFAULTclause to a column definition and only accepts a value matching the column’s type. This value is used if none is explicitly provided during anINSERT.db.alter(table: users, add: suffix, defaultValue: "SR") // ALTER TABLE "users" ADD COLUMN "suffix" TEXT DEFAULT 'SR'
Note: Unlike the
CREATE TABLEconstraint, default values may not be expression structures (includingCURRENT_TIME,CURRENT_DATE, orCURRENT_TIMESTAMP). -
collateadds aCOLLATEclause toExpression<String>(andExpression<String?>) column definitions with a collating sequence defined in theCollationenumeration.t.column(email, collate: .Nocase) // email TEXT NOT NULL COLLATE "NOCASE" t.column(name, collate: .Rtrim) // name TEXT COLLATE "RTRIM"
-
referencesadds aREFERENCESclause toInt64(andInt64?) column definitions and accepts a table or namespaced column expression. (See theforeignKeyfunction under Table Constraints for non-integer foreign key support.)db.alter(table: posts, add: user_id, references: users[id]) // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users"("id") db.alter(table: posts, add: user_id, references: users) // ALTER TABLE "posts" ADD COLUMN "user_id" INTEGER REFERENCES "users" // -- assumes "users" has a PRIMARY KEY
We can run CREATE INDEX statements by calling the create(index:) function on a database connection.
db.create(index: users, on: email)
// CREATE INDEX "index_users_on_email" ON "users" ("email")The index name is generated automatically based on the table and column names.
The create(index:) function has a couple default parameters we can override.
-
uniqueadds aUNIQUEconstraint to the index. Default:false.db.create(index: users, on: email, unique: true) // CREATE UNIQUE INDEX "index_users_on_email" ON "users" ("email")
-
ifNotExistsadds anIF NOT EXISTSclause to theCREATE TABLEstatement (which will bail out gracefully if the table already exists). Default:false.db.create(index: users, on: email, ifNotExists: true) // CREATE INDEX IF NOT EXISTS "index_users_on_email" ON "users" ("email")
We can run DROP INDEX statements by calling the drop(index:) function on a database connection.
db.drop(index: users, on: email)
// DROP INDEX "index_users_on_email"The drop(index:) function has one additional parameter, ifExists, which (when true) adds an IF EXISTS clause to the statement.
db.drop(index: users, on: email, ifExists: true)
// DROP INDEX IF EXISTS "index_users_on_email"We can run DROP TABLE statements by calling the drop(table:) function on a database connection.
db.drop(table: users)
// DROP TABLE "users"The drop(table:) function has one additional parameter, ifExists, which (when true) adds an IF EXISTS clause to the statement.
db.drop(table: users, ifExists: true)
// DROP TABLE IF EXISTS "users"SQLite.swift provides a convenience property on Database to query and set the PRAGMA user_version. This is a great way to manage your schema’s version over migrations.
if db.userVersion == 0 {
// handle first migration
db.userVersion = 1
}
if db.userVersion == 1 {
// handle second migration
db.userVersion = 2
}SQLite.swift supports serializing and deserializing any custom type as long as it conforms to the Value protocol.
protocol Value { typealias Datatype: Binding class var declaredDatatype: String { get } class func fromDatatypeValue(datatypeValue: Datatype) -> Self var datatypeValue: Datatype { get } }
The Datatype must be one of the basic Swift types that values are bridged through before serialization and deserialization (see Building Type-Safe SQL for a list of types).
Note:
Bindingis a protocol that SQLite.swift uses internally to directly map SQLite types to Swift types. Do not conform custom types to theBindingprotocol.
Once extended, the type can be used almost wherever typed expressions can be.
In SQLite, DATETIME columns can be treated as strings or numbers, so we can transparently bridge NSDate objects through Swift’s String or Int types.
To serialize NSDate objects as TEXT values (in ISO 8601), we’ll use String.
extension NSDate: Value {
class var declaredDatatype: String {
return String.declaredDatatype
}
class func fromDatatypeValue(stringValue: String) -> NSDate {
return SQLDateFormatter.dateFromString(stringValue)!
}
var datatypeValue: String {
return SQLDateFormatter.stringFromDate(self)
}
}
let SQLDateFormatter: NSDateFormatter = {
let formatter = NSDateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
formatter.locale = NSLocale(localeIdentifier: "en_US_POSIX")
formatter.timeZone = NSTimeZone(forSecondsFromGMT: 0)
return formatter
}()We can also treat them as INTEGER values using Int.
extension NSDate: Value {
class var declaredDatatype: String {
return Int.declaredDatatype
}
class func fromDatatypeValue(intValue: Int) -> Self {
return self(timeIntervalSince1970: NSTimeInterval(intValue))
}
var datatypeValue: Int {
return Int(timeIntervalSince1970)
}
}Note: SQLite’s
CURRENT_DATE,CURRENT_TIME, andCURRENT_TIMESTAMPhelpers returnTEXTvalues. Because of this (and the fact that Unix time is far less human-readable when we’re faced with the raw data), we recommend using theTEXTextension.
Once defined, we can use these types directly in SQLite statements.
let published_at = Expression<NSDate>("published_at")
let published = posts.filter(published_at <= NSDate())
// extension where Datatype == String:
// SELECT * FROM "posts" WHERE "published_at" <= '2014-11-18 12:45:30'
// extension where Datatype == Int:
// SELECT * FROM "posts" WHERE "published_at" <= 1416314730Any object that can be encoded and decoded can be stored as a blob of data in SQL.
We can create an NSData bridge rather trivially.
extension NSData: Value {
class var declaredDatatype: String {
return Blob.declaredDatatype
}
class func fromDatatypeValue(blobValue: Blob) -> Self {
return self(bytes: blobValue.bytes, length: blobValue.length)
}
var datatypeValue: Blob {
return Blob(bytes: bytes, length: length)
}
}We can bridge any type that can be initialized from and encoded to NSData.
// assumes NSData conformance, above
extension UIImage: Value {
class var declaredDatatype: String {
return NSData.declaredDatatype
}
class func fromDatatypeValue(blobValue: Blob) -> Self {
return self(data: NSData.fromDatatypeValue(blobValue))
}
var datatypeValue: Blob {
return UIImagePNGRepresentation(self).datatypeValue
}
}Note: See the Archives and Serializations Programming Guide for more information on encoding and decoding custom types.
Swift does not currently support generic subscripting, which means we cannot, by default, subscript Expressions with custom types to:
-
Namespace expressions. Use the
namespacefunction, instead:let avatar = Expression<UIImage?>("avatar") users[avatar] // fails to compile users.namespace(avatar) // "users"."avatar"
-
Access column data. Use the
getfunction, instead:let user = users.first! user[avatar] // fails to compile user.get(avatar) // UIImage?
We can, of course, write extensions, but they’re rather wordy.
extension Query {
subscript(column: Expression<UIImage>) -> Expression<UIImage> {
return namespace(column)
}
subscript(column: Expression<UIImage?>) -> Expression<UIImage?> {
return namespace(column)
}
}
extension Row {
subscript(column: Expression<UIImage>) -> UIImage {
return get(column)
}
subscript(column: Expression<UIImage?>) -> UIImage? {
return get(column)
}
}In addition to filter operators, SQLite.swift defines a number of operators that can modify expression values with arithmetic, bitwise operations, and concatenation.
| Swift | Types | SQLite |
|---|---|---|
+ |
Number -> Number |
+ |
- |
Number -> Number |
- |
* |
Number -> Number |
* |
/ |
Number -> Number |
/ |
% |
Int -> Int |
% |
<< |
Int -> Int |
<< |
>> |
Int -> Int |
>> |
& |
Int -> Int |
& |
| ` | ` | Int -> Int |
+ |
String -> String |
` |
Note: SQLite.swift also defines a bitwise XOR operator,
^, which expands the expressionlhs ^ rhsto~(lhs & rhs) & (lhs | rhs).
| Swift | Types | SQLite |
|---|---|---|
~ |
Int -> Int |
~ |
- |
Number -> Number |
- |
Many of SQLite’s core functions have been surfaced in and type-audited for SQLite.swift.
Note: SQLite.swift aliases the
??operator to theifnullfunction.name ?? email // ifnull("name", "email")
Most of SQLite’s aggregate functions have been surfaced in and type-audited for SQLite.swift.
We can create custom SQL functions by calling create(function:) on a database connection.
For example, to give queries access to MobileCoreServices.UTTypeConformsTo, we can write the following:
import MobileCoreServices
let typeConformsTo: (String, Expression<String>) -> Expression<Bool> = (
db.create(function: "typeConformsTo", deterministic: true) { UTI, conformsToUTI in
return UTTypeConformsTo(UTI, conformsToUTI) != 0
}
)Note: The optional
deterministicparameter is an optimization that causes the function to be created withSQLITE_DETERMINISTIC.
Note typeConformsTo’s signature:
(Expression<String>, String) -> Expression<Bool>Because of this, create(function:) expects a block with the following signature:
(String, String) -> BoolOnce assigned, the closure can be called wherever boolean expressions are accepted.
let attachments = db["attachments"]
let UTI = Expression<String>("UTI")
attachments.filter(typeConformsTo(UTI, kUTTypeImage))
// SELECT * FROM "attachments" WHERE "typeConformsTo"("UTI", 'public.image')Note: The return type of a function must be a core SQL type or conform to
Value.
We can create loosely-typed functions by handling an array of raw arguments, instead.
db.create(function: "typeConformsTo", deterministic: true) { args in
if let UTI = args[0] as? String, conformsToUTI = args[1] as? String {
return Int(UTTypeConformsTo(UTI, conformsToUTI))
}
return nil
}Creating a loosely-typed function cannot return a closure and instead must be wrapped manually or executed using raw SQL.
let stmt = db.prepare("SELECT * FROM attachments WHERE typeConformsTo(UTI, ?)")
for row in stmt.bind(kUTTypeImage) { /* ... */ }We can create custom collating sequences by calling create(collation:) on a database connection.
db.create(collation: "NODIACRITIC") { lhs, rhs in
return lhs.compare(rhs, options: .DiacriticInsensitiveSearch)
}We can reference a custom collation using the Custom member of the Collation enumeration.
restaurants.order(collate(.Custom("NODIACRITIC"), name))
// SELECT * FROM "restaurants" ORDER BY "name" COLLATE "NODIACRITIC"We can create a virtual table using the FTS4 module by calling create(vtable:) on a database connection.
let emails = db["emails"]
let subject = Expression<String>("subject")
let body = Expression<String>("body")
db.create(vtable: emails, using: fts4(subject, body))
// CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body")We can specify a tokenizer using the tokenize parameter.
db.create(vtable: emails, using: fts4([subject, body], tokenize: .Porter))
// CREATE VIRTUAL TABLE "emails" USING fts4("subject", "body", tokenize=porter)Once we insert a few rows, we can search using the match function, which takes a table or column as its first argument and a query string as its second.
emails.insert(
subject <- "Just Checking In",
body <- "Hey, I was just wondering...did you get my last email?"
)!
emails.filter(match(emails, "wonder*"))
// SELECT * FROM "emails" WHERE "emails" MATCH 'wonder*'
emails.filter(match(subject, "Re:*"))
// SELECT * FROM "emails" WHERE "subject" MATCH 'Re:*'Though we recommend you stick with SQLite.swift’s type-safe system whenever possible, it is possible to simply and safely prepare and execute raw SQL statements via a Database connection using the following functions.
-
executeruns an arbitrary number of SQL statements as a convenience.db.execute( "BEGIN TRANSACTION;" + "CREATE TABLE users (" + "id INTEGER PRIMARY KEY NOT NULL," + "email TEXT UNIQUE NOT NULL," + "name TEXT" + ");" + "CREATE TABLE posts (" + "id INTEGER PRIMARY KEY NOT NULL," + "title TEXT NOT NULL," + "body TEXT NOT NULL," + "published_at DATETIME" + ");" + "PRAGMA user_version = 1;" + "COMMIT TRANSACTION;" )
-
prepareprepares a singleStatementobject from a SQL string, optionally binds values to it (using the statement’sbindfunction), and returns the statement for deferred execution.let stmt = db.prepare("INSERT INTO users (email) VALUES (?)")
Once prepared, statements may be executed using
run, binding any unbound parameters.stmt.run("alice@mac.com") db.changes // -> {Some 1}
Statements with results may be iterated over.
let stmt = db.prepare("SELECT id, email FROM users") for row in stmt { println("id: \(row[0]), email: \(row[1])") // id: Optional(1), email: Optional("alice@mac.com") }
-
runprepares a singleStatementobject from a SQL string, optionally binds values to it (using the statement’sbindfunction), executes, and returns the statement.db.run("INSERT INTO users (email) VALUES (?)", "alice@mac.com")
-
scalarprepares a singleStatementobject from a SQL string, optionally binds values to it (using the statement’sbindfunction), executes, and returns the first value of the first row.db.scalar("SELECT count(*) FROM users") as! Int64
Statements also have a
scalarfunction, which can optionally re-bind values at execution.let stmt = db.prepare("SELECT count (*) FROM users") stmt.scalar() as! Int64
We can log SQL using the database’s trace function.
#if DEBUG
db.trace(println)
#endif