Skip to content

groue/GRDB.swift

master
Switch branches/tags
Code

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

GRDB: A toolkit for SQLite databases, with a focus on application development

A toolkit for SQLite databases, with a focus on application development

Swift 5.7 Platforms License


Latest release: September 9, 2022 • version 6.0.0CHANGELOGMigrating From GRDB 5 to GRDB 6

Requirements: iOS 11.0+ / macOS 10.13+ / tvOS 11.0+ / watchOS 4.0+ • SQLite 3.19.3+ • Swift 5.7+ / Xcode 14+

Swift version GRDB version
Swift 5.7+ v6.0.0
Swift 5.3 v5.26.0
Swift 5.2 v5.12.0
Swift 5.1 v4.14.0
Swift 5 v4.14.0
Swift 4.2 v4.14.0
Swift 4.1 v3.7.0
Swift 4 v2.10.0
Swift 3.2 v1.3.0
Swift 3.1 v1.3.0
Swift 3 v1.0
Swift 2.3 v0.81.2
Swift 2.2 v0.80.2

Contact:

What is this?

GRDB provides raw access to SQL and advanced SQLite features, because one sometimes enjoys a sharp tool. It has robust concurrency primitives, so that multi-threaded applications can efficiently use their databases. It grants your application models with persistence and fetching methods, so that you don't have to deal with SQL and raw database rows when you don't want to.

Compared to SQLite.swift or FMDB, GRDB can spare you a lot of glue code. Compared to Core Data or Realm, it can simplify your multi-threaded applications.

It comes with up-to-date documentation, general guides, and it is fast.

See Why Adopt GRDB? if you are looking for your favorite database library.


FeaturesUsageInstallationDocumentationFAQ


Features

Programming tools for both database beginners and SQLite experts:

In-depth integration with our programming environment:

  • Swift Concurrency: try await your database.
  • SwiftUI: Access and observe the database from your SwiftUI views.
  • Combine: Access and observe the database with Combine publishers.
  • RxSwift: Access and observe the database with RxSwift observables.

Usage

Start using the database in four steps
import GRDB

// 1. Open a database connection
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")

// 2. Define the database schema
try dbQueue.write { db in
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("name", .text).notNull()
        t.column("score", .integer).notNull()
    }
}

// 3. Define a record type
struct Player: Codable, FetchableRecord, PersistableRecord {
    var id: Int64
    var name: String
    var score: Int
}

// 4. Access the database
try dbQueue.write { db in
    try Player(id: 1, name: "Arthur", score: 100).insert(db)
    try Player(id: 2, name: "Barbara", score: 1000).insert(db)
}

let players: [Player] = try dbQueue.read { db in
    try Player.fetchAll(db)
}
Activate the WAL mode
import GRDB

// Simple database connection
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")

// Enhanced multithreading based on SQLite's WAL mode
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")

See Database Connections

Access to raw SQL
try dbQueue.write { db in
    try db.execute(sql: """
        CREATE TABLE place (
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          title TEXT NOT NULL,
          favorite BOOLEAN NOT NULL DEFAULT 0,
          latitude DOUBLE NOT NULL,
          longitude DOUBLE NOT NULL)
        """)
    
    try db.execute(sql: """
        INSERT INTO place (title, favorite, latitude, longitude)
        VALUES (?, ?, ?, ?)
        """, arguments: ["Paris", true, 48.85341, 2.3488])
    
    let parisId = db.lastInsertedRowID
    
    // Avoid SQL injection with SQL interpolation
    try db.execute(literal: """
        INSERT INTO place (title, favorite, latitude, longitude)
        VALUES (\("King's Cross"), \(true), \(51.52151), \(-0.12763))
        """)
}

See Executing Updates

Access to raw database rows and values
try dbQueue.read { db in
    // Fetch database rows
    let rows = try Row.fetchCursor(db, sql: "SELECT * FROM place")
    while let row = try rows.next() {
        let title: String = row["title"]
        let isFavorite: Bool = row["favorite"]
        let coordinate = CLLocationCoordinate2D(
            latitude: row["latitude"],
            longitude: row["longitude"])
    }
    
    // Fetch values
    let placeCount = try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM place")! // Int
    let placeTitles = try String.fetchAll(db, sql: "SELECT title FROM place") // [String]
}

let placeCount = try dbQueue.read { db in
    try Int.fetchOne(db, sql: "SELECT COUNT(*) FROM place")!
}

See Fetch Queries

Database model types aka "records"
struct Place {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
}

// snip: turn Place into a "record" by adopting the protocols that
// provide fetching and persistence methods.

try dbQueue.write { db in
    // Create database table
    try db.create(table: "place") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("title", .text).notNull()
        t.column("favorite", .boolean).notNull().defaults(to: false)
        t.column("longitude", .double).notNull()
        t.column("latitude", .double).notNull()
    }
    
    var berlin = Place(
        id: nil,
        title: "Berlin",
        isFavorite: false,
        coordinate: CLLocationCoordinate2D(latitude: 52.52437, longitude: 13.41053))
    
    try berlin.insert(db)
    berlin.id // some value
    
    berlin.isFavorite = true
    try berlin.update(db)
}

See Records

Query the database with the Swift query interface
try dbQueue.read { db in
    // Place?
    let paris = try Place.fetchOne(db, id: 1)
    
    // Place?
    let berlin = try Place.filter(Column("title") == "Berlin").fetchOne(db)
    
    // [Place]
    let favoritePlaces = try Place
        .filter(Column("favorite") == true)
        .order(Column("title"))
        .fetchAll(db)
    
    // Int
    let favoriteCount = try Place.filter(Column("favorite")).fetchCount(db)
    
    // SQL is always welcome
    let places = try Place.fetchAll(db, sql: "SELECT * FROM place")
}

See the Query Interface

Database changes notifications
// Define the observed value
let observation = ValueObservation.tracking { db in
    try Place.fetchAll(db)
}

// Start observation
let cancellable = observation.start(
    in: dbQueue,
    onError: { error in ... },
    onChange: { (places: [Place]) in print("Fresh places: \(places)") })

Ready-made support for Combine and RxSwift:

// Combine
let cancellable = observation.publisher(in: dbQueue).sink(
    receiveCompletion: { completion in ... },
    receiveValue: { (places: [Place]) in print("Fresh places: \(places)") })

// RxSwift
let disposable = observation.rx.observe(in: dbQueue).subscribe(
    onNext: { (places: [Place]) in print("Fresh places: \(places)") },
    onError: { error in ... })

See Database Observation, Combine Support, RxGRDB.

Documentation

GRDB runs on top of SQLite: you should get familiar with the SQLite FAQ. For general and detailed information, jump to the SQLite Documentation.

Demo Applications & Frequently Asked Questions

Reference

Getting Started

SQLite and SQL

Records and the Query Interface

Application Tools

Good to Know

General Guides & Good Practices

FAQ

Sample Code

Installation

The installation procedures below have GRDB use the version of SQLite that ships with the target operating system.

See Encryption for the installation procedure of GRDB with SQLCipher.

See Custom SQLite builds for the installation procedure of GRDB with a customized build of SQLite.

See Enabling FTS5 Support for the installation procedure of GRDB with support for the FTS5 full-text engine.

CocoaPods

CocoaPods is a dependency manager for Xcode projects. To use GRDB with CocoaPods (version 1.2 or higher), specify in your Podfile:

pod 'GRDB.swift'

GRDB can be installed as a framework, or a static library.

Swift Package Manager

The Swift Package Manager automates the distribution of Swift code. To use GRDB with SPM, add a dependency to https://github.com/groue/GRDB.swift.git

GRDB offers two libraries, GRDB and GRDB-dynamic. Pick only one. When in doubt, prefer GRDB. The GRDB-dynamic library can reveal useful if you are going to link it with multiple targets within your app and only wish to link to a shared, dynamic framework once. See How to link a Swift Package as dynamic for more information.

Note: Linux is not currently supported.

Warning: Due to an Xcode bug, you will get "No such module 'CSQLite'" errors when you want to embed the GRDB package in other targets than the main application (watch extensions, for example). UI and Unit testing targets are OK, though. See #642 for more information.

Carthage

Carthage is unsupported. For some context about this decision, see #433.

Manually

  1. Download a copy of GRDB, or clone its repository and make sure you checkout the latest tagged version.

  2. Embed the GRDB.xcodeproj project in your own project.

  3. Add the GRDB target in the Target Dependencies section of the Build Phases tab of your application target (extension target for WatchOS).

  4. Add the GRDB.framework to the Embedded Binaries section of the General tab of your application target (extension target for WatchOS).

💡 Tip: see the Demo Applications for examples of such integration.

Database Connections

GRDB provides two classes for accessing SQLite databases: DatabaseQueue and DatabasePool:

import GRDB

// Pick one:
let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")

The differences are:

  • Database pools allow concurrent database accesses (this can improve the performance of multithreaded applications).
  • Database pools open your SQLite database in the WAL mode (unless read-only).
  • Database queues support in-memory databases.

If you are not sure, choose DatabaseQueue. You will always be able to switch to DatabasePool later.

Database Queues

Open a database queue with the path to a database file:

import GRDB

let dbQueue = try DatabaseQueue(path: "/path/to/database.sqlite")
let inMemoryDBQueue = try DatabaseQueue()

SQLite creates the database file if it does not already exist. The connection is closed when the database queue gets deinitialized.

A database queue can be used from any thread. The write and read methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue:

// Modify the database:
try dbQueue.write { db in
    try db.create(table: "place") { ... }
    try Place(...).insert(db)
}

// Read values:
try dbQueue.read { db in
    let places = try Place.fetchAll(db)
    let placeCount = try Place.fetchCount(db)
}

Database access methods can return values:

let placeCount = try dbQueue.read { db in
    try Place.fetchCount(db)
}

let newPlaceCount = try dbQueue.write { db -> Int in
    try Place(...).insert(db)
    return try Place.fetchCount(db)
}

See the Concurrency guide for asynchronous database accesses.

A database queue serializes accesses to the database, which means that there is never more than one thread that uses the database.

  • When you don't need to modify the database, prefer the read method. It prevents any modification to the database.

  • The write method wraps your database statements in a transaction that commits if and only if no error occurs. On the first unhandled error, all changes are reverted, the whole transaction is rollbacked, and the error is rethrown.

    When precise transaction handling is required, see Transactions and Savepoints.

A database queue needs your application to follow rules in order to deliver its safety guarantees. Please refer to the Concurrency guide.

See Database Configuration for DatabaseQueue options.

💡 Tip: see the Demo Applications for sample code.

Database Pools

A database pool allows concurrent database accesses.

import GRDB
let dbPool = try DatabasePool(path: "/path/to/database.sqlite")

SQLite creates the database file if it does not already exist. The connection is closed when the database pool gets deinitialized.

Note: unless read-only, a database pool opens your database in the SQLite "WAL mode". The WAL mode does not fit all situations. Please have a look at https://www.sqlite.org/wal.html.

A database pool can be used from any thread. The write and read methods are synchronous, and block the current thread until your database statements are executed in a protected dispatch queue:

// Modify the database:
try dbPool.write { db in
    try db.create(table: "place") { ... }
    try Place(...).insert(db)
}

// Read values:
try dbPool.read { db in
    let places = try Place.fetchAll(db)
    let placeCount = try Place.fetchCount(db)
}

Database access methods can return values:

let placeCount = try dbPool.read { db in
    try Place.fetchCount(db)
}

let newPlaceCount = try dbPool.write { db -> Int in
    try Place(...).insert(db)
    return try Place.fetchCount(db)
}

See the Concurrency guide for asynchronous database accesses.

Database pools allow several threads to access the database at the same time:

  • When you don't need to modify the database, prefer the read method, because several threads can perform reads in parallel.

    Reads are generally non-blocking, unless the maximum number of concurrent reads has been reached. In this case, a read has to wait for another read to complete. That maximum number can be configured.

  • Reads are guaranteed an immutable view of the last committed state of the database, regardless of concurrent writes. This kind of isolation is called snapshot isolation.

  • Unlike reads, writes are serialized. There is never more than a single thread that is writing into the database.

  • The write method wraps your database statements in a transaction that commits if and only if no error occurs. On the first unhandled error, all changes are reverted, the whole transaction is rollbacked, and the error is rethrown.

    When precise transaction handling is required, see Transactions and Savepoints.

  • Database pools can take snapshots of the database.

A database pool needs your application to follow rules in order to deliver its safety guarantees. See the Concurrency guide for more details about database pools, how they differ from database queues, and advanced use cases.

See Database Configuration for DatabasePool options.

💡 Tip: see the Demo Applications for sample code.

Database Configuration

var config = Configuration()
config.readonly = true
config.foreignKeysEnabled = true // Default is already true
config.label = "MyDatabase"      // Useful when your app opens multiple databases
config.maximumReaderCount = 10   // (DatabasePool only) The default is 5

let dbQueue = try DatabaseQueue( // or DatabasePool
    path: "/path/to/database.sqlite",
    configuration: config)

In debug builds, you can increase the verbosity of error descriptions and trace events if you opt in for public statement arguments:

#if DEBUG
// Protect sensitive information by enabling verbose debugging in DEBUG builds only
config.publicStatementArguments = true
#endif

let dbQueue = try DatabaseQueue(path: ..., configuration: config)

do {
    try dbQueue.write { db in
        user.name = ...
        user.location = ...
        user.address = ...
        user.phoneNumber = ...
        try user.save(db)
    }
} catch {
    // Prints sensitive information in debug builds only
    print(error)
}

Warning: It is your responsibility to prevent sensitive information from leaking in unexpected locations, so you should not set the publicStatementArguments flag in release builds (think about GDPR and other privacy-related rules).

See Configuration for more details and configuration options.

SQLite API

In this section of the documentation, we will talk SQL. Jump to the query interface if SQL is not your cup of tea.

Advanced topics:

Executing Updates

Once granted with a database connection, the execute method executes the SQL statements that do not return any database row, such as CREATE TABLE, INSERT, DELETE, ALTER, etc.

For example:

try dbQueue.write { db in
    try db.execute(sql: """
        CREATE TABLE player (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            name TEXT NOT NULL,
            score INT)
        """)
    
    try db.execute(
        sql: "INSERT INTO player (name, score) VALUES (?, ?)",
        arguments: ["Barbara", 1000])
    
    try db.execute(
        sql: "UPDATE player SET score = :score WHERE id = :id",
        arguments: ["score": 1000, "id": 1])
    }
}

The ? and colon-prefixed keys like :score in the SQL query are the statements arguments. You pass arguments with arrays or dictionaries, as in the example above. See Values for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and StatementArguments for a detailed documentation of SQLite arguments.

You can also embed query arguments right into your SQL queries, with the literal argument label, as in the example below. See SQL Interpolation for more details.

try dbQueue.write { db in
    try db.execute(literal: """
        INSERT INTO player (name, score) VALUES (\("O'Brien"), \(550))
        """)
}

Never ever embed values directly in your raw SQL strings. See Avoiding SQL Injection for more information:

// WRONG: don't embed values in raw SQL strings
let id = 123
let name = textField.text
try db.execute(
    sql: "UPDATE player SET name = '\(name)' WHERE id = \(id)")

// CORRECT: use arguments dictionary
try db.execute(
    sql: "UPDATE player SET name = :name WHERE id = :id",
    arguments: ["name": name, "id": id])

// CORRECT: use arguments array
try db.execute(
    sql: "UPDATE player SET name = ? WHERE id = ?",
    arguments: [name, id])

// CORRECT: use SQL Interpolation
try db.execute(
    literal: "UPDATE player SET name = \(name) WHERE id = \(id)")

Join multiple statements with a semicolon:

try db.execute(sql: """
    INSERT INTO player (name, score) VALUES (?, ?);
    INSERT INTO player (name, score) VALUES (?, ?);
    """, arguments: ["Arthur", 750, "Barbara", 1000])

try db.execute(literal: """
    INSERT INTO player (name, score) VALUES (\("Arthur"), \(750));
    INSERT INTO player (name, score) VALUES (\("Barbara"), \(1000));
    """)

When you want to make sure that a single statement is executed, use Prepared Statements.

After an INSERT statement, you can get the row ID of the inserted row:

try db.execute(
    sql: "INSERT INTO player (name, score) VALUES (?, ?)",
    arguments: ["Arthur", 1000])
let playerId = db.lastInsertedRowID

Don't miss Records, that provide classic persistence methods:

var player = Player(name: "Arthur", score: 1000)
try player.insert(db)
let playerId = player.id

Fetch Queries

Database connections let you fetch database rows, plain values, and custom models aka "records".

Rows are the raw results of SQL queries:

try dbQueue.read { db in
    if let row = try Row.fetchOne(db, sql: "SELECT * FROM wine WHERE id = ?", arguments: [1]) {
        let name: String = row["name"]
        let color: Color = row["color"]
        print(name, color)
    }
}

Values are the Bool, Int, String, Date, Swift enums, etc. stored in row columns:

try dbQueue.read { db in
    let urls = try URL.fetchCursor(db, sql: "SELECT url FROM wine")
    while let url = try urls.next() {
        print(url)
    }
}

Records are your application objects that can initialize themselves from rows:

let wines = try dbQueue.read { db in
    try Wine.fetchAll(db, sql: "SELECT * FROM wine")
}

Fetching Methods

Throughout GRDB, you can always fetch cursors, arrays, sets, or single values of any fetchable type (database row, simple value, or custom record):

try Row.fetchCursor(...) // A Cursor of Row
try Row.fetchAll(...)    // [Row]
try Row.fetchSet(...)    // Set<Row>
try Row.fetchOne(...)    // Row?
  • fetchCursor returns a cursor over fetched values:

    let rows = try Row.fetchCursor(db, sql: "SELECT ...") // A Cursor of Row
  • fetchAll returns an array:

    let players = try Player.fetchAll(db, sql: "SELECT ...") // [Player]
  • fetchSet returns a set:

    let names = try String.fetchSet(db, sql: "SELECT ...") // Set<String>
  • fetchOne returns a single optional value, and consumes a single database row (if any).

    let count = try Int.fetchOne(db, sql: "SELECT COUNT(*) ...") // Int?

All those fetching methods require an SQL string that contains a single SQL statement. When you want to fetch from multiple statements joined with a semicolon, iterate the multiple prepared statements found in the SQL string:

let statements = try db.allStatements(sql: """
    SELECT ...; 
    SELECT ...; 
    SELECT ...;
    """)
while let statement = try statements.next() {
    let players = try Player.fetchAll(statement)
}

You can join the results of all statements yielded by the allStatements method, like the SQLite sqlite3_exec function:

// A single cursor of all rows from all statements
let rows = try db
    .allStatements(sql: "...")
    .flatMap { statement in try Row.fetchCursor(statement) }

See prepared statements for more information about allStatements().

Cursors

Whenever you consume several rows from the database, you can fetch an Array, a Set, or a Cursor.

The fetchAll() and fetchSet() methods return regular Swift array and sets, that you iterate like all other arrays and sets:

try dbQueue.read { db in
    // [Player]
    let players = try Player.fetchAll(db, sql: "SELECT ...")
    for player in players {
        // use player
    }
}

Unlike arrays and sets, cursors returned by fetchCursor() load their results step after step:

try dbQueue.read { db in
    // Cursor of Player
    let players = try Player.fetchCursor(db, sql: "SELECT ...")
    while let player = try players.next() {
        // use player
    }
}
  • Cursors can not be used on any thread: you must consume a cursor on the dispatch queue it was created in. Particularly, don't extract a cursor out of a database access method:

    // Wrong
    let cursor = try dbQueue.read { db in
        try Player.fetchCursor(db, ...)
    }
    while let player = try cursor.next() { ... }

    Conversely, arrays and sets may be consumed on any thread:

    // OK
    let array = try dbQueue.read { db in
        try Player.fetchAll(db, ...)
    }
    for player in array { ... }
  • Cursors can be iterated only one time. Arrays and sets can be iterated many times.

  • Cursors iterate database results in a lazy fashion, and don't consume much memory. Arrays and sets contain copies of database values, and may take a lot of memory when there are many fetched results.

  • Cursors are granted with direct access to SQLite, unlike arrays and sets that have to take the time to copy database values. If you look after extra performance, you may prefer cursors.

  • Cursors can feed Swift collections.

    You will most of the time use fetchAll or fetchSet when you want an array or a set. For more specific needs, you may prefer one of the initializers below. All of them accept an extra optional minimumCapacity argument which helps optimizing your app when you have an idea of the number of elements in a cursor (the built-in fetchAll and fetchSet do not perform such an optimization).

    Arrays and all types conforming to RangeReplaceableCollection:

    // [String]
    let cursor = try String.fetchCursor(db, ...)
    let array = try Array(cursor)

    Sets:

    // Set<Int>
    let cursor = try Int.fetchCursor(db, ...)
    let set = try Set(cursor)

    Dictionaries:

    // [Int64: [Player]]
    let cursor = try Player.fetchCursor(db)
    let dictionary = try Dictionary(grouping: cursor, by: { $0.teamID })
    
    // [Int64: Player]
    let cursor = try Player.fetchCursor(db).map { ($0.id, $0) }
    let dictionary = try Dictionary(uniqueKeysWithValues: cursor)
  • Cursors adopt the Cursor protocol, which looks a lot like standard lazy sequences of Swift. As such, cursors come with many convenience methods: compactMap, contains, dropFirst, dropLast, drop(while:), enumerated, filter, first, flatMap, forEach, joined, joined(separator:), max, max(by:), min, min(by:), map, prefix, prefix(while:), reduce, reduce(into:), suffix:

    // Prints all Github links
    try URL
        .fetchCursor(db, sql: "SELECT url FROM link")
        .filter { url in url.host == "github.com" }
        .forEach { url in print(url) }
    
    // An efficient cursor of coordinates:
    let locations = try Row.
        .fetchCursor(db, sql: "SELECT latitude, longitude FROM place")
        .map { row in
            CLLocationCoordinate2D(latitude: row[0], longitude: row[1])
        }
  • Cursors are not Swift sequences. That's because Swift sequences can't handle iteration errors, when reading SQLite results may fail at any time.

  • Cursors require a little care:

    • Don't modify the results during a cursor iteration:

      // Undefined behavior
      while let player = try players.next() {
          try db.execute(sql: "DELETE ...")
      }
    • Don't turn a cursor of Row into an array or a set. You would not get the distinct rows you expect. To get a array of rows, use Row.fetchAll(...). To get a set of rows, use Row.fetchSet(...). Generally speaking, make sure you copy a row whenever you extract it from a cursor for later use: row.copy().

If you don't see, or don't care about the difference, use arrays. If you care about memory and performance, use cursors when appropriate.

Row Queries

Fetching Rows

Fetch cursors of rows, arrays, sets, or single rows (see fetching methods):

try dbQueue.read { db in
    try Row.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Row
    try Row.fetchAll(db, sql: "SELECT ...", arguments: ...)    // [Row]
    try Row.fetchSet(db, sql: "SELECT ...", arguments: ...)    // Set<Row>
    try Row.fetchOne(db, sql: "SELECT ...", arguments: ...)    // Row?
    
    let rows = try Row.fetchCursor(db, sql: "SELECT * FROM wine")
    while let row = try rows.next() {
        let name: String = row["name"]
        let color: Color = row["color"]
        print(name, color)
    }
}

let rows = try dbQueue.read { db in
    try Row.fetchAll(db, sql: "SELECT * FROM player")
}

Arguments are optional arrays or dictionaries that fill the positional ? and colon-prefixed keys like :name in the query:

let rows = try Row.fetchAll(db,
    sql: "SELECT * FROM player WHERE name = ?",
    arguments: ["Arthur"])

let rows = try Row.fetchAll(db,
    sql: "SELECT * FROM player WHERE name = :name",
    arguments: ["name": "Arthur"])

See Values for more information on supported arguments types (Bool, Int, String, Date, Swift enums, etc.), and StatementArguments for a detailed documentation of SQLite arguments.

Unlike row arrays that contain copies of the database rows, row cursors are close to the SQLite metal, and require a little care:

Note: Don't turn a cursor of Row into an array or a set. You would not get the distinct rows you expect. To get a array of rows, use Row.fetchAll(...). To get a set of rows, use Row.fetchSet(...). Generally speaking, make sure you copy a row whenever you extract it from a cursor for later use: row.copy().

Column Values

Read column values by index or column name:

let name: String = row[0]      // 0 is the leftmost column
let name: String = row["name"] // Leftmost matching column - lookup is case-insensitive
let name: String = row[Column("name")] // Using query interface's Column

Make sure to ask for an optional when the value may be NULL:

let name: String? = row["name"]

The row[] subscript returns the type you ask for. See Values for more information on supported value types:

let bookCount: Int     = row["bookCount"]
let bookCount64: Int64 = row["bookCount"]
let hasBooks: Bool     = row["bookCount"] // false when 0

let string: String     = row["date"]      // "2015-09-11 18:14:15.123"
let date: Date         = row["date"]      // Date
self.date = row["date"] // Depends on the type of the property.

You can also use the as type casting operator:

row[...] as Int
row[...] as Int?

Warning: avoid the as! and as? operators:

if let int = row[...] as? Int { ... } // BAD - doesn't work
if let int = row[...] as Int? { ... } // GOOD

Generally speaking, you can extract the type you need, provided it can be converted from the underlying SQLite value:

  • Successful conversions include:

    • All numeric SQLite values to all numeric Swift types, and Bool (zero is the only false boolean).
    • Text SQLite values to Swift String.
    • Blob SQLite values to Foundation Data.

    See Values for more information on supported types (Bool, Int, String, Date, Swift enums, etc.)

  • NULL returns nil.

    let row = try Row.fetchOne(db, sql: "SELECT NULL")!
    row[0] as Int? // nil
    row[0] as Int  // fatal error: could not convert NULL to Int.

    There is one exception, though: the DatabaseValue type:

    row[0] as DatabaseValue // DatabaseValue.null
  • Missing columns return nil.

    let row = try Row.fetchOne(db, sql: "SELECT 'foo' AS foo")!
    row["missing"] as String? // nil
    row["missing"] as String  // fatal error: no such column: missing

    You can explicitly check for a column presence with the hasColumn method.

  • Invalid conversions throw a fatal error.

    let row = try Row.fetchOne(db, sql: "SELECT 'Mom’s birthday'")!
    row[0] as String // "Mom’s birthday"
    row[0] as Date?  // fatal error: could not convert "Mom’s birthday" to Date.
    row[0] as Date   // fatal error: could not convert "Mom’s birthday" to Date.
    
    let row = try Row.fetchOne(db, sql: "SELECT 256")!
    row[0] as Int    // 256
    row[0] as UInt8? // fatal error: could not convert 256 to UInt8.
    row[0] as UInt8  // fatal error: could not convert 256 to UInt8.

    Those conversion fatal errors can be avoided with the DatabaseValue type:

    let row = try Row.fetchOne(db, sql: "SELECT 'Mom’s birthday'")!
    let dbValue: DatabaseValue = row[0]
    if dbValue.isNull {
        // Handle NULL
    } else if let date = Date.fromDatabaseValue(dbValue) {
        // Handle valid date
    } else {
        // Handle invalid date
    }

    This extra verbosity is the consequence of having to deal with an untrusted database: you may consider fixing the content of your database instead. See Fatal Errors for more information.

  • SQLite has a weak type system, and provides convenience conversions that can turn String to Int, Double to Blob, etc.

    GRDB will sometimes let those conversions go through:

    let rows = try Row.fetchCursor(db, sql: "SELECT '20 small cigars'")
    while let row = try rows.next() {
        row[0] as Int   // 20
    }

    Don't freak out: those conversions did not prevent SQLite from becoming the immensely successful database engine you want to use. And GRDB adds safety checks described just above. You can also prevent those convenience conversions altogether by using the DatabaseValue type.

DatabaseValue

DatabaseValue is an intermediate type between SQLite and your values, which gives information about the raw value stored in the database.

You get DatabaseValue just like other value types:

let dbValue: DatabaseValue = row[0]
let dbValue: DatabaseValue? = row["name"] // nil if and only if column does not exist

// Check for NULL:
dbValue.isNull // Bool

// The stored value:
dbValue.storage.value // Int64, Double, String, Data, or nil

// All the five storage classes supported by SQLite:
switch dbValue.storage {
case .null:                 print("NULL")
case .int64(let int64):     print("Int64: \(int64)")
case .double(let double):   print("Double: \(double)")
case .string(let string):   print("String: \(string)")
case .blob(let data):       print("Data: \(data)")
}

You can extract regular values (Bool, Int, String, Date, Swift enums, etc.) from DatabaseValue with the DatabaseValueConvertible.fromDatabaseValue() method:

let dbValue: DatabaseValue = row["bookCount"]
let bookCount   = Int.fromDatabaseValue(dbValue)   // Int?
let bookCount64 = Int64.fromDatabaseValue(dbValue) // Int64?
let hasBooks    = Bool.fromDatabaseValue(dbValue)  // Bool?, false when 0

let dbValue: DatabaseValue = row["date"]
let string = String.fromDatabaseValue(dbValue)     // "2015-09-11 18:14:15.123"
let date   = Date.fromDatabaseValue(dbValue)       // Date?

fromDatabaseValue returns nil for invalid conversions:

let row = try Row.fetchOne(db, sql: "SELECT 'Mom’s birthday'")!
let dbValue: DatabaseValue = row[0]
let string = String.fromDatabaseValue(dbValue) // "Mom’s birthday"
let int    = Int.fromDatabaseValue(dbValue)    // nil
let date   = Date.fromDatabaseValue(dbValue)   // nil

Rows as Dictionaries

Row adopts the standard RandomAccessCollection protocol, and can be seen as a dictionary of DatabaseValue:

// All the (columnName, dbValue) tuples, from left to right:
for (columnName, dbValue) in row {
    ...
}

You can build rows from dictionaries (standard Swift dictionaries and NSDictionary). See Values for more information on supported types:

let row: Row = ["name": "foo", "date": nil]
let row = Row(["name": "foo", "date": nil])
let row = Row(/* [AnyHashable: Any] */) // nil if invalid dictionary

Yet rows are not real dictionaries: they may contain duplicate columns:

let row = try Row.fetchOne(db, sql: "SELECT 1 AS foo, 2 AS foo")!
row.columnNames    // ["foo", "foo"]
row.databaseValues // [1, 2]
row["foo"]         // 1 (leftmost matching column)
for (columnName, dbValue) in row { ... } // ("foo", 1), ("foo", 2)

When you build a dictionary from a row, you have to disambiguate identical columns, and choose how to present database values. For example:

  • A [String: DatabaseValue] dictionary that keeps leftmost value in case of duplicated column name:

    let dict = Dictionary(row, uniquingKeysWith: { (left, _) in left })
  • A [String: AnyObject] dictionary which keeps rightmost value in case of duplicated column name. This dictionary is identical to FMResultSet's resultDictionary from FMDB. It contains NSNull values for null columns, and can be shared with Objective-C:

    let dict = Dictionary(
        row.map { (column, dbValue) in
            (column, dbValue.storage.value as AnyObject)
        },
        uniquingKeysWith: { (_, right) in right })
  • A [String: Any] dictionary that can feed, for example, JSONSerialization:

    let dict = Dictionary(
        row.map { (column, dbValue) in
            (column, dbValue.storage.value)
        },
        uniquingKeysWith: { (left, _) in left })

See the documentation of Dictionary.init(_:uniquingKeysWith:) for more information.

Value Queries

Instead of rows, you can directly fetch values. There are many supported value types (Bool, Int, String, Date, Swift enums, etc.).

Like rows, fetch values as cursors, arrays, sets, or single values (see fetching methods). Values are extracted from the leftmost column of the SQL queries:

try dbQueue.read { db in
    try Int.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Int
    try Int.fetchAll(db, sql: "SELECT ...", arguments: ...)    // [Int]
    try Int.fetchSet(db, sql: "SELECT ...", arguments: ...)    // Set<Int>
    try Int.fetchOne(db, sql: "SELECT ...", arguments: ...)    // Int?
    
    let maxScore = try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") // Int?
    let names = try String.fetchAll(db, sql: "SELECT name FROM player")       // [String]
}

Int.fetchOne returns nil in two cases: either the SELECT statement yielded no row, or one row with a NULL value:

// No row:
try Int.fetchOne(db, sql: "SELECT 42 WHERE FALSE") // nil

// One row with a NULL value:
try Int.fetchOne(db, sql: "SELECT NULL")           // nil

// One row with a non-NULL value:
try Int.fetchOne(db, sql: "SELECT 42")             // 42

For requests which may contain NULL, fetch optionals:

try dbQueue.read { db in
    try Optional<Int>.fetchCursor(db, sql: "SELECT ...", arguments: ...) // A Cursor of Int?
    try Optional<Int>.fetchAll(db, sql: "SELECT ...", arguments: ...)    // [Int?]
    try Optional<Int>.fetchSet(db, sql: "SELECT ...", arguments: ...)    // Set<Int?>
}

💡 Tip: One advanced use case, when you fetch one value, is to distinguish the cases of a statement that yields no row, or one row with a NULL value. To do so, use Optional<Int>.fetchOne, which returns a double optional Int??:

// No row:
try Optional<Int>.fetchOne(db, sql: "SELECT 42 WHERE FALSE") // .none
// One row with a NULL value:
try Optional<Int>.fetchOne(db, sql: "SELECT NULL")           // .some(.none)
// One row with a non-NULL value:
try Optional<Int>.fetchOne(db, sql: "SELECT 42")             // .some(.some(42))

There are many supported value types (Bool, Int, String, Date, Swift enums, etc.). See Values for more information.

Values

GRDB ships with built-in support for the following value types:

Values can be used as statement arguments:

let url: URL = ...
let verified: Bool = ...
try db.execute(
    sql: "INSERT INTO link (url, verified) VALUES (?, ?)",
    arguments: [url, verified])

Values can be extracted from rows:

let rows = try Row.fetchCursor(db, sql: "SELECT * FROM link")
while let row = try rows.next() {
    let url: URL = row["url"]
    let verified: Bool = row["verified"]
}

Values can be directly fetched:

let urls = try URL.fetchAll(db, sql: "SELECT url FROM link")  // [URL]

Use values in Records:

struct Link: FetchableRecord {
    var url: URL
    var isVerified: Bool
    
    init(row: Row) {
        url = row["url"]
        isVerified = row["verified"]
    }
}

Use values in the query interface:

let url: URL = ...
let link = try Link.filter(Column("url") == url).fetchOne(db)

Data (and Memory Savings)

Data suits the BLOB SQLite columns. It can be stored and fetched from the database just like other values:

let rows = try Row.fetchCursor(db, sql: "SELECT data, ...")
while let row = try rows.next() {
    let data: Data = row["data"]
}

At each step of the request iteration, the row[] subscript creates two copies of the database bytes: one fetched by SQLite, and another, stored in the Swift Data value.

You have the opportunity to save memory by not copying the data fetched by SQLite:

while let row = try rows.next() {
    let data = row.dataNoCopy(named: "data") // Data?
}

The non-copied data does not live longer than the iteration step: make sure that you do not use it past this point.

Date and DateComponents

Date and DateComponents can be stored and fetched from the database.

Here is how GRDB supports the various date formats supported by SQLite:

SQLite format Date DateComponents
YYYY-MM-DD Read ¹ Read / Write
YYYY-MM-DD HH:MM Read ¹ ² Read ² / Write
YYYY-MM-DD HH:MM:SS Read ¹ ² Read ² / Write
YYYY-MM-DD HH:MM:SS.SSS Read ¹ ² / Write ¹ Read ² / Write
YYYY-MM-DDTHH:MM Read ¹ ² Read ²
YYYY-MM-DDTHH:MM:SS Read ¹ ² Read ²
YYYY-MM-DDTHH:MM:SS.SSS Read ¹ ² Read ²
HH:MM Read ² / Write
HH:MM:SS Read ² / Write
HH:MM:SS.SSS Read ² / Write
Timestamps since unix epoch Read ³
now

¹ Missing components are assumed to be zero. Dates are stored and read in the UTC time zone, unless the format is followed by a timezone indicator ⁽²⁾.

² This format may be optionally followed by a timezone indicator of the form [+-]HH:MM or just Z.

³ GRDB 2+ interprets numerical values as timestamps that fuel Date(timeIntervalSince1970:). Previous GRDB versions used to interpret numbers as julian days. Julian days are still supported, with the Date(julianDay:) initializer.

Warning: the range of valid years in the SQLite date formats is 0000-9999. You will need to pick another date format when your application needs to process years outside of this range. See the following chapters.

Date

Date can be stored and fetched from the database just like other values:

try db.execute(
    sql: "INSERT INTO player (creationDate, ...) VALUES (?, ...)",
    arguments: [Date(), ...])

let row = try Row.fetchOne(db, ...)!
let creationDate: Date = row["creationDate"]

Dates are stored using the format "YYYY-MM-DD HH:MM:SS.SSS" in the UTC time zone. It is precise to the millisecond.

Note: this format was chosen because it is the only format that is:

  • Comparable (ORDER BY date works)
  • Comparable with the SQLite keyword CURRENT_TIMESTAMP (WHERE date > CURRENT_TIMESTAMP works)
  • Able to feed SQLite date & time functions
  • Precise enough

Warning: the range of valid years in the SQLite date format is 0000-9999. You will experience problems with years outside of this range, such as decoding errors, or invalid date computations with SQLite date & time functions.

Some applications may prefer another date format:

  • Some may prefer ISO-8601, with a T separator.
  • Some may prefer ISO-8601, with a time zone.
  • Some may need to store years beyond the 0000-9999 range.
  • Some may need sub-millisecond precision.
  • Some may need exact Date roundtrip.
  • Etc.

You should think twice before choosing a different date format:

  • ISO-8601 is about exchange and communication, when SQLite is about storage and data manipulation. Sharing the same representation in your database and in JSON files only provides a superficial convenience, and should be the least of your priorities. Don't store dates as ISO-8601 without understanding what you lose. For example, ISO-8601 time zones forbid database-level date comparison.
  • Sub-millisecond precision and exact Date roundtrip are not as obvious needs as it seems at first sight. Dates generally don't precisely roundtrip as soon as they leave your application anyway, because the other systems your app communicates with use their own date representation (the Android version of your app, the server your application is talking to, etc.) On top of that, Date comparison is at least as hard and nasty as floating point comparison.

The customization of date format is explicit. For example:

let date = Date()
let timeInterval = date.timeIntervalSinceReferenceDate
try db.execute(
    sql: "INSERT INTO player (creationDate, ...) VALUES (?, ...)",
    arguments: [timeInterval, ...])

if let row = try Row.fetchOne(db, ...) {
    let timeInterval: TimeInterval = row["creationDate"]
    let creationDate = Date(timeIntervalSinceReferenceDate: timeInterval)
}

See also Codable Records for more date customization options, and DatabaseValueConvertible if you want to define a Date-wrapping type with customized database representation.

DateComponents

DateComponents is indirectly supported, through the DatabaseDateComponents helper type.

DatabaseDateComponents reads date components from all date formats supported by SQLite, and stores them in the format of your choice, from HH:MM to YYYY-MM-DD HH:MM:SS.SSS.

Warning: the range of valid years is 0000-9999. You will experience problems with years outside of this range, such as decoding errors, or invalid date computations with SQLite date & time functions. See Date for more information.

DatabaseDateComponents can be stored and fetched from the database just like other values:

let components = DateComponents()
components.year = 1973
components.month = 9
components.day = 18

// Store "1973-09-18"
let dbComponents = DatabaseDateComponents(components, format: .YMD)
try db.execute(
    sql: "INSERT INTO player (birthDate, ...) VALUES (?, ...)",
    arguments: [dbComponents, ...])

// Read "1973-09-18"
let row = try Row.fetchOne(db, sql: "SELECT birthDate ...")!
let dbComponents: DatabaseDateComponents = row["birthDate"]
dbComponents.format         // .YMD (the actual format found in the database)
dbComponents.dateComponents // DateComponents

NSNumber, NSDecimalNumber, and Decimal

NSNumber and Decimal can be stored and fetched from the database just like other values.

Here is how GRDB supports the various data types supported by SQLite:

Integer Double String
NSNumber Read / Write Read / Write Read
NSDecimalNumber Read / Write Read / Write Read
Decimal Read Read Read / Write
  • All three types can decode database integers and doubles:

    let number = try NSNumber.fetchOne(db, sql: "SELECT 10")            // NSNumber
    let number = try NSDecimalNumber.fetchOne(db, sql: "SELECT 1.23")   // NSDecimalNumber
    let number = try Decimal.fetchOne(db, sql: "SELECT -100")           // Decimal
  • All three types decode database strings as decimal numbers:

    let number = try NSNumber.fetchOne(db, sql: "SELECT '10'")          // NSDecimalNumber (sic)
    let number = try NSDecimalNumber.fetchOne(db, sql: "SELECT '1.23'") // NSDecimalNumber
    let number = try Decimal.fetchOne(db, sql: "SELECT '-100'")         // Decimal
  • NSNumber and NSDecimalNumber send 64-bit signed integers and doubles in the database:

    // INSERT INTO transfer VALUES (10)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSNumber(value: 10)])
    
    // INSERT INTO transfer VALUES (10.0)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSNumber(value: 10.0)])
    
    // INSERT INTO transfer VALUES (10)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSDecimalNumber(string: "10.0")])
    
    // INSERT INTO transfer VALUES (10.5)
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [NSDecimalNumber(string: "10.5")])

    Warning: since SQLite does not support decimal numbers, sending a non-integer NSDecimalNumber can result in a loss of precision during the conversion to double.

    Instead of sending non-integer NSDecimalNumber to the database, you may prefer:

    • Send Decimal instead (those store decimal strings in the database).
    • Send integers instead (for example, store amounts of cents instead of amounts of Euros).
  • Decimal sends decimal strings in the database:

    // INSERT INTO transfer VALUES ('10')
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [Decimal(10)])
    
    // INSERT INTO transfer VALUES ('10.5')
    try db.execute(sql: "INSERT INTO transfer VALUES (?)", arguments: [Decimal(string: "10.5")!])

UUID

UUID can be stored and fetched from the database just like other values.

GRDB stores uuids as 16-bytes data blobs, and decodes them from both 16-bytes data blobs and strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F".

Swift Enums

Swift enums and generally all types that adopt the RawRepresentable protocol can be stored and fetched from the database just like their raw values:

enum Color : Int {
    case red, white, rose
}

enum Grape : String {
    case chardonnay, merlot, riesling
}

// Declare empty DatabaseValueConvertible adoption
extension Color : DatabaseValueConvertible { }
extension Grape : DatabaseValueConvertible { }

// Store
try db.execute(
    sql: "INSERT INTO wine (grape, color) VALUES (?, ?)",
    arguments: [Grape.merlot, Color.red])

// Read
let rows = try Row.fetchCursor(db, sql: "SELECT * FROM wine")
while let row = try rows.next() {
    let grape: Grape = row["grape"]
    let color: Color = row["color"]
}

When a database value does not match any enum case, you get a fatal error. This fatal error can be avoided with the DatabaseValue type:

let row = try Row.fetchOne(db, sql: "SELECT 'syrah'")!

row[0] as String  // "syrah"
row[0] as Grape?  // fatal error: could not convert "syrah" to Grape.
row[0] as Grape   // fatal error: could not convert "syrah" to Grape.

let dbValue: DatabaseValue = row[0]
if dbValue.isNull {
    // Handle NULL
} else if let grape = Grape.fromDatabaseValue(dbValue) {
    // Handle valid grape
} else {
    // Handle unknown grape
}

Custom Value Types

Conversion to and from the database is based on the DatabaseValueConvertible protocol:

protocol DatabaseValueConvertible {
    /// Returns a value that can be stored in the database.
    var databaseValue: DatabaseValue { get }
    
    /// Returns a value initialized from dbValue, if possible.
    static func fromDatabaseValue(_ dbValue: DatabaseValue) -> Self?
}

All types that adopt this protocol can be used like all other values (Bool, Int, String, Date, Swift enums, etc.)

The databaseValue property returns DatabaseValue, a type that wraps the five values supported by SQLite: NULL, Int64, Double, String and Data. Since DatabaseValue has no public initializer, use DatabaseValue.null, or another type that already adopts the protocol: 1.databaseValue, "foo".databaseValue, etc. Conversion to DatabaseValue must not fail.

The fromDatabaseValue() factory method returns an instance of your custom type if the database value contains a suitable value. If the database value does not contain a suitable value, such as "foo" for Date, fromDatabaseValue must return nil (GRDB will interpret this nil result as a conversion error, and react accordingly).

Value types that adopt both DatabaseValueConvertible and an archival protocol (Codable, Encodable or Decodable) are automatically coded and decoded from JSON arrays and objects:

// Encoded as a JSON object in the database:
struct Color: Codable, DatabaseValueConvertible {
    var r: Double
    var g: Double
    var b: Double
}

For such codable value types, GRDB uses the standard JSONDecoder and JSONEncoder from Foundation. By default, Data values are handled with the .base64 strategy, Date with the .millisecondsSince1970 strategy, and non conforming floats with the .throw strategy.

In order to customize the JSON format, provide a custom implementation of the DatabaseValueConvertible requirements.

Note: standard sequences such as Array, Set, or Dictionary do not conform to DatabaseValueConvertible, even conditionally. You won't be able to directly fetch or store arrays, sets, or dictionaries as JSON database values. You can get free JSON support from these standard types when they are embedded as properties of Codable Records, though.

Transactions and Savepoints

Transactions and Safety

A transaction is a fundamental tool of SQLite that guarantees data consistency as well as proper isolation between application threads and database connections.

GRDB generally opens transactions for you, as a way to enforce its concurrency guarantees, and provide maximal security for both your application data and application logic:

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbQueue.write { db in
    try Credit(destinationAccount, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbPool.write { db in
    try Credit(destinationAccount, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

Yet you may need to exactly control when transactions take place:

Explicit Transactions

DatabaseQueue.inDatabase() and DatabasePool.writeWithoutTransaction() execute your database statements outside of any transaction:

// INSERT INTO credit ...
// INSERT INTO debit ...
try dbQueue.inDatabase { db in
    try Credit(destinationAccount, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

// INSERT INTO credit ...
// INSERT INTO debit ...
try dbPool.writeWithoutTransaction { db in
    try Credit(destinationAccount, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
}

Writing outside of any transaction is dangerous, for two reasons:

  • In our credit/debit example, you may successfully insert a credit, but fail inserting the debit, and end up with unbalanced accounts (oops).

    // UNSAFE DATABASE INTEGRITY
    try dbQueue.inDatabase { db in // or dbPool.writeWithoutTransaction
        try Credit(destinationAccount, amount).insert(db) // may succeed
        try Debit(sourceAccount, amount).insert(db)      // may fail
    }

    Transactions avoid this kind of bug.

  • Database pool concurrent reads can see an inconsistent state of the database:

    // UNSAFE CONCURRENCY
    try dbPool.writeWithoutTransaction { db in
        try Credit(destinationAccount, amount).insert(db)
        // <- Concurrent dbPool.read sees a partial db update here
        try Debit(sourceAccount, amount).insert(db)
    }

    Transactions avoid this kind of bug, too.

To open explicit transactions, use one of the Database.inTransaction, DatabaseQueue.inTransaction, or DatabasePool.writeInTransaction methods:

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbQueue.inDatabase { db in  // or dbPool.writeWithoutTransaction
    try db.inTransaction {
        try Credit(destinationAccount, amount).insert(db)
        try Debit(sourceAccount, amount).insert(db)
        return .commit
    }
}

// BEGIN TRANSACTION
// INSERT INTO credit ...
// INSERT INTO debit ...
// COMMIT
try dbQueue.inTransaction { db in  // or dbPool.writeInTransaction
    try Credit(destinationAccount, amount).insert(db)
    try Debit(sourceAccount, amount).insert(db)
    return .commit
}

If an error is thrown from the transaction block, the transaction is rollbacked and the error is rethrown by the inTransaction method. If you return .rollback instead of .commit, the transaction is also rollbacked, but no error is thrown.

You can also perform manual transaction management:

try dbQueue.inDatabase { db in  // or dbPool.writeWithoutTransaction
    try db.beginTransaction()
    ...
    try db.commit()
    
    try db.execute(sql: "BEGIN TRANSACTION")
    ...
    try db.execute(sql: "ROLLBACK")
}

Transactions can't be left opened unless you set the allowsUnsafeTransactions configuration flag:

// fatal error: A transaction has been left opened at the end of a database access
try dbQueue.inDatabase { db in
    try db.execute(sql: "BEGIN TRANSACTION")
    // <- no commit or rollback
}

You can ask if a transaction is currently opened:

func myCriticalMethod(_ db: Database) throws {
    precondition(db.isInsideTransaction, "This method requires a transaction")
    try ...
}

Yet, you have a better option than checking for transactions: critical database sections should use savepoints, described below:

func myCriticalMethod(_ db: Database) throws {
    try db.inSavepoint {
        // Here the database is guaranteed to be inside a transaction.
        try ...
    }
}

Savepoints

Statements grouped in a savepoint can be rollbacked without invalidating a whole transaction:

try dbQueue.write { db in
    // Makes sure both inserts succeed, or none:
    try db.inSavepoint {
        try Credit(destinationAccount, amount).insert(db)
        try Debit(sourceAccount, amount).insert(db)
        return .commit
    }
    
    // Other savepoints, etc...
}

If an error is thrown from the savepoint block, the savepoint is rollbacked and the error is rethrown by the inSavepoint method. If you return .rollback instead of .commit, the savepoint is also rollbacked, but no error is thrown.

Unlike transactions, savepoints can be nested. They implicitly open a transaction if no one was opened when the savepoint begins. As such, they behave just like nested transactions. Yet the database changes are only written to disk when the outermost transaction is committed:

try dbQueue.inDatabase { db in
    try db.inSavepoint {
        ...
        try db.inSavepoint {
            ...
            return .commit
        }
        ...
        return .commit  // writes changes to disk
    }
}

SQLite savepoints are more than nested transactions, though. For advanced uses, use SQLite savepoint documentation.

Transaction Kinds

SQLite supports three kinds of transactions: deferred (the default), immediate, and exclusive.

The transaction kind can be changed in the database configuration, or for each transaction:

// 1) Default configuration:
let dbQueue = try DatabaseQueue(path: "...")

// BEGIN DEFERRED TRANSACTION ...
dbQueue.write { db in ... }

// BEGIN EXCLUSIVE TRANSACTION ...
dbQueue.inTransaction(.exclusive) { db in ... }

// 2) Customized default transaction kind:
var config = Configuration()
config.defaultTransactionKind = .immediate
let dbQueue = try DatabaseQueue(path: "...", configuration: config)

// BEGIN IMMEDIATE TRANSACTION ...
dbQueue.write { db in ... }

// BEGIN EXCLUSIVE TRANSACTION ...
dbQueue.inTransaction(.exclusive) { db in ... }

Prepared Statements

Prepared Statements let you prepare an SQL query and execute it later, several times if you need, with different arguments.

try dbQueue.write { db in
    let insertSQL = "INSERT INTO player (name, score) VALUES (:name, :score)"
    let insertStatement = try db.makeStatement(sql: insertSQL)
    
    let selectSQL = "SELECT * FROM player WHERE name = ?"
    let selectStatement = try db.makeStatement(sql: selectSQL)
}

The ? and colon-prefixed keys like :name in the SQL query are the statement arguments. You set them with arrays or dictionaries (arguments are actually of type StatementArguments, which happens to adopt the ExpressibleByArrayLiteral and ExpressibleByDictionaryLiteral protocols).

insertStatement.arguments = ["name": "Arthur", "score": 1000]
selectStatement.arguments = ["Arthur"]

Alternatively, you can create a prepared statement with SQL Interpolation:

let insertStatement = try db.makeStatement(literal: "INSERT ...")
let selectStatement = try db.makeStatement(literal: "SELECT ...")
//                                         ~~~~~~~

Statements can be executed:

try insertStatement.execute()

Statements can be used wherever a raw SQL query string would fit (see fetch queries):

let rows = try Row.fetchCursor(selectStatement)    // A Cursor of Row
let players = try Player.fetchAll(selectStatement) // [Player]
let players = try Player.fetchSet(selectStatement) // Set<Player>
let player = try Player.fetchOne(selectStatement)  // Player?

You can set the arguments at the moment of the statement execution:

try insertStatement.execute(arguments: ["name": "Arthur", "score": 1000])
let player = try Player.fetchOne(selectStatement, arguments: ["Arthur"])

When you want to build multiple statements joined with a semicolon, use the allStatements method:

let statements = try db.allStatements(sql: """
    INSERT INTO player (name, score) VALUES (?, ?);
    INSERT INTO player (name, score) VALUES (?, ?);
    """, arguments: ["Arthur", 100, "O'Brien", 1000])
while let statement = try statements.next() {
    try statement.execute()
}

allStatements also supports SQL Interpolation:

let statements = try db.allStatements(literal: """
    INSERT INTO player (name, score) VALUES (\("Arthur"), \(100));
    INSERT INTO player (name, score) VALUES (\("O'Brien"), \(1000));
    """)
while let statement = try statements.next() {
    try statement.execute()
}

You can turn the cursor returned from allStatements into a regular Swift array, but in this case make sure all individual statements can compile even if the previous ones were not run:

// OK: Array of statements
let statements = try Array(db.allStatements(sql: """
    INSERT ...; 
    UPDATE ...; 
    SELECT ...;
    """))

// FAILURE: Can't build an array of statements since 
// the INSERT won't compile until CREATE TABLE is run.
let statements = try Array(db.allStatements(sql: """
    CREATE TABLE player ...; 
    INSERT INTO player ...;
    """))

See also Database.execute(sql:) in the Executing Updates chapter.

Note: it is a programmer error to reuse a prepared statement that has failed: GRDB may crash if you do so.

For more information about prepared statements, see the Statement reference.

Prepared Statements Cache

When the same query will be used several times in the lifetime of your application, you may feel a natural desire to cache prepared statements.

Don't cache statements yourself.

Note: This is because you don't have the necessary tools. Statements are tied to specific SQLite connections and dispatch queues which you don't manage yourself, especially when you use database pools. A change in the database schema may, or may not invalidate a statement.

Instead, use the cachedStatement method. GRDB does all the hard caching and memory management stuff for you:

let statement = try db.cachedStatement(sql: sql)

Cached statements also support SQL Interpolation:

let statement = try db.cachedStatement(literal: "INSERT ...")
//                                     ~~~~~~~

Warning: Should a cached prepared statement throw an error, don't reuse it (it is a programmer error). Instead, reload one from the cache.

Custom SQL Functions and Aggregates

SQLite lets you define SQL functions and aggregates.

A custom SQL function or aggregate extends SQLite:

SELECT reverse(name) FROM player;   -- custom function
SELECT maxLength(name) FROM player; -- custom aggregate

Custom SQL Functions

A function argument takes an array of DatabaseValue, and returns any valid value (Bool, Int, String, Date, Swift enums, etc.) The number of database values is guaranteed to be argumentCount.

SQLite has the opportunity to perform additional optimizations when functions are "pure", which means that their result only depends on their arguments. So make sure to set the pure argument to true when possible.

let reverse = DatabaseFunction("reverse", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in
    // Extract string value, if any...
    guard let string = String.fromDatabaseValue(values[0]) else {
        return nil
    }
    // ... and return reversed string:
    return String(string.reversed())
}

You make a function available to a database connection through its configuration:

var config = Configuration()
config.prepareDatabase { db in
    db.add(function: reverse)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

try dbQueue.read { db in
    // "oof"
    try String.fetchOne(db, sql: "SELECT reverse('foo')")!
}

Functions can take a variable number of arguments:

When you don't provide any explicit argumentCount, the function can take any number of arguments:

let averageOf = DatabaseFunction("averageOf", pure: true) { (values: [DatabaseValue]) in
    let doubles = values.compactMap { Double.fromDatabaseValue($0) }
    return doubles.reduce(0, +) / Double(doubles.count)
}
db.add(function: averageOf)

// 2.0
try Double.fetchOne(db, sql: "SELECT averageOf(1, 2, 3)")!

Functions can throw:

let sqrt = DatabaseFunction("sqrt", argumentCount: 1, pure: true) { (values: [DatabaseValue]) in
    guard let double = Double.fromDatabaseValue(values[0]) else {
        return nil
    }
    guard double >= 0 else {
        throw DatabaseError(message: "invalid negative number")
    }
    return sqrt(double)
}
db.add(function: sqrt)

// SQLite error 1 with statement `SELECT sqrt(-1)`: invalid negative number
try Double.fetchOne(db, sql: "SELECT sqrt(-1)")!

Use custom functions in the query interface:

// SELECT reverseString("name") FROM player
Player.select(reverseString(nameColumn))

GRDB ships with built-in SQL functions that perform unicode-aware string transformations. See Unicode.

Custom Aggregates

Before registering a custom aggregate, you need to define a type that adopts the DatabaseAggregate protocol:

protocol DatabaseAggregate {
    // Initializes an aggregate
    init()
    
    // Called at each step of the aggregation
    mutating func step(_ dbValues: [DatabaseValue]) throws
    
    // Returns the final result
    func finalize() throws -> DatabaseValueConvertible?
}

For example:

struct MaxLength : DatabaseAggregate {
    var maxLength: Int = 0
    
    mutating func step(_ dbValues: [DatabaseValue]) {
        // At each step, extract string value, if any...
        guard let string = String.fromDatabaseValue(dbValues[0]) else {
            return
        }
        // ... and update the result
        let length = string.count
        if length > maxLength {
            maxLength = length
        }
    }
    
    func finalize() -> DatabaseValueConvertible? {
        maxLength
    }
}

let maxLength = DatabaseFunction(
    "maxLength",
    argumentCount: 1,
    pure: true,
    aggregate: MaxLength.self)

Like custom SQL Functions, you make an aggregate function available to a database connection through its configuration:

var config = Configuration()
config.prepareDatabase { db in
    db.add(function: maxLength)
}
let dbQueue = try DatabaseQueue(path: dbPath, configuration: config)

try dbQueue.read { db in
    // Some Int
    try Int.fetchOne(db, sql: "SELECT maxLength(name) FROM player")!
}

The step method of the aggregate takes an array of DatabaseValue. This array contains as many values as the argumentCount parameter (or any number of values, when argumentCount is omitted).

The finalize method of the aggregate returns the final aggregated value (Bool, Int, String, Date, Swift enums, etc.).

SQLite has the opportunity to perform additional optimizations when aggregates are "pure", which means that their result only depends on their inputs. So make sure to set the pure argument to true when possible.

Use custom aggregates in the query interface:

// SELECT maxLength("name") FROM player
let request = Player.select(maxLength.apply(nameColumn))
try Int.fetchOne(db, request) // Int?

Database Schema Introspection

GRDB comes with a set of schema introspection methods:

try dbQueue.read { db in
    // Bool, true if the table exists
    try db.tableExists("player")
    
    // [ColumnInfo], the columns in the table
    try db.columns(in: "player")
    
    // PrimaryKeyInfo
    try db.primaryKey("player")
    
    // [ForeignKeyInfo], the foreign keys defined on the table
    try db.foreignKeys(on: "player")
    
    // [IndexInfo], the indexes defined on the table
    try db.indexes(on: "player")
    
    // Bool, true if column(s) is a unique key (primary key or unique index)
    try db.table("player", hasUniqueKey: ["email"])
}

// Bool, true if argument is the name of an internal SQLite table
Database.isSQLiteInternalTable(...)

// Bool, true if argument is the name of an internal GRDB table
Database.isGRDBInternalTable(...)

Row Adapters

Row adapters let you present database rows in the way expected by the row consumers.

They basically help two incompatible row interfaces to work together. For example, a row consumer expects a column named "consumed", but the produced row has a column named "produced".

In this case, the ColumnMapping row adapter comes in handy:

// Turn the 'produced' column into 'consumed':
let adapter = ColumnMapping(["consumed": "produced"])
let row = try Row.fetchOne(db, sql: "SELECT 'Hello' AS produced", adapter: adapter)!

// [consumed:"Hello"]
print(row)

// "Hello"
print(row["consumed"])

// ▿ [consumed:"Hello"]
//   unadapted: [produced:"Hello"]
print(row.debugDescription)

// [produced:"Hello"]
print(row.unadapted)

Record types are typical row consumers that expect database rows to have a specific layout so that they can decode them:

struct MyRecord: Decodable, FetchableRecord {
    var consumed: String
}
let record = try MyRecord.fetchOne(db, sql: "SELECT 'Hello' AS produced", adapter: adapter)!
print(record.consumed) // "Hello"

There are several situations where row adapters are useful:

  • They help disambiguate columns with identical names, which may happen when you select columns from several tables. See Joined Queries Support for an example.

  • They help when SQLite outputs unexpected column names, which may happen with some subqueries. See RenameColumnAdapter for an example.

Available row adapters are described below.

ColumnMapping

ColumnMapping renames columns. Build one with a dictionary whose keys are adapted column names, and values the column names in the raw row:

// [newA:0, newB:1]
let adapter = ColumnMapping(["newA": "a", "newB": "b"])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!

Note that columns that are not present in the dictionary are not present in the resulting adapted row.

EmptyRowAdapter

EmptyRowAdapter hides all columns.

let adapter = EmptyRowAdapter()
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!
row.isEmpty // true

This limit adapter may turn out useful in some narrow use cases. You'll be happy to find it when you need it.

RangeRowAdapter

RangeRowAdapter only exposes a range of columns.

// [b:1]
let adapter = RangeRowAdapter(1..<2)
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!

RenameColumnAdapter

RenameColumnAdapter lets you transform column names with a function:

// [arrr:0, brrr:1, crrr:2]
let adapter = RenameColumnAdapter { column in column + "rrr" }
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!

This adapter may turn out useful, for example, when subqueries contain duplicated column names:

let sql = "SELECT * FROM (SELECT 1 AS id, 2 AS id)"

// Prints ["id", "id:1"]
// Note the "id:1" column, generated by SQLite.
let row = try Row.fetchOne(db, sql: sql)!
print(Array(row.columnNames))

// Drop the `:...` suffix, and prints ["id", "id"]
let adapter = RenameColumnAdapter { String($0.prefix(while: { $0 != ":" })) }
let adaptedRow = try Row.fetchOne(db, sql: sql, adapter: adapter)!
print(Array(adaptedRow.columnNames))

ScopeAdapter

ScopeAdapter defines row scopes:

let adapter = ScopeAdapter([
    "left": RangeRowAdapter(0..<2),
    "right": RangeRowAdapter(2..<4)])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

ScopeAdapter does not change the columns and values of the fetched row. Instead, it defines scopes, which you access through the Row.scopes property:

row                   // [a:0 b:1 c:2 d:3]
row.scopes["left"]    // [a:0 b:1]
row.scopes["right"]   // [c:2 d:3]
row.scopes["missing"] // nil

Scopes can be nested:

let adapter = ScopeAdapter([
    "left": ScopeAdapter([
        "left": RangeRowAdapter(0..<1),
        "right": RangeRowAdapter(1..<2)]),
    "right": ScopeAdapter([
        "left": RangeRowAdapter(2..<3),
        "right": RangeRowAdapter(3..<4)])
    ])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

let leftRow = row.scopes["left"]!
leftRow.scopes["left"]  // [a:0]
leftRow.scopes["right"] // [b:1]

let rightRow = row.scopes["right"]!
rightRow.scopes["left"]  // [c:2]
rightRow.scopes["right"] // [d:3]

Any adapter can be extended with scopes:

let baseAdapter = RangeRowAdapter(0..<2)
let adapter = ScopeAdapter(base: baseAdapter, scopes: [
    "remainder": SuffixRowAdapter(fromIndex: 2)])
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c, 3 AS d", adapter: adapter)!

row // [a:0 b:1]
row.scopes["remainder"] // [c:2 d:3]

To see how ScopeAdapter can be used, see Joined Queries Support.

SuffixRowAdapter

SuffixRowAdapter hides the first columns in a row:

// [b:1 c:2]
let adapter = SuffixRowAdapter(fromIndex: 1)
let row = try Row.fetchOne(db, sql: "SELECT 0 AS a, 1 AS b, 2 AS c", adapter: adapter)!

Raw SQLite Pointers

If not all SQLite APIs are exposed in GRDB, you can still use the SQLite C Interface and call SQLite C functions.

Those functions are embedded right into the GRDB module, regardless of the underlying SQLite implementation (system SQLite, SQLCipher, or custom SQLite build):

import GRDB

let sqliteVersion = String(cString: sqlite3_libversion())

Raw pointers to database connections and statements are available through the Database.sqliteConnection and Statement.sqliteStatement properties:

try dbQueue.read { db in
    // The raw pointer to a database connection:
    let sqliteConnection = db.sqliteConnection

    // The raw pointer to a statement:
    let statement = try db.makeStatement(sql: "SELECT ...")
    let sqliteStatement = statement.sqliteStatement
}

Note

  • Those pointers are owned by GRDB: don't close connections or finalize statements created by GRDB.
  • GRDB opens SQLite connections in the "multi-thread mode", which (oddly) means that they are not thread-safe. Make sure you touch raw databases and statements inside their dedicated dispatch queues.
  • Use the raw SQLite C Interface at your own risk. GRDB won't prevent you from shooting yourself in the foot.

Records

On top of the SQLite API, GRDB provides protocols and a class that help manipulating database rows as regular objects named "records":

try dbQueue.write { db in
    if var place = try Place.fetchOne(db, id: 1) {
        place.isFavorite = true
        try place.update(db)
    }
}

Of course, you need to open a database connection, and create database tables first.

To define your custom records, you subclass the ready-made Record class, or you extend your structs and classes with protocols that come with focused sets of features: fetching methods, persistence methods, record comparison...

Extending structs with record protocols is more "swifty". Subclassing the Record class is more "classic". You can choose either way. See some examples of record definitions, and the list of record methods for an overview.

Note: if you are familiar with Core Data's NSManagedObject or Realm's Object, you may experience a cultural shock: GRDB records are not uniqued, do not auto-update, and do not lazy-load. This is both a purpose, and a consequence of protocol-oriented programming. You should read How to build an iOS application with SQLite and GRDB.swift for a general introduction.

💡 Tip: after you have read this chapter, check the Good Practices for Designing Record Types Guide.

💡 Tip: see the Demo Applications for sample apps that uses records.

Overview

Protocols and the Record Class

Records in a Glance

Inserting Records

To insert a record in the database, call the insert method:

let player = Player(name: "Arthur", email: "arthur@example.com")
try player.insert(db)

👉 insert is available for subclasses of the Record class, and types that adopt the PersistableRecord protocol.

Fetching Records

To fetch records from the database, call a fetching method:

let arthur = try Player.fetchOne(db,            // Player?
    sql: "SELECT * FROM players WHERE name = ?",
    arguments: ["Arthur"])

let bestPlayers = try Player                    // [Player]
    .order(Column("score").desc)
    .limit(10)
    .fetchAll(db)
    
let spain = try Country.fetchOne(db, id: "ES")  // Country?

👉 Fetching from raw SQL is available for subclasses of the Record class, and types that adopt the FetchableRecord protocol.

👉 Fetching without SQL, using the query interface, is available for subclasses of the Record class, and types that adopt both FetchableRecord and TableRecord protocol.

Updating Records

To update a record in the database, call the update method:

var player: Player = ...
player.score = 1000
try player.update(db)

It is possible to avoid useless updates:

// does not hit the database if score has not changed
try player.updateChanges(db) {
    $0.score = 1000
}

See the query interface for batch updates:

try Player
    .filter(Column("team") == "red")
    .updateAll(db, Column("score") += 1)

👉 update methods are available for subclasses of the Record class, and types that adopt the PersistableRecord protocol. Batch updates are available on the TableRecord protocol.

Deleting Records

To delete a record in the database, call the delete method:

let player: Player = ...
try player.delete(db)

You can also delete by primary key, unique key, or perform batch deletes (see Delete Requests):

try Player.deleteOne(db, id: 1)
try Player.deleteOne(db, key: ["email": "arthur@example.com"])
try Country.deleteAll(db, ids: ["FR", "US"])
try Player
    .filter(Column("email") == nil)
    .deleteAll(db)

👉 delete methods are available for subclasses of the Record class, and types that adopt the PersistableRecord protocol. Batch deletes are available on the TableRecord protocol.

Counting Records

To count records, call the fetchCount method:

let playerCount: Int = try Player.fetchCount(db)

let playerWithEmailCount: Int = try Player
    .filter(Column("email") == nil)
    .fetchCount(db)

👉 fetchCount is available for subclasses of the Record class, and types that adopt the TableRecord protocol.

Details follow:

Record Protocols Overview

GRDB ships with three record protocols. Your own types will adopt one or several of them, according to the abilities you want to extend your types with.

  • FetchableRecord is able to decode database rows.

    struct Place: FetchableRecord { ... }
    let places = try dbQueue.read { db in
        try Place.fetchAll(db, sql: "SELECT * FROM place")
    }

    💡 Tip: FetchableRecord can derive its implementation from the standard Decodable protocol. See Codable Records for more information.

    FetchableRecord can decode database rows, but it is not able to build SQL requests for you. For that, you also need TableRecord:

  • TableRecord is able to generate SQL queries:

    struct Place: TableRecord { ... }
    let placeCount = try dbQueue.read { db in
        // Generates and runs `SELECT COUNT(*) FROM place`
        try Place.fetchCount(db)
    }

    When a type adopts both TableRecord and FetchableRecord, it can load from those requests:

    struct Place: TableRecord, FetchableRecord { ... }
    try dbQueue.read { db in
        let places = try Place.order(Column("title")).fetchAll(db)
        let paris = try Place.fetchOne(id: 1)
    }
  • PersistableRecord is able to write: it can create, update, and delete rows in the database:

    struct Place : PersistableRecord { ... }
    try dbQueue.write { db in
        try Place.delete(db, id: 1)
        try Place(...).insert(db)
    }

    A persistable record can also compare itself against other records, and avoid useless database updates.

    💡 Tip: PersistableRecord can derive its implementation from the standard Encodable protocol. See Codable Records for more information.

FetchableRecord Protocol

The FetchableRecord protocol grants fetching methods to any type that can be built from a database row:

protocol FetchableRecord {
    /// Row initializer
    init(row: Row) throws
}

To use FetchableRecord, subclass the Record class, or adopt it explicitly. For example:

struct Place {
    var id: Int64?
    var title: String
    var coordinate: CLLocationCoordinate2D
}

extension Place : FetchableRecord {
    init(row: Row) {
        id = row["id"]
        title = row["title"]
        coordinate = CLLocationCoordinate2D(
            latitude: row["latitude"],
            longitude: row["longitude"])
    }
}

Rows also accept column enums:

extension Place : FetchableRecord {
    enum Columns: String, ColumnExpression {
        case id, title, latitude, longitude
    }
    
    init(row: Row) {
        id = row[Columns.id]
        title = row[Columns.title]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
    }
}

See column values for more information about the row[] subscript.

When your record type adopts the standard Decodable protocol, you don't have to provide the implementation for init(row:). See Codable Records for more information:

// That's all
struct Player: Decodable, FetchableRecord {
    var id: Int64
    var name: String
    var score: Int
}

FetchableRecord allows adopting types to be fetched from SQL queries:

try Place.fetchCursor(db, sql: "SELECT ...", arguments:...) // A Cursor of Place
try Place.fetchAll(db, sql: "SELECT ...", arguments:...)    // [Place]
try Place.fetchSet(db, sql: "SELECT ...", arguments:...)    // Set<Place>
try Place.fetchOne(db, sql: "SELECT ...", arguments:...)    // Place?

See fetching methods for information about the fetchCursor, fetchAll, fetchSet and fetchOne methods. See StatementArguments for more information about the query arguments.

Note: for performance reasons, the same row argument to init(row:) is reused during the iteration of a fetch query. If you want to keep the row for later use, make sure to store a copy: self.row = row.copy().

Note: The FetchableRecord.init(row:) initializer fits the needs of most applications. But some application are more demanding than others. When FetchableRecord does not exactly provide the support you need, have a look at the Beyond FetchableRecord chapter.

TableRecord Protocol

The TableRecord protocol generates SQL for you. To use TableRecord, subclass the Record class, or adopt it explicitly:

protocol TableRecord {
    static var databaseTableName: String { get }
    static var databaseSelection: [any SQLSelectable] { get }
}

The databaseSelection type property is optional, and documented in the Columns Selected by a Request chapter.

The databaseTableName type property is the name of a database table. By default, it is derived from the type name:

struct Place: TableRecord { }
print(Place.databaseTableName) // prints "place"

For example:

  • Place: place
  • Country: country
  • PostalAddress: postalAddress
  • HTTPRequest: httpRequest
  • TOEFL: toefl

You can still provide a custom table name:

struct Place: TableRecord {
    static let databaseTableName = "location"
}
print(Place.databaseTableName) // prints "location"

Subclasses of the Record class must always override their superclass's databaseTableName property:

class Place: Record {
    override class var databaseTableName: String { "place" }
}
print(Place.databaseTableName) // prints "place"

When a type adopts both TableRecord and FetchableRecord, it can be fetched using the query interface:

// SELECT * FROM place WHERE name = 'Paris'
let paris = try Place.filter(nameColumn == "Paris").fetchOne(db)

TableRecord can also fetch deal with primary and unique keys: see Fetching by Key and Testing for Record Existence.

PersistableRecord Protocol

GRDB record types can create, update, and delete rows in the database.

Those abilities are granted by three protocols:

// Defines how a record encodes itself into the database
protocol EncodableRecord {
    /// Defines the values persisted in the database
    func encode(to container: inout PersistenceContainer) throws
}

// Adds persistence methods
protocol MutablePersistableRecord: TableRecord, EncodableRecord {
    /// Optional method that lets your adopting type store its rowID upon
    /// successful insertion. Don't call it directly: it is called for you.
    mutating func didInsert(_ inserted: InsertionSuccess)
}

// Adds immutability
protocol PersistableRecord: MutablePersistableRecord {
    /// Non-mutating version of the optional didInsert(_:)
    func didInsert(_ inserted: InsertionSuccess)
}

Yes, three protocols instead of one. Here is how you pick one or the other:

  • If your type is a class, choose PersistableRecord. On top of that, implement didInsert(_:) if the database table has an auto-incremented primary key.

  • If your type is a struct, and the database table has an auto-incremented primary key, choose MutablePersistableRecord, and implement didInsert(_:).

  • Otherwise, choose PersistableRecord, and ignore didInsert(_:).

The encode(to:) method defines which values (Bool, Int, String, Date, Swift enums, etc.) are assigned to database columns.

The optional didInsert method lets the adopting type store its rowID after successful insertion, and is only useful for tables that have an auto-incremented primary key. It is called from a protected dispatch queue, and serialized with all database updates.

To use the persistable protocols, subclass the Record class, or adopt one of them explicitly. For example:

extension Place : MutablePersistableRecord {
    /// The values persisted in the database
    func encode(to container: inout PersistenceContainer) {
        container["id"] = id
        container["title"] = title
        container["latitude"] = coordinate.latitude
        container["longitude"] = coordinate.longitude
    }
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

var paris = Place(
    id: nil,
    title: "Paris",
    coordinate: CLLocationCoordinate2D(latitude: 48.8534100, longitude: 2.3488000))

try paris.insert(db)
paris.id   // some value

Persistence containers also accept column enums:

extension Place : MutablePersistableRecord {
    enum Columns: String, ColumnExpression {
        case id, title, latitude, longitude
    }
    
    func encode(to container: inout PersistenceContainer) {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
}

When your record type adopts the standard Encodable protocol, you don't have to provide the implementation for encode(to:). See Codable Records for more information:

// That's all
struct Player: Encodable, MutablePersistableRecord {
    var id: Int64?
    var name: String
    var score: Int
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

Persistence Methods

Record subclasses and types that adopt PersistableRecord are given methods that insert, update, and delete:

// INSERT
try place.insert(db)
let insertedPlace = try place.inserted(db) // non-mutating

// UPDATE
try place.update(db)
try place.update(db, columns: ["title"])

// Maybe UPDATE
try place.updateChanges(db, from: otherPlace)
try place.updateChanges(db) { $0.isFavorite = true }
try place.updateChanges(db) // Record class only

// INSERT or UPDATE
try place.save(db)
let savedPlace = place.saved(db) // non-mutating

// UPSERT
try place.upsert(db)
let insertedPlace = place.upsertAndFetch(db)

// DELETE
try place.delete(db)

// EXISTENCE CHECK
let exists = try place.exists(db)

See Upsert below for more information about upserts.

The TableRecord protocol comes with batch operations:

// UPDATE
try Place.updateAll(db, ...)

// DELETE
try Place.deleteAll(db)
try Place.deleteAll(db, ids:...)
try Place.deleteAll(db, keys:...)
try Place.deleteOne(db, id:...)
try Place.deleteOne(db, key:...)

For more information about batch updates, see Update Requests.

  • All persistence methods can throw a DatabaseError.

  • update and updateChanges throw PersistenceError if the database does not contain any row for the primary key of the record.

  • save makes sure your values are stored in the database. It performs an UPDATE if the record has a non-null primary key, and then, if no row was modified, an INSERT. It directly performs an INSERT if the record has no primary key, or a null primary key.

  • delete and deleteOne returns whether a database row was deleted or not. deleteAll returns the number of deleted rows. updateAll returns the number of updated rows. updateChanges returns whether a database row was updated or not.

All primary keys are supported, including composite primary keys that span several columns, and the implicit rowid primary key.

To customize persistence methods, you provide Persistence Callbacks, described below. Do not attempt at overriding the ready-made persistence methods.

Upsert

UPSERT is an SQLite feature that causes an INSERT to behave as an UPDATE or a no-op if the INSERT would violate a uniqueness constraint (primary key or unique index).

Note: Upsert apis are available from SQLite 3.35.0+: iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, or with a custom SQLite build or SQLCipher.

Note: With regard to persistence callbacks, an upsert behaves exactly like an insert. In particular: the aroundInsert(_:) and didInsert(_:) callbacks reports the rowid of the inserted or updated row; willUpdate, aroundUdate, didUdate are not called.

PersistableRecord provides three upsert methods:

  • upsert(_:)

    Inserts or updates a record.

    The upsert behavior is triggered by a violation of any uniqueness constraint on the table (primary key or unique index). In case of conflict, all columns but the primary key are overwritten with the inserted values:

    struct Player: Encodable, PersistableRecord {
        var id: Int64
        var name: String
        var score: Int
    }
    
    // INSERT INTO player (id, name, score)
    // VALUES (1, 'Arthur', 1000)
    // ON CONFLICT DO UPDATE SET
    //   name = excluded.name,
    //   score = excluded.score
    let player = Player(id: 1, name: "Arthur", score: 1000)
    try player.upsert(db)
  • upsertAndFetch(_:onConflict:doUpdate:) (requires FetchableRecord conformance)

    Inserts or updates a record, and returns the upserted record.

    The onConflict and doUpdate arguments let you further control the upsert behavior. Make sure you check the SQLite UPSERT documentation for detailed information.

    • onConflict: the "conflict target" is the array of columns in the uniqueness constraint (primary key or unique index) that triggers the upsert.

      If empty (the default), all uniqueness constraint are considered.

    • doUpdate: a closure that returns columns assignments to perform in case of conflict. Other columns are overwritten with the inserted values.

      By default, all inserted columns but the primary key and the conflict target are overwritten.

    In the example below, we upsert the new vocabulary word "jovial". It is inserted if that word is not already in the dictionary. Otherwise, count is incremented, isTainted is not overwritten, and kind is overwritten:

    // CREATE TABLE vocabulary(
    //   word TEXT NOT NULL PRIMARY KEY,
    //   kind TEXT NOT NULL,
    //   isTainted BOOLEAN DEFAULT 0,
    //   count INT DEFAULT 1))
    struct Vocabulary: Encodable, PersistableRecord {
        var word: String
        var kind: String
        var isTainted: Bool
    }
    
    // INSERT INTO vocabulary(word, kind, isTainted)
    // VALUES('jovial', 'adjective', 0)
    // ON CONFLICT(word) DO UPDATE SET \
    //   count = count + 1,   -- on conflict, count is incremented
    //   kind = excluded.kind -- on conflict, kind is overwritten
    // RETURNING *
    let vocabulary = Vocabulary(word: "jovial", kind: "adjective", isTainted: false)
    let upserted = try vocabulary.upsertAndFetch(
        db, onConflict: ["word"],
        doUpdate: { _ in
            [Column("count") += 1,            // on conflict, count is incremented
             Column("isTainted").noOverwrite] // on conflict, isTainted is NOT overwritten
        })

    The doUpdate closure accepts an excluded TableAlias argument that refers to the inserted values that trigger the conflict. You can use it to specify an explicit overwrite, or to perform a computation. In the next example, the upsert keeps the maximum date in case of conflict:

    // INSERT INTO message(id, text, date)
    // VALUES(...)
    // ON CONFLICT DO UPDATE SET \
    //   text = excluded.text,
    //   date = MAX(date, excluded.date)
    // RETURNING *
    let upserted = try message.upsertAndFetch(doUpdate: { excluded in
        // keep the maximum date in case of conflict
        [Column("date").set(to: max(Column("date"), excluded["date"]))]
    })
  • upsertAndFetch(_:as:onConflict:doUpdate:) (does not require FetchableRecord conformance)

    This method is identical to upsertAndFetch(_:onConflict:doUpdate:) described above, but you can provide a distinct FetchableRecord record type as a result, in order to specify the returned columns.

Persistence Methods and the RETURNING clause

SQLite is able to return values from a inserted, updated, or deleted row, with the RETURNING clause.

Note: Support for the RETURNING clause is available from SQLite 3.35.0+: iOS 15.0+, macOS 12.0+, tvOS 15.0+, watchOS 8.0+, or with a custom SQLite build or SQLCipher.

The RETURNING clause helps dealing with database features such as auto-incremented ids, default values, and generated columns. You can, for example, insert a few columns and fetch the default or generated ones in one step.

GRDB uses the RETURNING clause in all persistence methods that contain AndFetch in their name.

For example, given a database table with an auto-incremented primary key and a default score:

try dbQueue.write { db in
    try db.execute(sql: """
        CREATE TABLE player(
          id INTEGER PRIMARY KEY AUTOINCREMENT,
          name TEXT NOT NULL,
          score INTEGER NOT NULL DEFAULT 1000)
        """)
}

You can define a record type with full database information, and another partial record type that deals with a subset of columns:

// A player with full database information
struct Player: Codable, PersistableRecord, FetchableRecord {
    var id: Int64
    var name: String
    var score: Int
}

// A partial player
struct PartialPlayer: Encodable, PersistableRecord {
    static let databaseTableName = "player"
    var name: String
}

And now you can get a full player by inserting a partial one:

try dbQueue.write { db in
    let partialPlayer = PartialPlayer(name: "Alice")
    
    // INSERT INTO player (name) VALUES ('Alice') RETURNING *
    if let player = try partialPlayer.insertAndFetch(db, as: Player.self) {
        print(player.id)    // The inserted id
        print(player.name)  // The inserted name
        print(player.score) // The default score
    }
}

For extra precision, you can select only the columns you need, and fetch the desired value from the provided prepared statement:

try dbQueue.write { db in
    let partialPlayer = PartialPlayer(name: "Alice")
    
    // INSERT INTO player (name) VALUES ('Alice') RETURNING score
    let score = try partialPlayer.insertAndFetch(db, selection: [Column("score")]) { statement in
        try Int.fetchOne(statement)
    }
    print(score) // Prints 1000, the default score
}

There are other similar persistence methods, such as upsertAndFetch, saveAndFetch, updateAndFetch, updateChangesAndFetch, etc. They all behave like upsert, save, update, updateChanges, except that they return saved values. For example:

// Save and return the saved player
let savedPlayer = try player.saveAndFetch(db)

See Persistence Methods, Upsert, and updateChanges methods for more information.

Batch operations can return updated or deleted values:

Warning: Make sure you check the documentation of the RETURNING clause, which describes important limitations and caveats for batch operations.

let request = Player.filter(...)...

// Fetch all deleted players
// DELETE FROM player RETURNING *
let deletedPlayers = try request.deleteAndFetchAll(db) // [Player]

// Fetch a selection of columns from the deleted rows
// DELETE FROM player RETURNING name
let statement = try request.deleteAndFetchStatement(db, selection: [Column("name")])
let deletedNames = try String.fetchSet(statement)

// Fetch all updated players
// UPDATE player SET score = score + 10 RETURNING *
let updatedPlayers = try request.updateAndFetchAll(db, [Column("score") += 10]) // [Player]

// Fetch a selection of columns from the updated rows
// UPDATE player SET score = score + 10 RETURNING score
let statement = try request.updateAndFetchStatement(
    db, [Column("score") += 10],
    select: [Column("score")])
let updatedScores = try Int.fetchAll(statement)

Persistence Callbacks

Your custom type may want to perform extra work when the persistence methods are invoked.

To this end, your record type can implement persistence callbacks. Callbacks are methods that get called at certain moments of a record's life cycle. With callbacks it is possible to write code that will run whenever an record is inserted, updated, or deleted.

In order to use a callback method, you need to provide its implementation. For example, a frequently used callback is didInsert, in the case of auto-incremented database ids:

struct Player: MutablePersistableRecord {
    var id: Int64?
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}

try dbQueue.write { db in
    var player = Player(id: nil, ...)
    try player.insert(db)
    print(player.id) // didInsert was called: prints some non-nil id
}

When you subclass the Record class, override the callback, and make sure you call super at some point of your implementation:

class Player: Record {
    var id: Int64?
    
    // Update auto-incremented id upon successful insertion
    func didInsert(_ inserted: InsertionSuccess) {
        super.didInsert(inserted)
        id = inserted.rowID
    }
}

Callbacks can also help implementing record validation:

struct Link: PersistableRecord {
    var url: URL
    
    func willSave(_ db: Database) throws {
        if url.host == nil {
            throw ValidationError("url must be absolute.")
        }
    }
}

try link.insert(db) // Calls the willSave callback
try link.update(db) // Calls the willSave callback
try link.save(db)   // Calls the willSave callback
try link.upsert(db) // Calls the willSave callback

Available Callbacks

Here is a list with all the available persistence callbacks, listed in the same order in which they will get called during the respective operations:

  • Inserting a record (all record.insert and record.upsert methods)

    • willSave
    • aroundSave
    • willInsert
    • aroundInsert
    • didInsert
    • didSave
  • Updating a record (all record.update methods)

    • willSave
    • aroundSave
    • willUpdate
    • aroundUpdate
    • didUpdate
    • didSave
  • Deleting a record (only the record.delete(_:) method)

    • willDelete
    • aroundDelete
    • didDelete

For detailed information about each callback, check the reference.

In the MutablePersistableRecord protocol, willInsert and didInsert are mutating methods. In PersistableRecord, they are not mutating.

Note: The record.save(_:) method performs an UPDATE if the record has a non-null primary key, and then, if no row was modified, an INSERT. It directly performs an INSERT if the record has no primary key, or a null primary key. It triggers update and/or insert callbacks accordingly.

Warning: Callbacks are only invoked from persistence methods called on record instances. Callbacks are not invoked when you call a type method, perform a batch operations, or execute raw SQL.

Warning: When a did*** callback is invoked, do not assume that the change is actually persisted on disk, because the database may still be inside an uncommitted transaction. When you need to handle transaction completions, use the Transaction Hook. For example:

struct PictureFile: PersistableRecord {
    var path: String
    
    func willDelete(_ db: Database) {
        db.afterNextTransaction { _ in
            try? deleteFileOnDisk()
        }
    }
}

Identifiable Records

When a record type maps a table with a single-column primary key, it is recommended to have it adopt the standard Identifiable protocol.

struct Player: Identifiable, FetchableRecord, PersistableRecord {
    var id: Int64 // fulfills the Identifiable requirement
    var name: String
    var score: Int
}

When id has a database-compatible type (Int64, Int, String, UUID, ...), the Identifiable conformance unlocks type-safe record and request methods:

let player = try Player.fetchOne(db, id: 1)
let players = try Player.fetchAll(db, ids: [1, 2, 3])
let players = try Player.fetchSet(db, ids: [1, 2, 3])

let request = Player.filter(id: 1)
let request = Player.filter(ids: [1, 2, 3])

try Player.deleteOne(db, id: 1)
try Player.deleteAll(db, ids: [1, 2, 3])

Note: Identifiable is not available on all application targets, and not all tables have a single-column primary key. GRDB provides other methods that deal with primary and unique keys, but they won't check the type of their arguments:

// Those methods are not type-checked
try Player.fetchOne(db, key: 1)
try Player.fetchOne(db, key: ["email": "arthur@example.com"])
try Country.fetchAll(db, keys: ["FR", "US"])
try Citizenship.fetchOne(db, key: ["citizenId": 1, "countryCode": "FR"])

let request = Player.filter(key: 1)
let request = Player.filter(keys: [1, 2, 3])

try Player.deleteOne(db, key: 1)
try Player.deleteAll(db, keys: [1, 2, 3])

Some database tables have a single-column primary key which is not called "id":

try db.create(table: "country") { t in
    t.column("isoCode", .text).notNull().primaryKey()
    t.column("name", .text).notNull()
    t.column("population", .integer).notNull()
}

In this case, Identifiable conformance can be achieved, for example, by returning the primary key column from the id property:

struct Country: Identifiable, FetchableRecord, PersistableRecord {
    var isoCode: String
    var name: String
    var population: Int
    
    // Fulfill the Identifiable requirement
    var id: String { isoCode }
}

let france = try dbQueue.read { db in
    try Country.fetchOne(db, id: "FR")
}

Codable Records

Record types that adopt an archival protocol (Codable, Encodable or Decodable) get free database support just by declaring conformance to the desired record protocols:

// Declare a record...
struct Player: Codable, FetchableRecord, PersistableRecord {
    var name: String
    var score: Int
}

// ...and there you go:
try dbQueue.write { db in
    try Player(name: "Arthur", score: 100).insert(db)
    let players = try Player.fetchAll(db)
}

Codable records encode and decode their properties according to their own implementation of the Encodable and Decodable protocols. Yet databases have specific requirements:

  • Properties are always coded according to their preferred database representation, when they have one (all values that adopt the DatabaseValueConvertible protocol).
  • You can customize the encoding and decoding of dates and uuids.
  • Complex properties (arrays, dictionaries, nested structs, etc.) are stored as JSON.

For more information about Codable records, see:

💡 Tip: see the Demo Applications for sample code that uses Codable records.

JSON Columns

When a Codable record contains a property that is not a simple value (Bool, Int, String, Date, Swift enums, etc.), that value is encoded and decoded as a JSON string. For example:

enum AchievementColor: String, Codable {
    case bronze, silver, gold
}

struct Achievement: Codable {
    var name: String
    var color: AchievementColor
}

struct Player: Codable, FetchableRecord, PersistableRecord {
    var name: String
    var score: Int
    var achievements: [Achievement] // stored in a JSON column
}

try dbQueue.write { db in
    // INSERT INTO player (name, score, achievements)
    // VALUES (
    //   'Arthur',
    //   100,
    //   '[{"color":"gold","name":"Use Codable Records"}]')
    let achievement = Achievement(name: "Use Codable Records", color: .gold)
    let player = Player(name: "Arthur", score: 100, achievements: [achievement])
    try player.insert(db)
}

GRDB uses the standard JSONDecoder and JSONEncoder from Foundation. By default, Data values are handled with the .base64 strategy, Date with the .millisecondsSince1970 strategy, and non conforming floats with the .throw strategy.

You can customize the JSON format by implementing those methods:

protocol FetchableRecord {
    static func databaseJSONDecoder(for column: String) -> JSONDecoder
}

protocol EncodableRecord {
    static func databaseJSONEncoder(for column: String) -> JSONEncoder
}

💡 Tip: Make sure you set the JSONEncoder sortedKeys option. This option makes sure that the JSON output is stable. This stability is required for Record Comparison to work as expected, and database observation tools such as ValueObservation to accurately recognize changed records.

Column Names Coding Strategies

By default, Codable Records store their values into database columns that match their coding keys: the teamID property is stored into the teamID column.

This behavior can be overridden, so that you can, for example, store the teamID property into the team_id column:

protocol FetchableRecord {
    static var databaseColumnDecodingStrategy: DatabaseColumnDecodingStrategy { get }
}

protocol EncodableRecord {
    static var databaseColumnEncodingStrategy: DatabaseColumnEncodingStrategy { get }
}

See DatabaseColumnDecodingStrategy and DatabaseColumnEncodingStrategy to learn about all available strategies.

Date and UUID Coding Strategies

By default, Codable Records encode and decode their Date and UUID properties as described in the general Date and DateComponents and UUID chapters.

To sum up: dates encode themselves in the "YYYY-MM-DD HH:MM:SS.SSS" format, in the UTC time zone, and decode a variety of date formats and timestamps. UUIDs encode themselves as 16-bytes data blobs, and decode both 16-bytes data blobs and strings such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F".

Those behaviors can be overridden:

protocol FetchableRecord {
    static var databaseDateDecodingStrategy: DatabaseDateDecodingStrategy { get }
}

protocol EncodableRecord {
    static var databaseDateEncodingStrategy: DatabaseDateEncodingStrategy { get }
    static var databaseUUIDEncodingStrategy: DatabaseUUIDEncodingStrategy { get }
}

See DatabaseDateDecodingStrategy, DatabaseDateEncodingStrategy, and DatabaseUUIDEncodingStrategy to learn about all available strategies.

There is no customization of uuid decoding, because UUID can already decode all its encoded variants (16-bytes blobs and uuid strings, both uppercase and lowercase).

Customized date and uuid handling apply:

  • When encoding and decoding database rows to and from records (fetching and persistence methods).
  • In requests by single-column primary key: fetchOne(_:id:), filter(id:), deleteAll(_:keys:), etc.

They do not apply in other requests based on date or uuid values.

So make sure that dates and uuids are properly encoded in your requests. For example:

struct Player: Codable, FetchableRecord, PersistableRecord, Identifiable {
    // UUIDs are stored as strings
    static let databaseUUIDEncodingStrategy = DatabaseUUIDEncodingStrategy.uppercaseString
    var id: UUID
    ...
}

try dbQueue.write { db in
    let uuid = UUID()
    let player = Player(id: uuid, ...)
    
    // OK: inserts a player in the database, with a string uuid
    try player.insert(db)
    
    // OK: performs a string-based query, finds the inserted player
    _ = try Player.filter(id: uuid).fetchOne(db)

    // NOT OK: performs a blob-based query, fails to find the inserted player
    _ = try Player.filter(Column("id") == uuid).fetchOne(db)
    
    // OK: performs a string-based query, finds the inserted player
    _ = try Player.filter(Column("id") == uuid.uuidString).fetchOne(db)
}

The userInfo Dictionary

Your Codable Records can be stored in the database, but they may also have other purposes. In this case, you may need to customize their implementations of Decodable.init(from:) and Encodable.encode(to:), depending on the context.

The standard way to provide such context is the userInfo dictionary. Implement those properties:

protocol FetchableRecord {
    static var databaseDecodingUserInfo: [CodingUserInfoKey: Any] { get }
}

protocol EncodableRecord {
    static var databaseEncodingUserInfo: [CodingUserInfoKey: Any] { get }
}

For example, here is a Player type that customizes its decoding:

// A key that holds a decoder's name
let decoderName = CodingUserInfoKey(rawValue: "decoderName")!

struct Player: FetchableRecord, Decodable {
    init(from decoder: Decoder) throws {
        // Print the decoder name
        let decoderName = decoder.userInfo[decoderName] as? String
        print("Decoded from \(decoderName ?? "unknown decoder")")
        ...
    }
}

You can have a specific decoding from JSON...

// prints "Decoded from JSON"
let decoder = JSONDecoder()
decoder.userInfo = [decoderName: "JSON"]
let player = try decoder.decode(Player.self, from: jsonData)

... and another one from database rows:

extension Player: FetchableRecord {
    static let databaseDecodingUserInfo: [CodingUserInfoKey: Any] = [decoderName: "database row"]
}

// prints "Decoded from database row"
let player = try Player.fetchOne(db, ...)

Note: make sure the databaseDecodingUserInfo and databaseEncodingUserInfo properties are explicitly declared as [CodingUserInfoKey: Any]. If they are not, the Swift compiler may silently miss the protocol requirement, resulting in sticky empty userInfo.

Tip: Derive Columns from Coding Keys

Codable types are granted with a CodingKeys enum. You can use them to safely define database columns:

struct Player: Codable {
    var id: Int64
    var name: String
    var score: Int
}

extension Player: FetchableRecord, PersistableRecord {
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let name = Column(CodingKeys.name)
        static let score = Column(CodingKeys.score)
    }
}

See the query interface and Good Practices for Designing Record Types for further information.

Record Class

Record is a class that is designed to be subclassed. It inherits its features from the FetchableRecord, TableRecord, and PersistableRecord protocols. On top of that, Record instances can compare against previous versions of themselves in order to avoid useless updates.

Record subclasses define their custom database relationship by overriding database methods. For example:

class Place: Record {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
    
    init(id: Int64?, title: String, isFavorite: Bool, coordinate: CLLocationCoordinate2D) {
        self.id = id
        self.title = title
        self.isFavorite = isFavorite
        self.coordinate = coordinate
        super.init()
    }
    
    /// The table name
    override class var databaseTableName: String { "place" }
    
    /// The table columns
    enum Columns: String, ColumnExpression {
        case id, title, favorite, latitude, longitude
    }
    
    /// Creates a record from a database row
    required init(row: Row) throws {
        id = row[Columns.id]
        title = row[Columns.title]
        isFavorite = row[Columns.favorite]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
        try super.init(row: row)
    }
    
    /// The values persisted in the database
    override func encode(to container: inout PersistenceContainer) throws {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.favorite] = isFavorite
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
    
    /// Update record ID after a successful insertion
    override func didInsert(_ inserted: InsertionSuccess) {
        super.didInsert(inserted)
        id = inserted.rowID
    }
}

Record Comparison

Records that adopt the EncodableRecord protocol can compare against other records, or against previous versions of themselves.

This helps avoiding costly UPDATE statements when a record has not been edited.

The updateChanges Methods

The updateChanges methods perform a database update of the changed columns only (and does nothing if record has no change).

  • updateChanges(_:from:)

    This method lets you compare two records:

    if let oldPlayer = try Player.fetchOne(db, id: 42) {
        var newPlayer = oldPlayer
        newPlayer.score = 100
        if try newPlayer.updateChanges(db, from: oldPlayer) {
            print("player was modified, and updated in the database")
        } else {
            print("player was not modified, and database was not hit")
        }
    }
  • updateChanges(_:modify:)

    This method lets you update a record in place:

    if var player = try Player.fetchOne(db, id: 42) {
        let modified = try player.updateChanges(db) {
            $0.score = 100
        }
        if modified {
            print("player was modified, and updated in the database")
        } else {
            print("player was not modified, and database was not hit")
        }
    }
  • updateChanges(_:) (Record class only)

    Instances of the Record class are able to compare against themselves, and know if they have changes that have not been saved since the last fetch or saving:

    // Record class only
    if let player = try Player.fetchOne(db, id: 42) {
        player.score = 100
        if try player.updateChanges(db) {
            print("player was modified, and updated in the database")
        } else {
            print("player was not modified, and database was not hit")
        }
    }

The databaseEquals Method

This method returns whether two records have the same database representation:

let oldPlayer: Player = ...
var newPlayer: Player = ...
if newPlayer.databaseEquals(oldPlayer) == false {
    try newPlayer.save(db)
}

Note: The comparison is performed on the database representation of records. As long as your record type adopts the EncodableRecord protocol, you don't need to care about Equatable.

The databaseChanges and hasDatabaseChanges Methods

databaseChanges(from:) returns a dictionary of differences between two records:

let oldPlayer = Player(id: 1, name: "Arthur", score: 100)
let newPlayer = Player(id: 1, name: "Arthur", score: 1000)
for (column, oldValue) in try newPlayer.databaseChanges(from: oldPlayer) {
    print("\(column) was \(oldValue)")
}
// prints "score was 100"

The Record class is able to compare against itself:

// Record class only
let player = Player(id: 1, name: "Arthur", score: 100)
try player.insert(db)
player.score = 1000
for (column, oldValue) in try player.databaseChanges {
    print("\(column) was \(oldValue)")
}
// prints "score was 100"

Record instances also have a hasDatabaseChanges property:

// Record class only
player.score = 1000
if player.hasDatabaseChanges {
    try player.save(db)
}

Record.hasDatabaseChanges is false after a Record instance has been fetched or saved into the database. Subsequent modifications may set it, or not: hasDatabaseChanges is based on value comparison. Setting a property to the same value does not set the changed flag:

let player = Player(name: "Barbara", score: 750)
player.hasDatabaseChanges  // true

try player.insert(db)
player.hasDatabaseChanges  // false

player.name = "Barbara"
player.hasDatabaseChanges  // false

player.score = 1000
player.hasDatabaseChanges  // true
try player.databaseChanges // ["score": 750]

For an efficient algorithm which synchronizes the content of a database table with a JSON payload, check groue/SortedDifference.

Record Customization Options

GRDB records come with many default behaviors, that are designed to fit most situations. Many of those defaults can be customized for your specific needs:

Codable Records have a few extra options:

Conflict Resolution

Insertions and updates can create conflicts: for example, a query may attempt to insert a duplicate row that violates a unique index.

Those conflicts normally end with an error. Yet SQLite let you alter the default behavior, and handle conflicts with specific policies. For example, the INSERT OR REPLACE statement handles conflicts with the "replace" policy which replaces the conflicting row instead of throwing an error.

The five different policies are: abort (the default), replace, rollback, fail, and ignore.

SQLite let you specify conflict policies at two different places:

  • In the definition of the database table:

    // CREATE TABLE player (
    //     id INTEGER PRIMARY KEY AUTOINCREMENT,
    //     email TEXT UNIQUE ON CONFLICT REPLACE
    // )
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("email", .text).unique(onConflict: .replace) // <--
    }
    
    // Despite the unique index on email, both inserts succeed.
    // The second insert replaces the first row:
    try db.execute(sql: "INSERT INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])
    try db.execute(sql: "INSERT INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])
  • In each modification query:

    // CREATE TABLE player (
    //     id INTEGER PRIMARY KEY AUTOINCREMENT,
    //     email TEXT UNIQUE
    // )
    try db.create(table: "player") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("email", .text).unique()
    }
    
    // Again, despite the unique index on email, both inserts succeed.
    try db.execute(sql: "INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])
    try db.execute(sql: "INSERT OR REPLACE INTO player (email) VALUES (?)", arguments: ["arthur@example.com"])

When you want to handle conflicts at the query level, specify a custom persistenceConflictPolicy in your type that adopts the PersistableRecord protocol. It will alter the INSERT and UPDATE queries run by the insert, update and save persistence methods:

protocol MutablePersistableRecord {
    /// The policy that handles SQLite conflicts when records are
    /// inserted or updated.
    ///
    /// This property is optional: its default value uses the ABORT
    /// policy for both insertions and updates, so that GRDB generate
    /// regular INSERT and UPDATE queries.
    static var persistenceConflictPolicy: PersistenceConflictPolicy { get }
}

struct Player : MutablePersistableRecord {
    static let persistenceConflictPolicy = PersistenceConflictPolicy(
        insert: .replace,
        update: .replace)
}

// INSERT OR REPLACE INTO player (...) VALUES (...)
try player.insert(db)

Note: If you specify the ignore policy for inserts, the didInsert callback will be called with some random id in case of failed insert. You can detect failed insertions with insertAndFetch:

// How to detect failed `INSERT OR IGNORE`:
// INSERT OR IGNORE INTO player ... RETURNING *
if let insertedPlayer = try player.insertAndFetch(db) {
    // Succesful insertion
} else {
    // Ignored failure
}

Note: The replace policy may have to delete rows so that inserts and updates can succeed. Those deletions are not reported to transaction observers (this might change in a future release of SQLite).

The Implicit RowID Primary Key

All SQLite tables have a primary key. Even when the primary key is not explicit:

// No explicit primary key
try db.create(table: "event") { t in
    t.column("message", .text)
    t.column("date", .datetime)
}

// No way to define an explicit primary key
try db.create(virtualTable: "book", using: FTS4()) { t in
    t.column("title")
    t.column("author")
    t.column("body")
}

The implicit primary key is stored in the hidden column rowid. Hidden means that SELECT * does not select it, and yet it can be selected and queried: SELECT *, rowid ... WHERE rowid = 1.

Some GRDB methods will automatically use this hidden column when a table has no explicit primary key:

// SELECT * FROM event WHERE rowid = 1
let event = try Event.fetchOne(db, id: 1)

// DELETE FROM book WHERE rowid = 1
try Book.deleteOne(db, id: 1)

Exposing the RowID Column

By default, a record type that wraps a table without any explicit primary key doesn't know about the hidden rowid column.

Without primary key, records don't have any identity, and the persistence method can behave in undesired fashion: update() throws errors, save() always performs insertions and may break constraints, exists() is always false.

When SQLite won't let you provide an explicit primary key (as in full-text tables, for example), you may want to make your record type fully aware of the hidden rowid column:

  1. Have the databaseSelection static property (from the TableRecord protocol) return the hidden rowid column:

    struct Event : TableRecord {
        static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID]
    }
    
    // When you subclass Record, you need an override:
    class Book : Record {
        override class var databaseSelection: [any SQLSelectable] {
            [AllColumns(), Column.rowID]
        }
    }

    GRDB will then select the rowid column by default:

    // SELECT *, rowid FROM event
    let events = try Event.fetchAll(db)
  2. Have init(row:) from the FetchableRecord protocol consume the "rowid" column:

    struct Event : FetchableRecord {
        var id: Int64?
        
        init(row: Row) {
            id = row[Column.rowID] // or `row[.rowID]` with Swift 5.5+
        }
    }

    Your fetched records will then know their ids:

    let event = try Event.fetchOne(db)!
    event.id // some value
  3. Encode the rowid in encode(to:), and keep it in the didInsert(_:) callback (both from the PersistableRecord and MutablePersistableRecord protocols):

    struct Event : MutablePersistableRecord {
        var id: Int64?
        
        func encode(to container: inout PersistenceContainer) {
            container[Column.rowID] = id // or `container[.rowID]` with Swift 5.5+
            container["message"] = message
            container["date"] = date
        }
        
        // Update auto-incremented id upon successful insertion
        mutating func didInsert(_ inserted: InsertionSuccess) {
            id = inserted.rowID
        }
    }

    You will then be able to track your record ids, update them, or check for their existence:

    let event = Event(message: "foo", date: Date())
    
    // Insertion sets the record id:
    try event.insert(db)
    event.id // some value
    
    // Record can be updated:
    event.message = "bar"
    try event.update(db)
    
    // Record knows if it exists:
    event.exists(db) // true

Beyond FetchableRecord

Some GRDB users eventually discover that the FetchableRecord protocol does not fit all situations. Use cases that are not well handled by FetchableRecord include:

  • Your application needs polymorphic row decoding: it decodes some type or another, depending on the values contained in a database row.

  • Your application needs to decode rows with a context: each decoded value should be initialized with some extra value that does not come from the database.

Since those use cases are not well handled by FetchableRecord, don't try to implement them on top of this protocol: you'll just fight the framework.

Examples of Record Definitions

We will show below how to declare a record type for the following database table:

try dbQueue.write { db in
    try db.create(table: "place") { t in
        t.autoIncrementedPrimaryKey("id")
        t.column("title", .text).notNull()
        t.column("isFavorite", .boolean).notNull().defaults(to: false)
        t.column("longitude", .double).notNull()
        t.column("latitude", .double).notNull()
    }
}

Each one of the three examples below is correct. You will pick one or the other depending on your personal preferences and the requirements of your application:

Define a Codable struct, and adopt the record protocols you need

This is the shortest way to define a record type.

See the Record Protocols Overview, and Codable Records for more information.

struct Place: Codable {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    private var latitude: CLLocationDegrees
    private var longitude: CLLocationDegrees
    
    var coordinate: CLLocationCoordinate2D {
        get {
            CLLocationCoordinate2D(
                latitude: latitude,
                longitude: longitude)
        }
        set {
            latitude = newValue.latitude
            longitude = newValue.longitude
        }
    }
}

// SQL generation
extension Place: TableRecord {
    /// The table columns
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let title = Column(CodingKeys.title)
        static let isFavorite = Column(CodingKeys.isFavorite)
        static let latitude = Column(CodingKeys.latitude)
        static let longitude = Column(CodingKeys.longitude)
    }
}

// Fetching methods
extension Place: FetchableRecord { }

// Persistence methods
extension Place: MutablePersistableRecord {
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
Define a plain struct, and adopt the record protocols you need

See the Record Protocols Overview for more information.

struct Place {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
}

// SQL generation
extension Place: TableRecord {
    /// The table columns
    enum Columns: String, ColumnExpression {
        case id, title, isFavorite, latitude, longitude
    }
}

// Fetching methods
extension Place: FetchableRecord {
    /// Creates a record from a database row
    init(row: Row) {
        id = row[Columns.id]
        title = row[Columns.title]
        isFavorite = row[Columns.isFavorite]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
    }
}

// Persistence methods
extension Place: MutablePersistableRecord {
    /// The values persisted in the database
    func encode(to container: inout PersistenceContainer) {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.isFavorite] = isFavorite
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
    
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
Define a plain struct optimized for fetching performance

This struct derives its persistence methods from the standard Encodable protocol (see Codable Records), but performs optimized row decoding by accessing database columns with numeric indexes.

See the Record Protocols Overview for more information.

struct Place: Encodable {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    private var latitude: CLLocationDegrees
    private var longitude: CLLocationDegrees
    
    var coordinate: CLLocationCoordinate2D {
        get {
            CLLocationCoordinate2D(
                latitude: latitude,
                longitude: longitude)
        }
        set {
            latitude = newValue.latitude
            longitude = newValue.longitude
        }
    }
}

// SQL generation
extension Place: TableRecord {
    /// The table columns
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let title = Column(CodingKeys.title)
        static let isFavorite = Column(CodingKeys.isFavorite)
        static let latitude = Column(CodingKeys.latitude)
        static let longitude = Column(CodingKeys.longitude)
    }
    
    /// Arrange the selected columns and lock their order
    static let databaseSelection: [any SQLSelectable] = [
        Columns.id,
        Columns.title,
        Columns.favorite,
        Columns.latitude,
        Columns.longitude]
}

// Fetching methods
extension Place: FetchableRecord {
    /// Creates a record from a database row
    init(row: Row) {
        // For high performance, use numeric indexes that match the
        // order of Place.databaseSelection
        id = row[0]
        title = row[1]
        isFavorite = row[2]
        coordinate = CLLocationCoordinate2D(
            latitude: row[3],
            longitude: row[4])
    }
}

// Persistence methods
extension Place: MutablePersistableRecord {
    // Update auto-incremented id upon successful insertion
    mutating func didInsert(_ inserted: InsertionSuccess) {
        id = inserted.rowID
    }
}
Subclass the Record class

See the Record class for more information.

class Place: Record {
    var id: Int64?
    var title: String
    var isFavorite: Bool
    var coordinate: CLLocationCoordinate2D
    
    init(id: Int64?, title: String, isFavorite: Bool, coordinate: CLLocationCoordinate2D) {
        self.id = id
        self.title = title
        self.isFavorite = isFavorite
        self.coordinate = coordinate
        super.init()
    }
    
    /// The table name
    override class var databaseTableName: String { "place" }
    
    /// The table columns
    enum Columns: String, ColumnExpression {
        case id, title, isFavorite, latitude, longitude
    }
    
    /// Creates a record from a database row
    required init(row: Row) throws {
        id = row[Columns.id]
        title = row[Columns.title]
        isFavorite = row[Columns.isFavorite]
        coordinate = CLLocationCoordinate2D(
            latitude: row[Columns.latitude],
            longitude: row[Columns.longitude])
        try super.init(row: row)
    }
    
    /// The values persisted in the database
    override func encode(to container: inout PersistenceContainer) throws {
        container[Columns.id] = id
        container[Columns.title] = title
        container[Columns.isFavorite] = isFavorite
        container[Columns.latitude] = coordinate.latitude
        container[Columns.longitude] = coordinate.longitude
    }
    
    // Update auto-incremented id upon successful insertion
    override func didInsert(_ inserted: InsertionSuccess) {
        super.didInsert(inserted)
        id = inserted.rowID
    }
}

List of Record Methods

This is the list of record methods, along with their required protocols. The Record class adopts all these protocols, and adds a few extra methods.

Method Protocols Notes
Core Methods
init(row:) FetchableRecord
Type.databaseTableName TableRecord
Type.databaseSelection TableRecord *
Type.persistenceConflictPolicy PersistableRecord *
record.encode(to:) EncodableRecord
Insert and Update Records
record.insert(db) PersistableRecord
record.insertAndFetch(db) PersistableRecord & FetchableRecord
record.insertAndFetch(_:as:) PersistableRecord
record.insertAndFetch(_:selection:fetch:) PersistableRecord
record.inserted(db) PersistableRecord
record.save(db) PersistableRecord
record.saveAndFetch(db) PersistableRecord & FetchableRecord
record.saveAndFetch(_:as:) PersistableRecord
record.saveAndFetch(_:selection:fetch:) PersistableRecord
record.saved(db) PersistableRecord
record.update(db) PersistableRecord
record.updateAndFetch(db) PersistableRecord & FetchableRecord
record.updateAndFetch(_:as:) PersistableRecord
record.updateAndFetch(_:selection:fetch:) PersistableRecord
record.update(db, columns:...) PersistableRecord
record.updateAndFetch(_:columns:selection:fetch:) PersistableRecord
record.updateChanges(db, from:...) PersistableRecord *
record.updateChanges(db) { ... } PersistableRecord *
record.updateChangesAndFetch(_:columns:as:modify:) PersistableRecord
record.updateChangesAndFetch(_:columns:selection:fetch:modify:) PersistableRecord
record.updateChanges(db) Record *
record.upsert(db) PersistableRecord
record.upsertAndFetch(db) PersistableRecord & FetchableRecord
record.upsertAndFetch(_:as:) PersistableRecord
Type.updateAll(db, ...) TableRecord
Type.filter(...).updateAll(db, ...) TableRecord ²
Delete Records
record.delete(db) PersistableRecord
Type.deleteOne(db, key:...) TableRecord ¹
Type.deleteOne(db, id:...) TableRecord & Identifiable ¹
Type.deleteAll(db) TableRecord
Type.deleteAll(db, keys:...) TableRecord ¹
Type.deleteAll(db, ids:...) TableRecord & Identifiable ¹
Type.filter(...).deleteAll(db) TableRecord ²
Persistence Callbacks
record.willInsert(_:) PersistableRecord
record.aroundInsert(_:insert:) PersistableRecord
record.didInsert(_:) PersistableRecord
record.willUpdate(_:columns:) PersistableRecord
record.aroundUpdate(_:columns:update:) PersistableRecord
record.didUpdate(_:) PersistableRecord
record.willSave(_:) PersistableRecord
record.aroundSave(_:save:) PersistableRecord
record.didSave(_:) PersistableRecord
record.willDelete(_:) PersistableRecord
record.aroundDelete(_:delete:) PersistableRecord
record.didDelete(deleted:) PersistableRecord
Check Record Existence
record.exists(db) PersistableRecord
Type.exists(db, key: ...) TableRecord ¹
Type.exists(db, id: ...) TableRecord & Identifiable ¹
Type.filter(...).isEmpty(db) TableRecord ²
Convert Record to Dictionary
record.databaseDictionary EncodableRecord
Count Records
Type.fetchCount(db) TableRecord
Type.filter(...).fetchCount(db) TableRecord ²
Fetch Record Cursors
Type.fetchCursor(db) FetchableRecord & TableRecord
Type.fetchCursor(db, keys:...) FetchableRecord & TableRecord ¹
Type.fetchCursor(db, ids:...) FetchableRecord & TableRecord & Identifiable ¹
Type.fetchCursor(db, sql: sql) FetchableRecord ³
Type.fetchCursor(statement) FetchableRecord
Type.filter(...).fetchCursor(db) FetchableRecord & TableRecord ²
Fetch Record Arrays
Type.fetchAll(db) FetchableRecord & TableRecord
Type.fetchAll(db, keys:...) FetchableRecord & TableRecord ¹
Type.fetchAll(db, ids:...) FetchableRecord & TableRecord & Identifiable ¹
Type.fetchAll(db, sql: sql) FetchableRecord ³
Type.fetchAll(statement) FetchableRecord
Type.filter(...).fetchAll(db) FetchableRecord & TableRecord ²
Fetch Record Sets
Type.fetchSet(db) FetchableRecord & TableRecord
Type.fetchSet(db, keys:...) FetchableRecord & TableRecord ¹
Type.fetchSet(db, ids:...) FetchableRecord & TableRecord & Identifiable ¹
Type.fetchSet(db, sql: sql) FetchableRecord ³
Type.fetchSet(statement) FetchableRecord
Type.filter(...).fetchSet(db) FetchableRecord & TableRecord ²
Fetch Individual Records
Type.fetchOne(db) FetchableRecord & TableRecord
Type.fetchOne(db, key:...) FetchableRecord & TableRecord ¹
Type.fetchOne(db, id:...) FetchableRecord & TableRecord & Identifiable ¹
Type.fetchOne(db, sql: sql) FetchableRecord ³
Type.fetchOne(statement) FetchableRecord
Type.filter(...).fetchOne(db) FetchableRecord & TableRecord ²
Codable Records
Type.databaseDecodingUserInfo FetchableRecord *
Type.databaseJSONDecoder(for:) FetchableRecord *
Type.databaseDateDecodingStrategy FetchableRecord *
Type.databaseEncodingUserInfo EncodableRecord *
Type.databaseJSONEncoder(for:) EncodableRecord *
Type.databaseDateEncodingStrategy EncodableRecord *
Type.databaseUUIDEncodingStrategy EncodableRecord *
Define Associations
Type.belongsTo(...) TableRecord *
Type.hasMany(...) TableRecord *
Type.hasOne(...) TableRecord *
Type.hasManyThrough(...) TableRecord *
Type.hasOneThrough(...) TableRecord *
Building Query Interface Requests
record.request(for:...) TableRecord & EncodableRecord *
Type.all() TableRecord ²
Type.none() TableRecord ²
Type.select(...) TableRecord ²
Type.select(..., as:...) TableRecord ²
Type.selectPrimaryKey(as:...) TableRecord ²
Type.annotated(with:...) TableRecord ²
Type.filter(...) TableRecord ²
Type.filter(id:) TableRecord & Identifiable *
Type.filter(ids:) TableRecord & Identifiable *
Type.matching(...) TableRecord ²
Type.including(all:) TableRecord ²
Type.including(optional:) TableRecord ²
Type.including(required:) TableRecord ²
Type.joining(optional:) TableRecord ²
Type.joining(required:) TableRecord ²
Type.group(...) TableRecord ²
Type.groupByPrimaryKey() TableRecord ²
Type.having(...) TableRecord ²
Type.order(...) TableRecord ²
Type.orderByPrimaryKey() TableRecord ²
Type.limit(...) TableRecord ²
Type.with(...) TableRecord ²
Record Comparison
record.databaseEquals(...) EncodableRecord
record.databaseChanges(from:...) EncodableRecord
record.updateChanges(db, from:...) PersistableRecord
record.updateChanges(db) { ... } PersistableRecord
record.hasDatabaseChanges Record
record.databaseChanges Record
record.updateChanges(db) Record

¹ All unique keys are supported: primary keys (single-column, composite, implicit RowID) and unique indexes:

try Player.fetchOne(db, id: 1)                                // Player?
try Player.fetchOne(db, key: ["email": "arthur@example.com"]) // Player?
try Country.fetchAll(db, keys: ["FR", "US"])                  // [Country]

² See Fetch Requests:

let request = Player.filter(emailColumn != nil).order(nameColumn)
let players = try request.fetchAll(db)  // [Player]
let count = try request.fetchCount(db)  // Int

³ See SQL queries:

let player = try Player.fetchOne(db, sql: "SELECT * FROM player WHERE id = ?", arguments: [1]) // Player?

See Prepared Statements:

let statement = try db.makeStatement(sql: "SELECT * FROM player WHERE id = ?")
let player = try Player.fetchOne(statement, arguments: [1])  // Player?

The Query Interface

The query interface lets you write pure Swift instead of SQL:

try dbQueue.write { db in
    // Update database schema
    try db.create(table: "wine") { t in ... }
    
    // Fetch records
    let wines = try Wine
        .filter(originColumn == "Burgundy")
        .order(priceColumn)
        .fetchAll(db)
    
    // Count
    let count = try Wine
        .filter(colorColumn == Color.red)
        .fetchCount(db)
    
    // Update
    try Wine
        .filter(originColumn == "Burgundy")
        .updateAll(db, priceColumn *= 0.75)
    
    // Delete
    try Wine
        .filter(corkedColumn == true)
        .deleteAll(db)
}

You need to open a database connection before you can query the database.

Please bear in mind that the query interface can not generate all possible SQL queries. You may also prefer writing SQL, and this is just OK. From little snippets to full queries, your SQL skills are welcome:

try dbQueue.write { db in
    // Update database schema (with SQL)
    try db.execute(sql: "CREATE TABLE wine (...)")
    
    // Fetch records (with SQL)
    let wines = try Wine.fetchAll(db,
        sql: "SELECT * FROM wine WHERE origin = ? ORDER BY price",
        arguments: ["Burgundy"])
    
    // Count (with an SQL snippet)
    let count = try Wine
        .filter(sql: "color = ?", arguments: [Color.red])
        .fetchCount(db)
    
    // Update (with SQL)
    try db.execute(sql: "UPDATE wine SET price = price * 0.75 WHERE origin = 'Burgundy'")
    
    // Delete (with SQL)
    try db.execute(sql: "DELETE FROM wine WHERE corked")
}

So don't miss the SQL API.

Note: the generated SQL may change between GRDB releases, without notice: don't have your application rely on any specific SQL output.

Database Schema

Once granted with a database connection, you can setup your database schema without writing SQL:

Create Tables

// CREATE TABLE place (
//   id INTEGER PRIMARY KEY AUTOINCREMENT,
//   title TEXT,
//   favorite BOOLEAN NOT NULL DEFAULT 0,
//   latitude DOUBLE NOT NULL,
//   longitude DOUBLE NOT NULL
// )
try db.create(table: "place") { t in
    t.autoIncrementedPrimaryKey("id")
    t.column("title", .text)
    t.column("favorite", .boolean).notNull().defaults(to: false)
    t.column("longitude", .double).notNull()
    t.column("latitude", .double).notNull()
}

The create(table:) method covers nearly all SQLite table creation features. For virtual tables, see Full-Text Search, or use raw SQL.

SQLite itself has many reference documents about table creation: CREATE TABLE, Datatypes In SQLite Version 3, SQLite Foreign Key Support, ON CONFLICT, The WITHOUT ROWID Optimization.

Configure table creation:

// CREATE TABLE example ( ... )
try db.create(table: "example") { t in ... }
    
// CREATE TEMPORARY TABLE example IF NOT EXISTS (
try db.create(table: "example", options: [.temporary, .ifNotExists]) { t in ... }

💡 Tip: database table names should be singular, and camelCased. Make them look like Swift identifiers: place, country, postalAddress, httpRequest.

This will help you using Associations when you need them. Database table names that follow another naming convention are totally OK, but you will need to perform extra configuration.

Note: WITHOUT ROWID tables can not be tracked with Database Observation tools.

Add regular columns with their name and eventual type (text, integer, double, real, numeric, boolean, blob, date, datetime and any) - see SQLite data types:

// CREATE TABLE example (
//   a,
//   name TEXT,
//   creationDate DATETIME,
try db.create(table: "example") { t in
    t.column("a")
    t.column("name", .text)
    t.column("creationDate", .datetime)

Define not null columns, and set default values:

    // email TEXT NOT NULL,
    t.column("email", .text).notNull()
    
    // name TEXT NOT NULL DEFAULT 'Anonymous',
    t.column("name", .text).notNull().defaults(to: "Anonymous")

Use an individual column as primary, unique, or foreign key. When defining a foreign key, the referenced column is the primary key of the referenced table (unless you specify otherwise):

    // id INTEGER PRIMARY KEY AUTOINCREMENT,
    t.autoIncrementedPrimaryKey("id")
    
    // uuid TEXT PRIMARY KEY,
    t.column("uuid", .text).primaryKey()
    
    // email TEXT UNIQUE,
    t.column("email", .text).unique()
    
    // countryCode TEXT REFERENCES country(code) ON DELETE CASCADE,
    t.column("countryCode", .text).references("country", onDelete: .cascade)

💡 Tip: when you need an integer primary key that automatically generates unique values, it is highly recommended that you use the autoIncrementedPrimaryKey method:

try db.create(table: "example") { t in
    t.autoIncrementedPrimaryKey("id")
    ...
}

The reason for this recommendation is that auto-incremented primary keys prevent the reuse of ids. This prevents your app or database observation tools to think that a row was updated, when it was actually deleted, then replaced. Depending on your application needs, this may be acceptable. But usually it is not.

Create an index on the column:

    t.column("score", .integer).indexed()

For extra index options, see Create Indexes below.

Perform integrity checks on individual columns, and SQLite will only let conforming rows in. In the example below, the $0 closure variable is a column which lets you build any SQL expression.

    // name TEXT CHECK (LENGTH(name) > 0)
    // score INTEGER CHECK (score > 0)
    t.column("name", .text).check { length($0) > 0 }
    t.column("score", .integer).check(sql: "score > 0")

Columns can also be defined with a raw sql String, or an SQL literal in which you can safely embed raw values without any risk of syntax errors or SQL injection:

    t.column(sql: "name TEXT")
    
    let defaultName: String = ...
    t.column(literal: "name TEXT DEFAULT \(defaultName)")

Other table constraints can involve several columns:

    // PRIMARY KEY (a, b),
    t.primaryKey(["a", "b"])
    
    // UNIQUE (a, b) ON CONFLICT REPLACE,
    t.uniqueKey(["a", "b"], onConflict: .replace)
    
    // FOREIGN KEY (a, b) REFERENCES parents(c, d),
    t.foreignKey(["a", "b"], references: "parents")
    
    // CHECK (a + b < 10),
    t.check(Column("a") + Column("b") < 10)
    
    // CHECK (a + b < 10)
    t.check(sql: "a + b < 10")
    
    // Raw SQL constraints
    t.constraint(sql: "CHECK (a + b < 10)")
    t.constraint(literal: "CHECK (a + b < \(10))")

Generated columns are available with a custom SQLite build:

    t.column("totalScore", .integer).generatedAs(sql: "score + bonus")
    t.column("totalScore", .integer).generatedAs(Column("score") + Column("bonus"))
}

Modify Tables

SQLite lets you modify existing tables:

// ALTER TABLE referer RENAME TO referrer
try db.rename(table: "referer", to: "referrer")

// ALTER TABLE player ADD COLUMN hasBonus BOOLEAN
// ALTER TABLE player RENAME COLUMN url TO homeURL
// ALTER TABLE player DROP COLUMN score
try db.alter(table: "player") { t in
    t.add(column: "hasBonus", .boolean)
    t.rename(column: "url", to: "homeURL") // SQLite 3.25+
    t.drop(column: "score") // SQLite 3.35+
}

Note: SQLite restricts the possible table alterations, and may require you to recreate dependent triggers or views. See the documentation of the ALTER TABLE for details. See Advanced Database Schema Changes for a way to lift restrictions.

Drop Tables

Drop tables with the drop(table:) method:

try db.drop(table: "obsolete")

Create Indexes

Create indexes with the create(index:) method:

// CREATE INDEX byName ON users(lastName, firstName)
try db.create(index: "byName", on: "users", columns: ["lastName, "firstName"])

// CREATE UNIQUE INDEX byEmail IF NOT EXISTS ON users(email)
try db.create(index: "byEmail", on: "users", columns: ["email"], options: [.unique, .ifNotExists])

Relevant SQLite documentation:

Requests

The query interface requests let you fetch values from the database:

let request = Player.filter(emailColumn != nil).order(nameColumn)
let players = try request.fetchAll(db)  // [Player]
let count = try request.fetchCount(db)  // Int

Query interface requests usually start from a type that adopts the TableRecord protocol, such as a Record subclass (see Records):

class Player: Record { ... }

// The request for all players:
let request = Player.all()
let players = try request.fetchAll(db) // [Player]

When you can not use a record type, use Table:

// The request for all rows from the player table:
let table = Table("player")
let request = table.all()
let rows = try request.fetchAll(db)    // [Row]

// The request for all players from the player table:
let table = Table<Player>("player")
let request = table.all()
let players = try request.fetchAll(db) // [Player]

Note: all examples in the documentation below use a record type, but you can always substitute a Table instead.

Next, declare the table columns that you want to use for filtering, or sorting:

let idColumn = Column("id")
let nameColumn = Column("name")

You can also declare column enums, if you prefer:

// Columns.id and Columns.name can be used just as
// idColumn and nameColumn declared above.
enum Columns: String, ColumnExpression {
    case id
    case name
}

You can now build requests with the following methods: all, none, select, distinct, filter, matching, group, having, order, reversed, limit, joining, including, with. All those methods return another request, which you can further refine by applying another method: Player.select(...).filter(...).order(...).

  • all(), none(): the requests for all rows, or no row.

    // SELECT * FROM player
    Player.all()

    By default, all columns are selected. See Columns Selected by a Request.

  • select(...) and select(..., as:) define the selected columns. See Columns Selected by a Request.

    // SELECT name FROM player
    Player.select(nameColumn, as: String.self)
  • annotated(with: expression...) extends the selection.

    // SELECT *, (score + bonus) AS total FROM player
    Player.annotated(with: (scoreColumn + bonusColumn).forKey("total"))
  • annotated(with: aggregate) extends the selection with association aggregates.

    // SELECT team.*, COUNT(DISTINCT player.id) AS playerCount
    // FROM team
    // LEFT JOIN player ON player.teamId = team.id
    // GROUP BY team.id
    Team.annotated(with: Team.players.count)
  • annotated(withRequired: association) and annotated(withOptional: association) extends the selection with Associations.

    // SELECT player.*, team.color
    // FROM player
    // JOIN team ON team.id = player.teamId
    Player.annotated(withRequired: Player.team.select(colorColumn))
  • distinct() performs uniquing.

    // SELECT DISTINCT name FROM player
    Player.select(nameColumn, as: String.self).distinct()
  • filter(expression) applies conditions.

    // SELECT * FROM player WHERE id IN (1, 2, 3)
    Player.filter([1,2,3].contains(idColumn))
    
    // SELECT * FROM player WHERE (name IS NOT NULL) AND (height > 1.75)
    Player.filter(nameColumn != nil && heightColumn > 1.75)
  • filter(id:) and filter(ids:) are type-safe methods available on Identifiable Records:

    // SELECT * FROM player WHERE id = 1
    Player.filter(id: 1)
    
    // SELECT * FROM country WHERE isoCode IN ('FR', 'US')
    Country.filter(ids: ["FR", "US"])
  • filter(key:) and filter(keys:) apply conditions on primary and unique keys:

    // SELECT * FROM player WHERE id = 1
    Player.filter(key: 1)
    
    // SELECT * FROM country WHERE isoCode IN ('FR', 'US')
    Country.filter(keys: ["FR", "US"])
    
    // SELECT * FROM citizenship WHERE citizenId = 1 AND countryCode = 'FR'
    Citizenship.filter(key: ["citizenId": 1, "countryCode": "FR"])
    
    // SELECT * FROM player WHERE email = 'arthur@example.com'
    Player.filter(key: ["email": "arthur@example.com"])
  • matching(pattern) performs full-text search.

    // SELECT * FROM document WHERE document MATCH 'sqlite database'
    let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database")
    Document.matching(pattern)

    When the pattern is nil, no row will match.

  • group(expression, ...) groups rows.

    // SELECT name, MAX(score) FROM player GROUP BY name
    Player
        .select(nameColumn, max(scoreColumn))
        .group(nameColumn)
  • having(expression) applies conditions on grouped rows.

    // SELECT team, MAX(score) FROM player GROUP BY team HAVING MIN(score) >= 1000
    Player
        .select(teamColumn, max(scoreColumn))
        .group(teamColumn)
        .having(min(scoreColumn) >= 1000)
  • having(aggregate) applies conditions on grouped rows, according to an association aggregate.

    // SELECT team.*
    // FROM team
    // LEFT JOIN player ON player.teamId = team.id
    // GROUP BY team.id
    // HAVING COUNT(DISTINCT player.id) >= 5
    Team.having(Team.players.count >= 5)
  • order(ordering, ...) sorts.

    // SELECT * FROM player ORDER BY name
    Player.order(nameColumn)
    
    // SELECT * FROM player ORDER BY score DESC, name
    Player.order(scoreColumn.desc, nameColumn)

    SQLite considers NULL values to be smaller than any other values for sorting purposes. Hence, NULLs naturally appear at the beginning of an ascending ordering and at the end of a descending ordering. With a custom SQLite build, this can be changed using .ascNullsLast and .descNullsFirst:

    // SELECT * FROM player ORDER BY score ASC NULLS LAST
    Player.order(nameColumn.ascNullsLast)

    Each order call clears any previous ordering:

    // SELECT * FROM player ORDER BY name
    Player.order(scoreColumn).order(nameColumn)
  • reversed() reverses the eventual orderings.

    // SELECT * FROM player ORDER BY score ASC, name DESC
    Player.order(scoreColumn.desc, nameColumn).reversed()

    If no ordering was already specified, this method has no effect:

    // SELECT * FROM player
    Player.all().reversed()
  • limit(limit, offset: offset) limits and pages results.

    // SELECT * FROM player LIMIT 5
    Player.limit(5)
    
    // SELECT * FROM player LIMIT 5 OFFSET 10
    Player.limit(5, offset: 10)
  • joining(...) and including(...) fetch and join records through Associations.

    // SELECT player.*, team.*
    // FROM player
    // JOIN team ON team.id = player.teamId
    Player.including(required: Player.team)
  • with(cte) embeds a common table expression:

    // WITH ... SELECT * FROM player
    let cte = CommonTableExpression(...)
    Player.with(cte)
  • Other requests that involve the primary key:

    • selectPrimaryKey(as:) selects the primary key.

      // SELECT id FROM player
      Player.selectPrimaryKey(as: Int64.self)    // QueryInterfaceRequest<Int64>
      
      // SELECT code FROM country
      Country.selectPrimaryKey(as: String.self)  // QueryInterfaceRequest<String>
      
      // SELECT citizenId, countryCode FROM citizenship
      Citizenship.selectPrimaryKey(as: Row.self) // QueryInterfaceRequest<Row>
    • orderByPrimaryKey() sorts by primary key.

      // SELECT * FROM player ORDER BY id
      Player.orderByPrimaryKey()
      
      // SELECT * FROM country ORDER BY code
      Country.orderByPrimaryKey()
      
      // SELECT * FROM citizenship ORDER BY citizenId, countryCode
      Citizenship.orderByPrimaryKey()
    • groupByPrimaryKey() groups rows by primary key.

You can refine requests by chaining those methods:

// SELECT * FROM player WHERE (email IS NOT NULL) ORDER BY name
Player.order(nameColumn).filter(emailColumn != nil)

The select, order, group, and limit methods ignore and replace previously applied selection, orderings, grouping, and limits. On the opposite, filter, matching, and having methods extend the query:

Player                          // SELECT * FROM player
    .filter(nameColumn != nil)  // WHERE (name IS NOT NULL)
    .filter(emailColumn != nil) //        AND (email IS NOT NULL)
    .order(nameColumn)          // - ignored -
    .reversed()                 // - ignored -
    .order(scoreColumn)         // ORDER BY score
    .limit(20, offset: 40)      // - ignored -
    .limit(10)                  // LIMIT 10

Raw SQL snippets are also accepted, with eventual arguments:

// SELECT DATE(creationDate), COUNT(*) FROM player WHERE name = 'Arthur' GROUP BY date(creationDate)
Player
    .select(sql: "DATE(creationDate), COUNT(*)")
    .filter(sql: "name = ?", arguments: ["Arthur"])
    .group(sql: "DATE(creationDate)")

Columns Selected by a Request

By default, query interface requests select all columns:

// SELECT * FROM player
struct Player: TableRecord { ... }
let request = Player.all()

// SELECT * FROM player
let table = Table("player")
let request = table.all()

The selection can be changed for each individual requests, or in the case of record-based requests, for all requests built from this record type.

The select(...) and select(..., as:) methods change the selection of a single request (see Fetching from Requests for detailed information):

let request = Player.select(max(Column("score")))
let maxScore = try Int.fetchOne(db, request) // Int?

let request = Player.select(max(Column("score")), as: Int.self)
let maxScore = try request.fetchOne(db)      // Int?

The default selection for a record type is controlled by the databaseSelection property:

struct RestrictedPlayer : TableRecord {
    static let databaseTableName = "player"
    static let databaseSelection: [any SQLSelectable] = [Column("id"), Column("name")]
}

struct ExtendedPlayer : TableRecord {
    static let databaseTableName = "player"
    static let databaseSelection: [any SQLSelectable] = [AllColumns(), Column.rowID]
}

// SELECT id, name FROM player
let request = RestrictedPlayer.all()

// SELECT *, rowid FROM player
let request = ExtendedPlayer.all()

Note: make sure the databaseSelection property is explicitly declared as [any SQLSelectable]. If it is not, the Swift compiler may silently miss the protocol requirement, resulting in sticky SELECT * requests. To verify your setup, see the How do I print a request as SQL? FAQ.

Expressions

Feed requests with SQL expressions built from your Swift code:

SQL Operators

GRDB comes with a Swift version of many SQLite built-in operators, listed below. But not all: see Embedding SQL in Query Interface Requests for a way to add support for missing SQL operators.

  • =, <>, <, <=, >, >=, IS, IS NOT

    Comparison operators are based on the Swift operators ==, !=, ===, !==, <, <=, >, >=:

    // SELECT * FROM player WHERE (name = 'Arthur')
    Player.filter(nameColumn == "Arthur")
    
    // SELECT * FROM player WHERE (name IS NULL)
    Player.filter(nameColumn == nil)
    
    // SELECT * FROM player WHERE (score IS 1000)
    Player.filter(scoreColumn === 1000)
    
    // SELECT * FROM rectangle WHERE width < height
    Rectangle.filter(widthColumn < heightColumn)

    Subqueries are supported:

    // SELECT * FROM player WHERE score = (SELECT max(score) FROM player)
    let maximumScore = Player.select(max(scoreColumn))
    Player.filter(scoreColumn == maximumScore)
    
    // SELECT * FROM player WHERE score = (SELECT max(score) FROM player)
    let maximumScore = SQLRequest("SELECT max(score) FROM player")
    Player.filter(scoreColumn == maximumScore)

    Note: SQLite string comparison, by default, is case-sensitive and not Unicode-aware. See string comparison if you need more control.

  • *, /, +, -

    SQLite arithmetic operators are derived from their Swift equivalent:

    // SELECT ((temperature * 1.8) + 32) AS fahrenheit FROM planet
    Planet.select((temperatureColumn * 1.8 + 32).forKey("fahrenheit"))

    Note: an expression like nameColumn + "rrr" will be interpreted by SQLite as a numerical addition (with funny results), not as a string concatenation. See the concat operator below.

    When you want to join a sequence of expressions with the + or * operator, use joined(operator:):

    // SELECT score + bonus + 1000 FROM player
    let values = [
        scoreColumn,
        bonusColumn,
        1000.databaseValue]
    Player.select(values.joined(operator: .add))

    Note in the example above how you concatenate raw values: 1000.databaseValue. A plain 1000 would not compile.

    When the sequence is empty, joined(operator: .add) returns 0, and joined(operator: .multiply) returns 1.

  • ||

    Concatenate several strings:

    // SELECT firstName || ' ' || lastName FROM player
    Player.select([firstNameColumn, " ".databaseValue, lastNameColumn].joined(operator: .concat))

    Note in the example above how you concatenate raw strings: " ".databaseValue. A plain " " would not compile.

    When the sequence is empty, joined(operator: .concat) returns the empty string.

  • AND, OR, NOT

    The SQL logical operators are derived from the Swift &&, || and !:

    // SELECT * FROM player WHERE ((NOT verified) OR (score < 1000))
    Player.filter(!verifiedColumn || scoreColumn < 1000)

    When you want to join a sequence of expressions with the AND or OR operator, use joined(operator:):

    // SELECT * FROM player WHERE (verified AND (score >= 1000) AND (name IS NOT NULL))
    let conditions = [
        verifiedColumn,
        scoreColumn >= 1000,
        nameColumn != nil]
    Player.filter(conditions.joined(operator: .and))

    When the sequence is empty, joined(operator: .and) returns true, and joined(operator: .or) returns false:

    // SELECT * FROM player WHERE 1
    Player.filter([].joined(operator: .and))
    
    // SELECT * FROM player WHERE 0
    Player.filter([].joined(operator: .or))
  • BETWEEN, IN, NOT IN

    To check inclusion in a Swift sequence (array, set, range…), call the contains method:

    // SELECT * FROM player WHERE id IN (1, 2, 3)
    Player.filter([1, 2, 3].contains(idColumn))
    
    // SELECT * FROM player WHERE id NOT IN (1, 2, 3)
    Player.filter(![1, 2, 3].contains(idColumn))
    
    // SELECT * FROM player WHERE score BETWEEN 0 AND 1000
    Player.filter((0...1000).contains(scoreColumn))
    
    // SELECT * FROM player WHERE (score >= 0) AND (score < 1000)
    Player.filter((0..<1000).contains(scoreColumn))
    
    // SELECT * FROM player WHERE initial BETWEEN 'A' AND 'N'
    Player.filter(("A"..."N").contains(initialColumn))
    
    // SELECT * FROM player WHERE (initial >= 'A') AND (initial < 'N')
    Player.filter(("A"..<"N").contains(initialColumn))

    To check inclusion inside a subquery, call the contains method as well:

    // SELECT * FROM player WHERE id IN (SELECT playerId FROM playerSelection)
    let selectedPlayerIds = PlayerSelection.select(playerIdColumn)
    Player.filter(selectedPlayerIds.contains(idColumn))
    
    // SELECT * FROM player WHERE id IN (SELECT playerId FROM playerSelection)
    let selectedPlayerIds = SQLRequest("SELECT playerId FROM playerSelection")
    Player.filter(selectedPlayerIds.contains(idColumn))

    To check inclusion inside a common table expression, call the contains method as well:

    // WITH selectedName AS (...)
    // SELECT * FROM player WHERE name IN selectedName
    let cte = CommonTableExpression(named: "selectedName", ...)
    Player
        .with(cte)
        .filter(cte.contains(nameColumn))

    Note: SQLite string comparison, by default, is case-sensitive and not Unicode-aware. See string comparison if you need more control.

  • EXISTS, NOT EXISTS

    To check if a subquery would return rows, call the exists method:

    // Teams that have at least one other player
    //
    //  SELECT * FROM team
    //  WHERE EXISTS (SELECT * FROM player WHERE teamID = team.id)
    let teamAlias = TableAlias()
    let player = Player.filter(Column("teamID") == teamAlias[Column("id")])
    let teams = Team.aliased(teamAlias).filter(player.exists())
    
    // Teams that have no player
    //
    //  SELECT * FROM team
    //  WHERE NOT EXISTS (SELECT * FROM player WHERE teamID = team.id)
    let teams = Team.aliased(teamAlias).filter(!player.exists())

    In the above example, you use a TableAlias in order to let a subquery refer to a column from another table.

    In the next example, which involves the same table twice, the table alias requires an explicit disambiguation with TableAlias(name:):

    // Players who coach at least one other player
    //
    //  SELECT coach.* FROM player coach
    //  WHERE EXISTS (SELECT * FROM player WHERE coachId = coach.id)
    let coachAlias = TableAlias(name: "coach")
    let coachedPlayer = Player.filter(Column("coachId") == coachAlias[Column("id")])
    let coaches = Player.aliased(coachAlias).filter(coachedPlayer.exists())

    Finally, subqueries can also be expressed as SQL, with SQL Interpolation:

    // SELECT coach.* FROM player coach
    // WHERE EXISTS (SELECT * FROM player WHERE coachId = coach.id)
    let coachedPlayer = SQLRequest("SELECT * FROM player WHERE coachId = \(coachAlias[Column("id")])")
    let coaches = Player.aliased(coachAlias).filter(coachedPlayer.exists())
  • LIKE

    The SQLite LIKE operator is available as the like method:

    // SELECT * FROM player WHERE (email LIKE '%@example.com')
    Player.filter(emailColumn.like("%@example.com"))
    
    // SELECT * FROM book WHERE (title LIKE '%10\%%' ESCAPE '\')
    Player.filter(emailColumn.like("%10\\%%", escape: "\\"))

    Note: the SQLite LIKE operator is case-insensitive but not Unicode-aware. For example, the expression 'a' LIKE 'A' is true but 'æ' LIKE 'Æ' is false.

  • MATCH

    The full-text MATCH operator is available through FTS3Pattern (for FTS3 and FTS4 tables) and FTS5Pattern (for FTS5):

    FTS3 and FTS4:

    let pattern = FTS3Pattern(matchingAllTokensIn: "SQLite database")
    
    // SELECT * FROM document WHERE document MATCH 'sqlite database'
    Document.matching(pattern)
    
    // SELECT * FROM document WHERE content MATCH 'sqlite database'
    Document.filter(contentColumn.match(pattern))

    FTS5:

    let pattern = FTS5Pattern(matchingAllTokensIn: "SQLite database")
    
    // SELECT * FROM document WHERE document MATCH 'sqlite database'
    Document.matching(pattern)
  • AS

    To give an alias to an expression, use the forKey method:

    // SELECT (score + bonus) AS total
    // FROM player
    Player.select((Column("score") + Column("bonus")).forKey("total"))

    If you need to refer to this aliased column in another place of the request, use a detached column:

    // SELECT (score + bonus) AS total
    // FROM player 
    // ORDER BY total
    Player
        .select((Column("score") + Column("bonus")).forKey("total"))
        .order(Column("total").detached)

    Unlike Column("total"), the detached column Column("total").detached is never associated to the "player" table, so it is always rendered as total in the generated SQL, even when the request involves other tables via an association or a common table expression.

SQL Functions

GRDB comes with a Swift version of many SQLite built-in functions, listed below. But not all: see Embedding SQL in Query Interface Requests for a way to add support for missing SQL functions.

  • ABS, AVG, COUNT, DATETIME, JULIANDAY, LENGTH, MAX, MIN, SUM, TOTAL:

    Those are based on the abs, average, count, dateTime, julianDay, length, max, min, sum and total Swift functions:

    // SELECT MIN(score), MAX(score) FROM player
    Player.select(min(scoreColumn), max(scoreColumn))
    
    // SELECT COUNT(name) FROM player
    Player.select(count(nameColumn))
    
    // SELECT COUNT(DISTINCT name) FROM player
    Player.select(count(distinct: nameColumn))
    
    // SELECT JULIANDAY(date, 'start of year') FROM game
    Game.select(julianDay(dateColumn, .startOfYear))

    For more information about the functions dateTime and julianDay, see Date And Time Functions.

  • IFNULL

    Use the Swift ?? operator:

    // SELECT IFNULL(name, 'Anonymous') FROM player
    Player.select(nameColumn ?? "Anonymous")
    
    // SELECT IFNULL(name, email) FROM player
    Player.select(nameColumn ?? emailColumn)
  • LOWER, UPPER

    The query interface does not give access to those SQLite functions. Nothing against them, but they are not unicode aware.

    Instead, GRDB extends SQLite with SQL functions that call the Swift built-in string functions capitalized, lowercased, uppercased, localizedCapitalized, localizedLowercased and localizedUppercased:

    Player.select(nameColumn.uppercased())

    Note: When comparing strings, you'd rather use a collation:

    let name: String = ...
    
    // Not recommended
    nameColumn.uppercased() == name.uppercased()
    
    // Better
    nameColumn.collating(.caseInsensitiveCompare) == name
  • Custom SQL functions and aggregates

    You can apply your own custom SQL functions and aggregates:

    let f = DatabaseFunction("f", ...)
    
    // SELECT f(name) FROM player
    Player.select(f.apply(nameColumn))

Embedding SQL in Query Interface Requests

You will sometimes want to extend your query interface requests with SQL snippets. This can happen because GRDB does not provide a Swift interface for some SQL function or operator, or because you want to use an SQLite construct that GRDB does not support.

Support for extensibility is large, but not unlimited. All the SQL queries built by the query interface request have the shape below. If you need something else, you'll have to use raw SQL requests.

WITH ...     -- 1
SELECT ...   -- 2
FROM ...     -- 3
JOIN ...     -- 4
WHERE ...    -- 5
GROUP BY ... -- 6
HAVING ...   -- 7
ORDER BY ... -- 8
LIMIT ...    -- 9
  1. WITH ...: see Common Table Expressions.

  2. SELECT ...

    The selection can be provided as raw SQL:

    // SELECT IFNULL(name, 'O''Brien'), score FROM player
    let request = Player.select(sql: "IFNULL(name, 'O''Brien'), score")
    
    // SELECT IFNULL(name, 'O''Brien'), score FROM player
    let defaultName = "O'Brien"
    let request = Player.select(sql: "IFNULL(name, ?), score", arguments: [suffix])

    The selection can be provided with SQL Interpolation:

    // SELECT IFNULL(name, 'O''Brien'), score FROM player
    let defaultName = "O'Brien"
    let request = Player.select(literal: "IFNULL(name, \(defaultName)), score")

    The selection can be provided with a mix of Swift and SQL Interpolation:

    // SELECT IFNULL(name, 'O''Brien') AS displayName, score FROM player
    let defaultName = "O'Brien"
    let displayName: SQL = "IFNULL(\(Column("name")), \(defaultName)) AS displayName"
    let request = Player.select(displayName, Column("score"))

    When the custom SQL snippet should behave as a full-fledged expression, with support for the + Swift operator, the forKey aliasing method, and all other SQL Operators, build an expression literal with the SQL.sqlExpression method:

    // SELECT IFNULL(name, 'O''Brien') AS displayName, score FROM player
    let defaultName = "O'Brien"
    let displayName = SQL("IFNULL(\(Column("name")), \(defaultName))").sqlExpression
    let request = Player.select(displayName.forKey("displayName"), Column("score"))

    Such expression literals allow you to build a reusable support library of SQL functions or operators that are missing from the query interface. For example, you can define a Swift date function:

    func date(_ value: some SQLSpecificExpressible) -> SQLExpression {
        SQL("DATE(\(value))").sqlExpression
    }
    
    // SELECT * FROM "player" WHERE DATE("createdAt") = '2020-01-23'
    let request = Player.filter(date(Column("createdAt")) == "2020-01-23")

    See the Query Interface Organization for more information about SQLSpecificExpressible and SQLExpression.

  3. FROM ...: only one table is supported here. You can not customize this SQL part.

  4. JOIN ...: joins are fully controlled by Associations. You can not customize this SQL part.

  5. WHERE ...

    The WHERE clause can be provided as raw SQL:

    // SELECT * FROM player WHERE score >= 1000
    let request = Player.filter(sql: "score >= 1000")
    
    // SELECT * FROM player WHERE score >= 1000
    let minScore = 1000
    let request = Player.filter(sql: "score >= ?", arguments: [minScore])

    The WHERE clause can be provided with SQL Interpolation:

    // SELECT * FROM player WHERE score >= 1000
    let minScore = 1000
    let request = Player.filter(literal: "score >= \(minScore)")

    The WHERE clause can be provided with a mix of Swift and SQL Interpolation:

    // SELECT * FROM player WHERE (score >= 1000) AND (team = 'red')
    let minScore = 1000
    let scoreCondition: SQL = "\(Column("score")) >= \(minScore)"
    let request = Player.filter(scoreCondition && Column("team") == "red")

    See SELECT ... above for more SQL Interpolation examples.

  6. GROUP BY ...

    The GROUP BY clause can be provided as raw SQL, SQL Interpolation, or a mix of Swift and SQL Interpolation, just as the selection and the WHERE clause (see above).

  7. HAVING ...

    The HAVING clause can be provided as raw SQL, SQL Interpolation, or a mix of Swift and SQL Interpolation, just as the selection and the WHERE clause (see above).

  8. ORDER BY ...

    The ORDER BY clause can be provided as raw SQL, SQL Interpolation, or a mix of Swift and SQL Interpolation, just as the selection and the WHERE clause (see above).

    In order to support the desc and asc query interface operators, and the reversed() query interface method, you must provide your orderings as expression literals with the SQL.sqlExpression method:

    // SELECT * FROM "player" 
    // ORDER BY (score + bonus) ASC, name DESC
    let total = SQL("(score + bonus)").sqlExpression
    let request = Player
        .order(total.desc, Column("name"))
        .reversed()
  9. LIMIT ...: use the limit(_:offset:) method. You can not customize this SQL part.

Fetching from Requests

Once you have a request, you can fetch the records at the origin of the request:

// Some request based on `Player`
let request = Player.filter(...)... // QueryInterfaceRequest<Player>

// Fetch players:
try request.fetchCursor(db) // A Cursor of Player
try request.fetchAll(db)    // [Player]
try request.fetchSet(db)    // Set<Player>
try request.fetchOne(db)    // Player?

For example:

let allPlayers = try Player.fetchAll(db)                            // [Player]
let arthur = try Player.filter(nameColumn == "Arthur").fetchOne(db) // Player?

See fetching methods for information about the fetchCursor, fetchAll, fetchSet and fetchOne methods.

You sometimes want to fetch other values.

The simplest way is to use the request as an argument to a fetching method of the desired type:

// Fetch an Int
let request = Player.select(max(scoreColumn))
let maxScore = try Int.fetchOne(db, request) // Int?

// Fetch a Row
let request = Player.select(min(scoreColumn), max(scoreColumn))
let row = try Row.fetchOne(db, request)!     // Row
let minScore = row[0] as Int?
let maxScore = row[1] as Int?

You can also change the request so that it knows the type it has to fetch:

  • With asRequest(of:), useful when you use Associations:

    struct BookInfo: FetchableRecord, Decodable {
        var book: Book
        var author: Author
    }
    
    // A request of BookInfo
    let request = Book
        .including(required: Book.author)
        .asRequest(of: BookInfo.self)
    
    let bookInfos = try dbQueue.read { db in
        try request.fetchAll(db) // [BookInfo]
    }
  • With select(..., as:), which is handy when you change the selection:

    // A request of Int
    let request = Player.select(max(scoreColumn), as: Int.self)
    
    let maxScore = try dbQueue.read { db in
        try request.fetchOne(db) // Int?
    }

Fetching by Key

Fetching records according to their primary key is a common task.

Identifiable Records can use the type-safe methods fetchOne(_:id:), fetchAll(_:ids:) and fetchSet(_:ids:):

try Player.fetchOne(db, id: 1)