GRDB.swift

GRDB.swift is "a SQLite toolkit for Swift applications" developed as the most feature-rich SQLite library in the Swift ecosystem. With the concept of "type-safe and reactive database operations," it provides complex database operations through Swift-like type-safe and intuitive APIs. Comprehensively supporting all functionality needed for modern iOS/macOS app development such as ValueObservation, DatabasePool, and migration management, it has established a solid position as an enterprise-level database solution.

ORMSwiftSQLiteiOSDatabaseReactive

GitHub Overview

groue/GRDB.swift

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

Stars7,641
Watchers88
Forks768
Created:June 30, 2015
Language:Swift
License:MIT License

Topics

databasedatabase-observationgrdbspmsqlsql-buildersqlitesqlite-databases

Star History

groue/GRDB.swift Star History
Data as of: 7/19/2025, 09:29 AM

Library

GRDB.swift

Overview

GRDB.swift is "a SQLite toolkit for Swift applications" developed as the most feature-rich SQLite library in the Swift ecosystem. With the concept of "type-safe and reactive database operations," it provides complex database operations through Swift-like type-safe and intuitive APIs. Comprehensively supporting all functionality needed for modern iOS/macOS app development such as ValueObservation, DatabasePool, and migration management, it has established a solid position as an enterprise-level database solution.

Details

GRDB.swift 2025 edition boasts mature APIs and the highest level of functionality as the definitive solution for Swift SQLite operations with over 7 years of development experience. Protocol-oriented design enables flexible model definition through combinations of FetchableRecord, PersistableRecord, and TableRecord. It integrates multiple approaches including SQL query builders, raw SQL access, and reactive database observation, supporting a wide range of needs from simple CRUD operations to complex analytics. It actively adopts the latest technologies such as Codable integration, Swift Concurrency support, and parallel reading through WAL mode.

Key Features

  • Type-Safe SQL Query Builder: Type-safe query construction through QueryInterfaceRequest
  • Database Observation: Real-time data change notifications through ValueObservation
  • Robust Concurrency Control: Thread-safe operations through DatabaseQueue/DatabasePool
  • Migration Management: Automatic schema evolution through DatabaseMigrator
  • Direct SQLite Access: Compatibility of raw SQL and high-level APIs
  • Protocol-Oriented Design: Complete integration with Swift Codable

Pros and Cons

Pros

  • Deep integration with Swift language features and excellent type safety
  • Rich feature set suitable for enterprise applications
  • Easy implementation of reactive UIs through database observation
  • Access to all SQLite functionality without restrictions
  • Excellent documentation and active community support
  • Latest asynchronous processing through Swift Concurrency support

Cons

  • High learning cost and complex for beginners
  • May be excessive for simple apps due to rich functionality
  • Complex installation of latest versions due to CocoaPods deployment issues
  • Dependency management constraints due to lack of Carthage support
  • Compatibility issues with legacy classes that are not Sendable
  • Limited learning resources for other mobile developers

Reference Pages

Code Examples

Setup

// Package.swift
dependencies: [
    .package(url: "https://github.com/groue/GRDB.swift.git", from: "6.24.1")
]

// CocoaPods (specific version required)
// Podfile
pod 'GRDB.swift', '~> 6.24.1'

// Or latest version workaround
pod 'GRDB.swift', :git => 'https://github.com/groue/GRDB.swift.git', :tag => 'v6.29.3'
import GRDB

// Database connection initialization
var dbQueue: DatabaseQueue!

func setupDatabase() throws {
    let databaseURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("Database.sqlite")
    
    dbQueue = try DatabaseQueue(path: databaseURL.path)
}

Basic Usage

// Record definition (Codable + GRDB protocols)
struct Player: Codable, FetchableRecord, PersistableRecord {
    var id: Int64?
    var name: String
    var score: Int
    var team: String?
    var createdAt: Date
    
    // Table name specification (optional)
    static let databaseTableName = "player"
    
    // Column definitions
    enum Columns {
        static let id = Column(CodingKeys.id)
        static let name = Column(CodingKeys.name)
        static let score = Column(CodingKeys.score)
        static let team = Column(CodingKeys.team)
        static let createdAt = Column(CodingKeys.createdAt)
    }
}

// Database schema creation
func createSchema() throws {
    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().defaults(to: 0)
            t.column("team", .text)
            t.column("createdAt", .datetime).notNull()
        }
    }
}

Query Execution

// Data insertion
func insertPlayer(name: String, score: Int, team: String?) throws {
    try dbQueue.write { db in
        var player = Player(
            id: nil,
            name: name,
            score: score,
            team: team,
            createdAt: Date()
        )
        try player.insert(db)
        print("Inserted player with ID: \(player.id!)")
    }
}

// Single record retrieval
func getPlayer(id: Int64) throws -> Player? {
    try dbQueue.read { db in
        return try Player.fetchOne(db, id: id)
    }
}

// All records retrieval
func getAllPlayers() throws -> [Player] {
    try dbQueue.read { db in
        return try Player.fetchAll(db)
    }
}

// Conditional search (Query Interface)
func getPlayersWithHighScore(threshold: Int) throws -> [Player] {
    try dbQueue.read { db in
        return try Player
            .filter(Player.Columns.score >= threshold)
            .order(Player.Columns.score.desc)
            .fetchAll(db)
    }
}

// Raw SQL queries
func getTopPlayersByTeam() throws -> [Row] {
    try dbQueue.read { db in
        return try Row.fetchAll(db, sql: """
            SELECT team, MAX(score) as max_score, COUNT(*) as player_count
            FROM player
            WHERE team IS NOT NULL
            GROUP BY team
            ORDER BY max_score DESC
            """)
    }
}

Data Operations

// Record update
func updatePlayerScore(_ player: Player, newScore: Int) throws {
    try dbQueue.write { db in
        var updatedPlayer = player
        updatedPlayer.score = newScore
        try updatedPlayer.update(db)
    }
}

// Partial update
func updatePlayerTeam(id: Int64, newTeam: String) throws {
    try dbQueue.write { db in
        try Player
            .filter(key: id)
            .updateAll(db, Player.Columns.team.set(to: newTeam))
    }
}

// Record deletion
func deletePlayer(_ player: Player) throws {
    try dbQueue.write { db in
        try player.delete(db)
    }
}

// Conditional deletion
func deletePlayersWithLowScore(threshold: Int) throws -> Int {
    try dbQueue.write { db in
        return try Player
            .filter(Player.Columns.score < threshold)
            .deleteAll(db)
    }
}

// Complex query example
func getPlayerStatistics() throws -> PlayerStatistics {
    try dbQueue.read { db in
        let totalPlayers = try Player.fetchCount(db)
        let averageScore = try Double.fetchOne(db, sql: "SELECT AVG(score) FROM player") ?? 0
        let topScore = try Int.fetchOne(db, sql: "SELECT MAX(score) FROM player") ?? 0
        let teams = try String.fetchAll(db, sql: "SELECT DISTINCT team FROM player WHERE team IS NOT NULL")
        
        return PlayerStatistics(
            totalPlayers: totalPlayers,
            averageScore: averageScore,
            topScore: topScore,
            teams: teams
        )
    }
}

struct PlayerStatistics {
    let totalPlayers: Int
    let averageScore: Double
    let topScore: Int
    let teams: [String]
}

Configuration and Customization

// Database observation (ValueObservation)
func observePlayers() -> AnyPublisher<[Player], Error> {
    ValueObservation
        .tracking { db in
            try Player
                .order(Player.Columns.score.desc)
                .fetchAll(db)
        }
        .publisher(in: dbQueue)
        .eraseToAnyPublisher()
}

// Usage example
class PlayerViewModel: ObservableObject {
    @Published var players: [Player] = []
    private var cancellable: AnyCancellable?
    
    func startObserving() {
        cancellable = observePlayers()
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { completion in
                    if case .failure(let error) = completion {
                        print("Observer error: \(error)")
                    }
                },
                receiveValue: { players in
                    self.players = players
                }
            )
    }
}

// Parallel reading with DatabasePool
func setupConcurrentDatabase() throws -> DatabasePool {
    let databaseURL = try FileManager.default
        .url(for: .applicationSupportDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
        .appendingPathComponent("ConcurrentDatabase.sqlite")
    
    var config = Configuration()
    config.prepareDatabase { db in
        // Enable WAL mode for parallel reading
        try db.execute(sql: "PRAGMA journal_mode = WAL")
    }
    
    return try DatabasePool(path: databaseURL.path, configuration: config)
}

// Custom SQL functions
func setupCustomFunctions() throws {
    try dbQueue.write { db in
        let removeAccents = DatabaseFunction("removeAccents", argumentCount: 1, pure: true) { (arguments: [DatabaseValue]) in
            guard let string = String.fromDatabaseValue(arguments[0]) else {
                return nil
            }
            return string.folding(options: .diacriticInsensitive, locale: nil)
        }
        db.add(function: removeAccents)
    }
}

Error Handling

// Error handling and transactions
enum DatabaseError: Error {
    case playerNotFound
    case invalidData
    case transactionFailed
}

func performBulkPlayerUpdate(playerUpdates: [PlayerUpdate]) throws {
    try dbQueue.write { db in
        try db.inSavepoint {
            for update in playerUpdates {
                guard let existingPlayer = try Player.fetchOne(db, id: update.playerId) else {
                    throw DatabaseError.playerNotFound
                }
                
                var updatedPlayer = existingPlayer
                updatedPlayer.score = update.newScore
                updatedPlayer.team = update.newTeam
                
                try updatedPlayer.update(db)
            }
        }
    }
}

struct PlayerUpdate {
    let playerId: Int64
    let newScore: Int
    let newTeam: String?
}

// Asynchronous processing and error handling
@MainActor
func loadPlayersAsync() async throws -> [Player] {
    return try await withCheckedThrowingContinuation { continuation in
        dbQueue.asyncRead { result in
            switch result {
            case .success(let db):
                do {
                    let players = try Player.fetchAll(db)
                    continuation.resume(returning: players)
                } catch {
                    continuation.resume(throwing: error)
                }
            case .failure(let error):
                continuation.resume(throwing: error)
            }
        }
    }
}

// Database migration
func setupMigrations() throws {
    var migrator = DatabaseMigrator()
    
    // Initial version
    migrator.registerMigration("v1.0") { db in
        try db.create(table: "player") { t in
            t.autoIncrementedPrimaryKey("id")
            t.column("name", .text).notNull()
            t.column("score", .integer).notNull().defaults(to: 0)
        }
    }
    
    // Version 1.1: Add team column
    migrator.registerMigration("v1.1") { db in
        try db.alter(table: "player") { t in
            t.add(column: "team", .text)
        }
    }
    
    // Version 1.2: Add createdAt column
    migrator.registerMigration("v1.2") { db in
        try db.alter(table: "player") { t in
            t.add(column: "createdAt", .datetime).notNull().defaults(to: Date())
        }
    }
    
    // Execute migration
    try migrator.migrate(dbQueue)
}