GRDB.swift

repository·master·Indexed 27 days ago

https://github.com/groue/grdb.swift

A toolkit for SQLite databases designed for Swift application development. GRDB provides high-level tools for SQL generation, database observation, robust concurrency, and schema migrations, while maintaining low-level access to SQLite features. It includes a Swift Query Interface for type-safe SQL generation and supports integration with SwiftUI via the GRDBQuery package and ValueObservation.

Tokens
103.5K
Snippets
302
Records
415
Agent score
93%

What's inside GRDB.swift

  1. Overview of GRDB features

    master

    GRDB is a toolkit for SQLite databases designed for application development. Key features include:

    • SQL Generation: Enhance models with persistence and fetching methods to avoid raw SQL and manual row handling.
    • Database Observation: Receive notifications when database values change.
    • Robust Concurrency: Efficient multi-threaded database access, including support for WAL (Write-Ahead Logging) for concurrent reads and writes.
    • Migrations: Tools to evolve your database schema as your application version changes.
    • SQLite Power: Full support for advanced SQLite features for developers who need them.
  2. Understand GRDB Associations

    master

    Associations are connections between Record types that streamline database operations, making them safer and more efficient. Instead of manually filtering records using foreign key IDs, you can declare relationships (like belongsTo or hasMany) to allow GRDB to generate optimized SQL joins and prefetching requests.

    // Example: Defining associations between Author and Book
    
    extension Author {
        static let books = hasMany(Book.self)
        var books: QueryInterfaceRequest<Book> {
            request(for: Author.books)
        }
    }
    
    extension Book {
        static let author = belongsTo(Author.self)
        var author: QueryInterfaceRequest<Author> {
            request(for: Book.author)
        }
    }
    
    // Usage: Fetching associated records efficiently
    struct BookInfo: Decodable, FetchableRecord {
        let book: Book
        let author: Author?
    }
    
    let request = Book.including(optional: Book.author)
    let bookInfos = try BookInfo.fetchAll(db, request)
  3. Understand DatabasePool concurrency and WAL mode

    master

    A DatabasePool uses SQLite's WAL (Write-Ahead Logging) mode by default (unless Configuration.readonly is set). This enables concurrent reads and writes:

    • Writes: Executed serially on a single writer dispatch queue.
    • Reads: Executed in parallel across a pool of read-only connections. The maximum number of concurrent reads is controlled by Configuration.maximumReaderCount.
  4. Understand GRDB Experimental Features and ABI Stability

    master

    Experimental Features

    Features marked with the 🔥 EXPERIMENTAL badge are advanced and not yet stabilized. They are not protected by semantic versioning and may break between minor releases.

    ABI Stability

    GRDB does not support library evolution or ABI stability. However, it follows semantic versioning for API stability. If you need to build binary frameworks, you can enable the BUILD_LIBRARY_FOR_DISTRIBUTION Xcode option.

  5. Understand GRDB Core Principles

    master

    GRDB is designed around several key architectural principles to provide a safe and predictable database experience in Swift:

    • Record Types as Plain Values: Unlike traditional ORMs that use 'active records' with auto-updating or lazy loading (which can cause multi-threading issues), GRDB treats database records as simple, immutable-friendly Swift values.
    • Raw SQL Support: GRDB embraces raw SQL for complex queries, ensuring that developers aren't forced into inefficient or illegible query builder translations when SQL is the better tool.
    • Database as Single Source of Truth: Unlike Core Data or Realm, which may maintain multiple versions of data across contexts or threads, GRDB treats the SQLite database file as the unambiguous, single source of truth.
    • SQLite & Application Focus: GRDB is specialized for SQLite and front-end GUI applications. It is not intended for server-side database drivers (like PostgreSQL or MySQL) but provides advanced features like migrations, database observation, and multi-threading safety specifically for client-side needs.
  6. Key architectural components in GRDBDemo

    master

    When studying the GRDBDemo implementation, focus on these core components to understand how to structure your own app:

    • AppDatabase: The central type for database access. It manages the schema via DatabaseMigrator and provides methods for reading and writing data.
    • Persistence: Responsible for instantiating different database instances, such as a persistent database on disk for the main app and in-memory databases for SwiftUI previews.
    • Record types (e.g., Player): Models that conform to Record and Codable to enable seamless database interaction.
    • @Observable Models (e.g., PlayerListModel): Objects that observe the database using ValueObservation to ensure the UI stays in sync with the underlying data.
    • Environment Injection: Using GRDBDemoApp to feed the database into the SwiftUI environment so it can be accessed by views.
  7. Compare DatabaseQueue and DatabasePool

    master

    Choosing between DatabaseQueue and DatabasePool depends on your concurrency requirements:

    • DatabaseQueue: Opens a single connection and serializes all accesses (reads and writes). Only one thread uses the database at any time. This is simpler but limits concurrency.
    • DatabasePool: Manages a pool of connections and uses WAL mode to allow concurrent reads and writes. While writes are still serialized, multiple threads can perform isolated reads simultaneously. This allows different readers to see different database states at the same time without blocking each other.
  8. Quickstart with GRDB

    master

    To start using GRDB, follow these four steps:

    1. Open a database connection: Use DatabaseQueue to connect to a SQLite file.
    2. Define the database schema: Use db.create(table:) within a write block to set up your tables.
    3. Define a record type: Create a Swift struct that conforms to Codable, FetchableRecord, and PersistableRecord to map database rows to Swift objects.
    4. Write and read data: Use dbQueue.write for modifications (like insert) and dbQueue.read for fetching data (like fetchAll).
    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.primaryKey("id", .text)
            t.column("name", .text).notNull()
            t.column("score", .integer).notNull()
        }
    }
    
    // 3. Define a record type
    struct Player: Codable, FetchableRecord, PersistableRecord {
        var id: String
        var name: String
        var score: Int
    }
    
    // 4. Write and read in 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)
    }
  9. Quickstart: Use GRDB in four steps

    master

    To start using GRDB, follow these four steps:

    1. Open a database connection using DatabaseQueue.
    2. Define your database schema using db.create(table:...) within a write block.
    3. Define a record type by conforming a struct or class to FetchableRecord and PersistableRecord.
    4. Perform writes and reads using dbQueue.write and dbQueue.read blocks.
    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.primaryKey("id", .text)
            t.column("name", .text).notNull()
            t.column("score", .integer).notNull()
        }
    }
    
    // 3. Define a record type
    struct Player: Codable, Identifiable, FetchableRecord, PersistableRecord {
        var id: String
        var name: String
        var score: Int
        
        enum Columns {
            static let name = Column(CodingKeys.name)
            static let score = Column(CodingKeys.score)
        }
    }
    
    // 4. Write and read in 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)
    }
    
    try dbQueue.read { db in
        let player = try Player.find(db, id: "1")
        
        let bestPlayers = try Player
            .order(\.score.desc)
            .limit(10)
            .fetchAll(db)
    }
  10. Migrate to GRDB 6: DatabaseRegionObservation API

    master

    The DatabaseRegionObservation.start(in:onError:onChange:) method now returns a cancellable object. The DatabaseRegionObservation.extent property has been removed; instead, control the duration of the observation using the returned cancellable.

    let observation = DatabaseRegionObservation.tracking(Player.all())
    
    // GRDB 6 usage
    let cancellable = observation.start(
        in: dbQueue,
        onError: { error in /* handle error */ },
        onChange: { db in
            print("Players were modified")
        }
    )
    let observation = DatabaseRegionObservation.tracking(Player.all())
    
    // GRDB 6
    let cancellable = observation.start(
        in: dbQueue,
        onError: { error in /* handle error */ },
        onChange: { db in
            print("Players were modified")
        })
  11. Store complex properties as JSON columns

    master

    When a Codable record contains a property that is not a simple value (like an array, dictionary, or nested struct), GRDB automatically encodes and decodes it as a JSON string in the database.

    To customize the JSON format, implement the following protocol requirements:

    • static func databaseJSONDecoder(for column: String) -> JSONDecoder in FetchableRecord
    • static func databaseJSONEncoder(for column: String) -> JSONEncoder in EncodableRecord

    Important: Always set the sortedKeys option on your JSONEncoder to ensure stable JSON output. This is required for Record Comparison and ValueObservation to work correctly.

    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
        let achievement = Achievement(name: "Use Codable Records", color: .gold)
        let player = Player(name: "Arthur", score: 100, achievements: [achievement])
        try player.insert(db)
    }