Skip to content

GRDB.swift

Databases & CachingDatabaseSwift

What it is

GRDB is a SQLite toolkit for Swift offering raw SQL, a query interface, database observation and record types, with strong concurrency guarantees.

Access the database through a queue or pool. Records map rows to types, and ValueObservation emits whenever the underlying data changes.

Installation

.package(url: "https://github.com/groue/GRDB.swift.git", from: "7.0.0")

Getting started

The smallest useful thing you can do with it, and what each part means.

Records, migrations and queries
struct Book: Codable, FetchableRecord, MutablePersistableRecord {
    var id: Int64?
    var title: String
    var year: Int

    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

var migrator = DatabaseMigrator()
migrator.registerMigration("createBook") { db in
    try db.create(table: "book") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("title", .text).notNull()
        t.column("year", .integer).notNull().indexed()
    }
}
try migrator.migrate(dbQueue)

let recent = try await dbQueue.read { db in
    try Book.filter(Column("year") >= 1990)
            .order(Column("year").desc)
            .limit(10)
            .fetchAll(db)
}
Reads and writes go through explicit read and write blocks, so the API makes it structurally difficult to touch the database from the wrong context.

Advanced usage

Where the library earns its place over a simpler alternative.

Observation and concurrent reads
// Emits the current value, then again on every relevant change.
let observation = ValueObservation.tracking { db in
    try Book.order(Column("year").desc).fetchAll(db)
}

for try await books in observation.values(in: dbQueue) {
    self.books = books
}

// A pool allows concurrent reads alongside one writer (WAL mode).
var config = Configuration()
config.prepareDatabase { db in
    try db.execute(sql: "PRAGMA journal_mode = WAL")
}
let dbPool = try DatabasePool(path: path, configuration: config)
ValueObservation tracks which tables the query touched, so it re-emits only on relevant writes rather than on every change to the database.

Errors and fixes

The failures you are most likely to hit, and what actually resolves them.

SQLite error 5: database is locked
A long write is blocking. Shorten write transactions, and use DatabasePool with WAL so readers do not contend with the writer.
Records do not save the generated id
Implement didInsert on a MutablePersistableRecord to capture the assigned row id.

Best practices

  • Use DatabasePool with WAL for concurrent reads; DatabaseQueue serialises everything.
  • Always use DatabaseMigrator rather than ad hoc schema changes.
  • Use ValueObservation for UI rather than polling.
  • Keep write blocks short — they hold the write lock for their duration.

Background

Why it exists, and what it was reacting to.

GRDB's distinguishing feature is its concurrency model: a DatabaseQueue or DatabasePool enforces safe access patterns, so the data races that plague hand-rolled SQLite code are prevented by the API.