Write type-safe SQL queries with string interpolation in GRDB
SQL Interpolation lets you safely embed Swift values directly in SQL queries using string interpolation syntax. It prevents SQL injection while keeping your queries readable and maintainable.
SQL Interpolation provides the convenience of string interpolation with the safety of parameterized queries:
let name = "O'Brien"let id = 42try db.execute(literal: "UPDATE player SET name = \(name) WHERE id = \(id)")// Executes: UPDATE player SET name = 'O''Brien' WHERE id = 42// Values are properly escaped and parameterized!
Use the literal argument label (not sql) to enable SQL interpolation.
let name = "Arthur"let score = 1000try dbQueue.write { db in try db.execute(literal: """ INSERT INTO player (name, score) VALUES (\(name), \(score)) """)}
// SQLRequest with interpolationlet name = "Arthur"let request: SQLRequest<Player> = """ SELECT * FROM player WHERE name = \(name) """let players = try request.fetchAll(db)
let conditions: [SQL] = [ "score > \(minScore)", "name IS NOT NULL", "createdAt > \(startDate)"]let whereClause = conditions.joined(separator: " AND ")let query: SQL = "SELECT * FROM player WHERE \(literal: whereClause)"
let name = "Arthur"let query: SQL = "SELECT * FROM player WHERE name = \(name)"try dbQueue.read { db in let (sql, arguments) = try query.build(db) print(sql) // "SELECT * FROM player WHERE name = ?" print(arguments) // ["Arthur"]}
// Arraylet ids = [1, 2, 3]let query: SQL = "SELECT * FROM player WHERE id IN \(ids)"// SELECT * FROM player WHERE id IN (1, 2, 3)// Empty array (always false)let emptyIds: [Int] = []let query: SQL = "SELECT * FROM player WHERE id IN \(emptyIds)"// SELECT * FROM player WHERE id IN (NULL) -- Always false// NOT INlet excludedIds = [7, 8, 9]let query: SQL = "SELECT * FROM player WHERE id NOT IN \(excludedIds)"
struct Player: TableRecord { static let databaseTableName = "player"}// Table name from typelet query: SQL = "SELECT * FROM \(Player.self)"// SELECT * FROM player// Table name from instancelet player: Player = ...let query: SQL = "INSERT INTO \(tableOf: player) ..."
struct Player: TableRecord { }// All columnslet query: SQL = "SELECT \(columnsOf: Player.self) FROM player"// SELECT player.* FROM player// With table aliaslet query: SQL = """ SELECT \(columnsOf: Player.self, tableAlias: "p") FROM player p """// SELECT p.* FROM player p
// Descending orderlet ordering = Column("score").desclet query: SQL = "SELECT * FROM player ORDER BY \(ordering)"// SELECT * FROM player ORDER BY score DESC// Multiple orderingslet orderings = [Column("score").desc, Column("name")]let orderClause = orderings.map(\.sqlOrdering).joined(separator: ", ")let query: SQL = "SELECT * FROM player ORDER BY \(literal: orderClause)"
// DANGEROUS: Direct string interpolationlet userInput = "'; DROP TABLE player; --"let sql = "SELECT * FROM player WHERE name = '\(userInput)'"try db.execute(sql: sql)// Executes: SELECT * FROM player WHERE name = ''; DROP TABLE player; --'// 💥 Your table is gone!
How it works:
Values are converted to statement arguments
Arguments are sent separately from the SQL query
SQLite treats arguments as data, never as executable SQL
// ❌ Hardcoded table namelet query: SQL = "SELECT * FROM player"// ❌ Hardcoded column name let query: SQL = "SELECT * FROM player WHERE score > \(minScore)"// ❌ Direct string interpolation (SQL injection!)let sql = "SELECT * FROM player WHERE name = '\(name)'"try db.execute(sql: sql)
Here’s a real-world example using SQL interpolation:
struct Player: Codable, FetchableRecord, PersistableRecord, TableRecord { var id: Int64 var name: String var score: Int var teamId: Int64? var createdAt: Date static let databaseTableName = "player"}extension Player { /// Custom search with dynamic filters static func search( name: String? = nil, minScore: Int? = nil, teamId: Int64? = nil, createdAfter: Date? = nil ) -> SQLRequest<Player> { var conditions: [SQL] = [] if let name = name { conditions.append("\(CodingKeys.name) LIKE \('%' || \(name) || '%')") } if let minScore = minScore { conditions.append("\(CodingKeys.score) >= \(minScore)") } if let teamId = teamId { conditions.append("\(CodingKeys.teamId) = \(teamId)") } if let date = createdAfter { conditions.append("\(CodingKeys.createdAt) > \(date)") } let whereClause = conditions.isEmpty ? SQL("1 = 1") : conditions.joined(separator: " AND ") return """ SELECT \(columnsOf: self) FROM \(self) WHERE \(literal: whereClause) ORDER BY \(CodingKeys.score) DESC """ }}// Usagelet players = try dbQueue.read { db in try Player.search( name: "Art", minScore: 1000, createdAfter: Date().addingTimeInterval(-86400) ).fetchAll(db)}