We just open sourced a new Swift package, StructuredQueries, which provides a very type-safe API for building SQL in Swift. It leverages a ton of advanced Swift features—including macros, parameter packs, dynamic member lookup with metatype key paths, result builders, and more—to achieve things we haven't seen in other languages and libraries, and with a minimal amount of userland code.
For example, applying the library's @Table
macro to the following two Swift data types:
@Table
struct RemindersList {
let id: Int
var title = ""
}
@Table
struct Reminder {
let id: Int
var isCompleted = false
var remindersListID: Int
var title = ""
}
…enables a suite of APIs that are type-safe from building the query all the way to decoding. This includes select statements with complex joins, as well as insert, update, and delete statements:
RemindersList
.group(by: \.id)
.leftJoin(Reminder.where { !$0.isCompleted }) { $0.id == $1.remindersListID }
.select { ($0, $1.count()) }
// SELECT
// "remindersLists"."id", "remindersLists"."title", count("reminders"."id")
// FROM "remindersLists"
// JOIN "reminders" ON "remindersLists"."id" = "reminders"."remindersListID"
// WHERE (NOT "reminders"."isCompleted")
// GROUP BY "remindersLists"."id"
// => (RemindersList, Int)
Reminder.insert {
($0.remindersListID, $0.title)
} values: {
(1, "Get groceries")
(2, "Take a walk")
(1, "Get haircut")
}
.returning(\.id)
// INSERT INTO "reminders"
// ("remindersListID", "title")
// VALUES
// (1, 'Get groceries'),
// (2, 'Take a walk'),
// (1, 'Get haircut')
// RETURNING "id"
// => Int
Reminder
.where { $0.id == 2 }
.update { $0.isCompleted.toggle() }
.returning(\.self)
// UPDATE "reminders"
// SET "isCompleted" = (NOT "reminders"."isCompleted")
// WHERE "reminders"."id" = 2
// RETURNING "id", "isCompleted", "remindersListID", "title"
// => Reminder
We've been truly impressed with what parameter packs unlock in the language, especially when combined with other features.
Parameter packs are definitely still young and thorny. We’ve reported many issues in the process (and have many more we will report soon), and we have employed many workarounds in the library’s implementation, but we’re excited to see any future improvements.
Metatype key paths are a recent addition to Swift that have allowed us to implement a statically type-safe version of Ruby on Rails' "scopes." By defining static let
s that describe queries, one can compose them into instances via dynamic member lookup:
extension Reminder {
static let notDeleted = Self.where { $0.deletedAt.isNot(nil) }
}
Reminder
.where(\.isCompleted)
.notDeleted
And while these type-safe query building APIs are fun, our library also employs a #sql
macro for building "safe SQL strings" that can be composed into a query builder, or executed on their own. The macro can do rudimentary linting on the SQL string’s syntax (e.g. check for balanced delimiters and no bindings in identifiers), and then use a custom string interpolation that prevents SQL injection.
We currently have just a single SQLite driver via SharingGRDB, which uses GRDB's database observation to create a SwiftData-like experience for SQL. But we're eager to push things to other databases (Postgres, MySQL) and database libraries!
Please check out the repo and documentation for more!