Tables & CRUD Operations
This guide covers the fundamental Create, Read, Update, and Delete (CRUD) operations in SQLite.swift using the type-safe expression builder.
Creating Tables
You can create CREATE TABLE statements by calling the create method on a Table object. The builder closure allows you to define columns and constraints.
let users = Table("users")
let id = Expression<Int64>("id")
let email = Expression<String>("email")
let name = Expression<String?>("name")
try db.run(users.create { t in
t.column(id, primaryKey: true)
t.column(email, unique: true)
t.column(name)
})
// SQL: CREATE TABLE "users" (
// "id" INTEGER PRIMARY KEY NOT NULL,
// "email" TEXT UNIQUE NOT NULL,
// "name" TEXT
// )
Expression<T>columns areNOT NULLby default.Expression<T?>columns are nullable by default.
Column Constraints
The column method accepts several parameters to define constraints:
primaryKey:t.column(id, primaryKey: true)ort.column(id, primaryKey: .autoincrement)unique:t.column(email, unique: true)check:t.column(email, check: email.like("%@%"))defaultValue:t.column(name, defaultValue: "Anonymous")collate:t.column(email, collate: .nocase)references:t.column(user_id, references: users, id)
Table Constraints
For constraints spanning multiple columns, use table-level functions:
primaryKey():t.primaryKey(userId, accountId)unique():t.unique(firstName, lastName)check():t.check(balance >= 0)foreignKey():t.foreignKey(user_id, references: users, id, delete: .setNull)
Inserting and Upserting Rows
Inserting
To insert a row, call the insert method with a list of Setters, created using the <- operator. insert returns the rowid of the new row.
let rowid = try db.run(users.insert(email <- "alice@mac.com", name <- "Alice"))
// INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice')
To handle potential constraint violations, you can specify an OnConflict resolution:
try db.run(users.insert(or: .replace, email <- "alice@mac.com", name <- "Alice B."))
// INSERT OR REPLACE INTO ...
Upserting (Insert or Update)
An "upsert" is an operation that inserts a row, but if the insertion would violate a uniqueness constraint, it performs an update instead. This requires SQLite 3.24.0+.
try db.run(users.upsert(email <- "alice@mac.com", name <- "Alice C.", onConflictOf: email))
// INSERT INTO "users" ("email", "name") VALUES ('alice@mac.com', 'Alice C.')
// ON CONFLICT ("email") DO UPDATE SET "name" = "excluded"."name"
Selecting Rows
Query objects are lazily executed when you iterate over them.
Iterating and Accessing Values
Prepare a query with db.prepare() and loop through the results. Access column values using the expression as a subscript on the Row object.
for user in try db.prepare(users) {
print("id: \(user[id]), email: \(user[email])")
// id: 1, email: alice@mac.com
}
For safer error handling, especially in complex iterations, use the RowIterator API:
let rowIterator = try db.prepareRowIterator(users)
while let row = try rowIterator.failableNext() {
// Handle row
}
Plucking a Single Row
To fetch just the first matching row, use db.pluck().
if let user = try db.pluck(users) {
print("First user: \(user[email])")
}
// SELECT * FROM "users" LIMIT 1
Building Complex Queries
You can chain methods to build more specific queries:
-
select: Choose which columns to return.users.select(id, email) // SELECT "id", "email" FROM "users" -
join: Combine rows from multiple tables.users.join(posts, on: posts[userId] == users[id]) // SELECT * FROM "users" INNER JOIN "posts" ON ("posts"."user_id" = "users"."id") -
filter/where: Add aWHEREclause.users.filter(age >= 21 && name != nil) // SELECT * FROM "users" WHERE (("age" >= 21) AND ("name" IS NOT NULL)) -
order: Sort the results.users.order(email.desc, name.asc) // SELECT * FROM "users" ORDER BY "email" DESC, "name" ASC -
limit: Paginate results.users.limit(10, offset: 20) // SELECT * FROM "users" LIMIT 10 OFFSET 20
Aggregation
Perform aggregate calculations directly on a query.
let count = try db.scalar(users.count)
// SELECT count(*) FROM "users"
let maxAge = try db.scalar(users.select(age.max))
// SELECT max("age") FROM "users"
Available aggregates include count, max, min, average, sum, and total.
Updating Rows
To update rows, chain the update method onto a filtered query. It returns the number of rows changed.
let alice = users.filter(id == 1)
let changes = try db.run(alice.update(email <- "alice.liddell@mac.com"))
if changes > 0 {
print("Updated Alice's email")
}
// UPDATE "users" SET "email" = 'alice.liddell@mac.com' WHERE ("id" = 1)
Calling update on an unfiltered query will update every row in the table!
Deleting Rows
Similarly, use the delete method on a filtered query. It returns the number of rows deleted.
let alice = users.filter(id == 1)
let changes = try db.run(alice.delete())
if changes > 0 {
print("Deleted Alice")
}
// DELETE FROM "users" WHERE ("id" = 1)
Calling delete on an unfiltered query will delete every row in the table!