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.
GitHub Overview
groue/GRDB.swift
A toolkit for SQLite databases, with a focus on application development
Topics
Star History
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)
}