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 NULL
by 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 Setter
s, 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 aWHERE
clause.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!