SQL, as a first-class language in Swift.
Real SQLite, written in typed, composable, refactorable Swift.
No raw query strings for supported queries. No stringly typed columns.
No ORM hiding the SQL.
SQL is too important to bury in strings. SwiftQL takes a different approach: it brings SQLite into Swift's type system using macros, generics, operators, and result builders.
Tables are Swift structs. Columns are typed properties. Statements are values written in SQL order. Selected rows decode back into the Swift type you asked for.
Concretely: rename a column and the compiler finds every query that used it, instead of your users finding them at runtime. A string-based query survives the rename, ships, and fails on a device you cannot reach.
import Foundation
import SwiftQL
@SQLTable
struct Person {
var id: String
var occupationId: String?
var name: String
var age: Int
}
let databaseURL = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("sqlite")
let database = try GRDBDatabase(url: databaseURL, logger: nil)
try database.makeRequest(with: sqlCreate(Person.self)).execute()
let query = sql { schema in
let person = schema.table(Person.self)
Select(person)
From(person)
Where(person.name == "Fred")
}The temporary file keeps this example self-contained. In an application, use
your durable database URL and reuse one GRDBDatabase for that path. The basic
sqlCreate call uses CREATE TABLE IF NOT EXISTS; it creates this first table
but does not migrate an existing schema when Person changes.
That Swift expression emits recognizable SQLite:
SELECT t0.id AS id, t0.occupationId AS occupationId,
t0.name AS name, t0.age AS age
FROM Person AS t0
WHERE (t0.name == 'Fred')Create a request and execute it without leaving Swift's type system:
let request = database.makeRequest(with: query)
let people: [Person] = try request.fetchAll()
let firstPerson: Person? = try request.fetchOne()Select(person) fixes the row type when the query is constructed, so both
execution methods expose their result types directly. There are no untyped row
dictionaries or manual result casts.
The Where clause is ordinary Swift, too:
Where(person.name == "Fred")There is no "name" lookup string to mistype. Xcode can complete and navigate
the table model, while the compiler catches missing fields, incompatible
expression types, and invalid clause ordering. Rename a model property and the
compiler leads you to the queries affected by it.
If you know SQLite, you already know the shape of SwiftQL: Select, From,
Join, Where, GroupBy, Having, and With appear in SQL order and retain
their SQL meaning.
Add the following line to the dependencies section in your Package.swift
file:
.package(url: "https://github.com/lukevanin/swiftql.git", from: "1.6.0")
1.6.0 is the latest published package. The examples above use APIs retained
by v1.3; the static-query surface remains available from version 1.2.0. Pin a
source revision only when intentionally testing later changes from main.
In Xcode, follow Apple's Adding package dependencies to your app,
and specify the package URL https://github.com/lukevanin/swiftql.git.
Getting started walks through defining a table, creating it, inserting, selecting, binding values, updating, deleting, and grouping work in a transaction. It is the fastest path from an empty file to a working query, and it doubles as a quick reference afterwards.
From there:
- Select queries for joins, grouping, subqueries, and CTEs.
- Declared queries
to write queries as ordinary Swift functions with
@SQLQuery. - Advanced usage for connections, statement preparation, row lifetime, and the full transaction contract.
- The full documentation, whose examples are connected to executable test scenarios, so the API shown in the guides stays aligned with the library.
The Getting Started playground is that same walkthrough
with the code running beside the explanation: nine pages against a real SQLite
file, from defining a table through inserting, selecting, updating, deleting,
named bindings, lazy result sets, and transactions, to observing a live query.
Open Examples/SwiftQLExamples.xcworkspace, build the SwiftQLExamples scheme
for My Mac, and step through it.
Each page prints its results and states the output it expects next to the code producing it, so you can change a query and see immediately what moved.
Examples/TodoApp is a SwiftUI application for
iOS 17 and macOS 14 whose entire data layer is SwiftQL. It shows the parts a
guide covers one at a time working together: a four-table schema with a
many-to-many join, declared @SQLQueries reads including a join and a grouped
aggregate, one query serving every filter, sort, and search combination,
writes that return their row through RETURNING, an atomic move between
lists, and an interface fed entirely by live queries — completing a to-do
updates the list and the sidebar counts with no reload call anywhere. Its
queries are checked against a schema snapshot at build time, and 62 tests
cover the query layer.
Open Examples/TodoApp/TodoApp.xcodeproj and run the TodoApp scheme.
WHATSNEW.md describes each release in plain language — what you can do that you could not before, and whether it affects code you already wrote. CHANGELOG.md remains the exact record.
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.
What sets SwiftQL apart is positional correspondence with SQL. Every other typed query library in this table reaches 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 writes each clause once, under its SQL name, in SQL's grammatical order. The Swift source and the statement it produces have the same shape.
That is what makes porting mechanical rather than interpretive. A query moves between SQL and SwiftQL a clause at a time, in both directions, without first being redesigned into somebody's builder vocabulary. The porting guide is the proof: a clause-by-clause mapping table, worked ports up to recursive common table expressions, and an explicit list of the places the correspondence is not exact.
- You need schema migrations. SwiftQL does not provide them.
sqlCreatecreates a table but does not migrate an existing schema. Use GRDB'sDatabaseMigratoralongside SwiftQL - they compose, because SwiftQL sits on GRDB rather than replacing it. - You prefer a chained query builder. Several Swift libraries express typed
queries as method chains. If
.select { }.where { }reads better to you thanSelect/From/Where, that preference is the whole argument. - You want the database to disappear. SwiftData is a better fit if you would rather model an object graph than think about tables, and you can require recent Apple platforms.
- You are writing a server with a non-SQLite backend. Fluent covers PostgreSQL, MySQL, and MongoDB today. SwiftQL is SQLite-only; other dialects are roadmap work, not shipped work.
- Your queries are already written and working. The cost of SwiftQL is learning its expression surface. The benefit arrives when the schema changes, so a stable schema you rarely touch may not repay it.
- Tables
and projections.
@SQLTableand@SQLResultderive typed table, column, and result metadata at compile time. There are no generated model files to keep in sync. - Expressions. Compose boolean, numeric, text, optional, conditional, and aggregate expressions with Swift operators and generic constraints.
- Queries.
Build selects with inner, left, and cross joins; grouping and
HAVING; ordering and pagination; scalar and table subqueries; compound queries; and ordinary or recursive common table expressions. - Writes and table creation. Create basic tables and construct typed inserts, updates, and deletes with the same SQL-shaped API.
- Bindings and results.
Keep invocation values in fresh immutable binding packets, then decode
fetchAll()andfetchOne()results directly into Swift values. - Static query contracts. Define database-independent SQL, parameter, result, identity, and cardinality metadata before opening a database, then prepare it against a compatible driver.
- Declared queries.
Write a query as an ordinary Swift function with
@SQLQuery, or a whole container of them with@SQLQueries, and call it like any other function. A SwiftPM build-tool plugin can prepare every declared query against a schema snapshot at build time; see COMPATIBILITY.md for the build systems it runs under. - Live data.
Observe typed query results with
for try awaitoverstream()andstreamOne(), the canonical live-query API. GRDB-backed Combine publishers track the same database region a query reads, andXLObservableQueryorXLQueryObserveradopt either one from SwiftUI. - JSON.
Read into a JSON document with the
->and->>selection operators andjsonExtract, build documents withjsonArrayandjsonObject, change them with the insert, replace, set, remove, and patch functions, collect rows withjsonGroupArrayandjsonGroupObject, and address any of it withXLJSONPathinstead of a path string. The JSONB variants read and write SQLite's binary representation. - Your domain. Extend SQLite with Swift enums, custom value types, and type-safe custom SQL functions.
SwiftQL does not replace the relational model with an object graph, and it does not make SQL disappear. It preserves the database concepts that make SQL powerful, then gives them native Swift names, types, composition, completion, and refactoring support.
The boundary is deliberate. Swift checks the APIs, table fields, result shapes, expression types, and supported statement composition that it can prove. SQLite remains the authority for the live schema, runtime constraints, coercion rules, and dialect-specific behavior. SwiftQL deliberately grows its SQLite coverage without blurring that line.
- Design rationale explains why SwiftQL is SQL-shaped, why queries are result builders in SQL order, why column values are Swift types rather than SQLite types, and what those choices cost.
- Compiler compatibility records the supported Swift toolchains and reproducible CI matrix.
- SQLite conformance records the evidence boundary for SwiftQL's currently supported public subset.
- Changelog records released behavior, and the unreleased section records what has landed since the latest published version.
- Performance benchmarks measure query construction, preparation, caching, binding, execution, and decoding.
- First-party source coverage preserves the reproducible coverage baseline and its raw evidence.
- The roadmap tracks reliability, SQLite conformance, query declarations, Swift 6, and future database work.
For maintainers, releasing SwiftQL documents exact-tag validation, artifact provenance, publication, verification, and recovery.
MIT license. See LICENSE.md.
