Build type-safe SQL queries with Swift using GRDB’s Query Interface
The Query Interface is GRDB’s Swift API for building SQL queries. It provides a type-safe, composable way to construct SELECT, INSERT, UPDATE, and DELETE statements without writing raw SQL.
The Query Interface approximates the SQLite SELECT query grammar through a hierarchy of Swift types and protocols. Instead of concatenating SQL strings, you build queries using Swift methods that compile to optimized SQL.
// Vulnerable to SQL injection, brittle to schema changestry db.execute( sql: "SELECT * FROM player WHERE name = ? AND score > ? ORDER BY score DESC", arguments: ["O'Brien", 1000])
Types that conform to TableRecord can use the Query Interface:
struct Player: Codable, FetchableRecord, PersistableRecord { var id: Int64 var name: String var score: Int enum Columns { static let id = Column(CodingKeys.id) static let name = Column(CodingKeys.name) static let score = Column(CodingKeys.score) }}extension Player: TableRecord { static let databaseTableName = "player"}
// Single columnPlayer.order(\.$score.desc)// Multiple columnsPlayer.order(\.$score.desc, \.$name)// With collationPlayer.order(\.$name.collating(.localizedCaseInsensitiveCompare))
// All columns (default)Player.all()// Specific columnsPlayer.select(\.$name, \.$score)// Single column as simple typePlayer.select(\.$name, as: String.self)// Computed columnsPlayer.select((\.$score + \.$bonus).forKey("total"))
// Group by columnPlayer.group(\.$teamId)// With aggregate functionsPlayer .select(\.$teamId, count(Column("*")).forKey("playerCount")) .group(\.$teamId)// With HAVING clausePlayer .group(\.$teamId) .having(count(Column("*")) > 5)
Here’s a real-world query combining multiple features:
struct Player: Codable, FetchableRecord, PersistableRecord, TableRecord { var id: Int64 var name: String var score: Int var teamId: Int64? // Find top players on a team static func topPlayers(teamId: Int64, limit: Int = 10) -> QueryInterfaceRequest<Player> { Player .filter { $0.teamId == teamId } .filter { $0.score > 0 } .order(\.$score.desc, \.$name) .limit(limit) }}// Usagelet topPlayers = try dbQueue.read { db in try Player.topPlayers(teamId: 5).fetchAll(db)}
Define request methods as static functions on your record types. This keeps queries reusable and testable.