Real SQLite, written in typed, composable, refactorable Swift. No query strings. No stringly typed columns. No ORM hiding the SQL.
.package(url: "https://github.com/lukevanin/swiftql.git", from: "1.5.4")Every clause appears once, under its SQL name, in SQL's grammatical order. Read a SwiftQL query and you are reading the statement it produces.
SELECT * FROM Person
WHERE name = 'Fred'
ORDER BY age ASC
LIMIT 5
let query = sql { schema in
let person = schema.table(Person.self)
Select(person)
From(person)
Where(person.name == "Fred")
OrderBy(person.age.ascending())
Limit(5)
}
That correspondence is the whole design. Other typed query libraries reach SQL through a method chain: you start from a table type and attach clauses to it, in an order the library accepts rather than the order SQL defines. SwiftQL keeps SQL's own order, which is why porting a query is a mechanical exercise rather than an interpretive one - clause by clause, in both directions. The porting guide maps every clause and works through ports up to recursive common table expressions.
A string-based query survives the rename, ships, and fails on a device you cannot reach. SwiftQL moves that failure to the build.
@SQLTable
struct Person {
var id: String
var occupationId: String?
var name: String
var age: Int
}
let request = database.makeRequest(with: query)
let people: [Person] = try request.fetchAll()
let first: Person? = try request.fetchOne()
Select(person) fixes the row type when the query is
constructed, so both execution methods expose their result type directly.
There are no untyped row dictionaries and no manual casts. Xcode completes
and navigates the table model, and the compiler catches missing fields,
incompatible expression types, and invalid clause ordering.
A first-class construct that only handles simple statements is a demo. SwiftQL's aim is to express what SQLite expresses, with the remaining surface tracked as conformance work rather than left undefined.
Inner, left, and cross joins; grouping and HAVING; ordering and pagination; scalar and table subqueries; compound queries.
Ordinary and recursive CTEs, with the self-reference passed to the closure as a typed table like any other.
Boolean, numeric, text, optional, conditional, and aggregate expressions composed with Swift operators and generic constraints.
Typed inserts, updates, and deletes with the same SQL-shaped API, plus basic table creation.
Observe typed results through GRDB-backed Combine publishers, or adopt XLQueryObserver directly from SwiftUI.
Extend SQLite with Swift enums, custom value types, and type-safe custom SQL functions.
What is supported is not a claim, it is an inventory. Every feature is recorded with evidence from a real SQLite engine, versioned with the library, and published as a conformance report. The gaps are recorded there too.
Swift has good persistence libraries. They differ mainly in how much of the relational model they ask you to give up.
| Shape | Where the SQL lives | Type safety comes from | |
|---|---|---|---|
| SwiftQL | Result builder, one clause per statement in SQL order | Visible, in SQL order, in Swift | Macros + generics, checked at compile time |
| StructuredQueries | Chained methods with typed closures and key paths | Emitted in SQL order; #sql escape hatch for typed SQL strings |
@Table macro + key paths |
| GRDB | Records + query interface, with raw SQL always available | Visible when you write it; strings when you drop down | Codable records and column definitions |
| SQLite.swift | Expression DSL | Behind the DSL | Generic Expression types |
| SwiftData | Object graph | Hidden - no SQL surface | @Model macro over an opaque store |
| Fluent | Server-side ORM | Hidden behind the model layer | Model definitions and property wrappers |
SwiftQL is not a GRDB alternative, it is a typed query layer above it. Execution, connection management, transactions, and observation are GRDB's, and deliberately so: that part is mature, well understood, and not worth reimplementing. SwiftQL replaces the part where queries become strings.
sqlCreate creates a table but does not migrate an existing schema. Use GRDB's DatabaseMigrator alongside SwiftQL - they compose, because SwiftQL sits on GRDB rather than replacing it..select { }.where { } reads better to you than Select / From / Where, that preference is the whole argument.