SQL (formerly SwifQL) is a strongly typed, declarative, composable Swift DSL for building SQL.
Write SQL concepts directly in Swift, compose them as values, and prepare the result for PostgreSQL, MySQL, or DuckDB. SQL builds statements. Execution stays with your database driver.
Generated SQL examples in this README use PostgreSQL .plain rendering unless noted otherwise. Use .splitted when you need query placeholders and bind values for a driver.
Requires Swift 6.3 or newer.
.package(
url: "https://github.com/SwiftStream/SQL",
from: "2.1.0"
)Then depend on product SQL:
.target(
name: "App",
dependencies: [
.product(name: "SQL", package: "SQL")
]
)and import it:
import SQLThe idea is simple: start from the SQL you actually want.
SELECT "users"."id", "users"."email"
FROM "users"
WHERE "users"."email" = 'john@example.com'
LIMIT 10Then express the same idea directly in Swift:
let query = SQL
.select(\User.$id, \User.$email)
.from(User.table)
.where(\User.$email == "john@example.com")
.limit(10)or declaratively:
let query = SQL {
Select(\User.$id, \User.$email)
From(User.table)
Where(\User.$email == "john@example.com")
Limit(10)
}Simple SQL should stay simple. Monster-complex SQL should still be possible without escaping into a second query language.
Write SQL ideas in Swift, compose them like Swift values, and let the selected dialect prepare the final statement.
Model-backed authoring is first-class.
struct User: Table {
static var tableName: String { "users" }
@Column("id") var id: Int
@Column("email") var email: String
@Column("name") var name: String
@Column("active") var active: Bool
@Column("role") var role: String
init() {}
}Now the table and columns can be referenced through the model:
User.table
\User.$id
\User.$email
\User.$nameUse these references throughout model-backed queries.
When you do not have a model type, explicit paths are available too:
let users = Path.Table("users")
let email = users.column("email")Use Path.Table(...) and Path.Column(...) when you need explicit paths. Model-backed code can keep using User.table and type-safe key paths.
SQL supports two declarative styles. Chain clauses directly, or use the result builder when that reads better for the query you are writing.
Chain SQL clauses directly:
let query = SQL
.select(\User.$id, \User.$email, \User.$name)
.from(User.table)
.where(\User.$active == true)
.orderBy(.asc(\User.$name))
.limit(20)which gives:
SELECT "users"."id", "users"."email", "users"."name"
FROM "users"
WHERE "users"."active" = TRUE
ORDER BY "users"."name" ASC
LIMIT 20Start directly from SQL:
SQL.select(...)
SQL.insertInto(...)
SQL.update(...)
SQL.delete(from: ...)
SQL.where(...)The same query can be written with SQL { ... }:
let query = SQL {
Select(\User.$id, \User.$email, \User.$name)
From(User.table)
Where(\User.$active == true)
OrderBy(.asc(\User.$name))
Limit(20)
}This prepares to the same SQL as the fluent form above.
When a clause needs multiple children, conditions, or loops, switch only that clause to its result-builder form:
let query = SQL {
Select {
\User.$id
\User.$email
\User.$name
}
From(User.table)
Where {
\User.$active == true
if let email {
\User.$email == email
}
if let roles {
Or {
for role in roles {
\User.$role == role
}
}
}
}
OrderBy {
OrderByItem.asc(\User.$name)
}
Limit(20)
}Use concise calls for fixed SQL and clause builders when Swift control flow makes the query clearer.
Both declarative forms use the same preparation and binding behavior. Clauses are ordinary composable SQLable values, so you can extract and reuse them when that makes a query easier to read.
Inside a query, you can mix normal Swift literals and values directly with model-backed SQL expressions. You do not need to wrap them in SQLable first.
As each expression is built, SQL's operator overloads produce composable SQLable values automatically:
let minimumID = 100
let idOffset = 1
let email = "john@example.com"
let query = SQL {
Select(
\User.$id,
\User.$id + idOffset
)
From(User.table)
Where(
\User.$id >= minimumID
&& \User.$email == email
&& \User.$active == true
)
}Here idOffset, minimumID, email, and true are ordinary Swift values. They become part of SQL expressions only where they are used with SQL operands.
PostgreSQL gives:
SELECT "users"."id", "users"."id" + 1
FROM "users"
WHERE "users"."id" >= 100 AND "users"."email" = 'john@example.com' AND "users"."active" = TRUEThere are two expressions to be careful with in ordinary Swift code outside a query: == nil and != nil.
When Wrapped: SQLable, Optional<Wrapped> is also SQLable. That means these expressions can participate in SQL overload resolution even when they look like ordinary Swift optional checks:
let someOptional: String? = nil
let a = someOptional == nil
let b = someOptional != nilDepending on the surrounding type context, a and b can resolve to SQLable predicates instead of Bool.
If you mean a normal Swift nil-check, make the result type explicit:
let someOptional: String? = nil
let a: Bool = someOptional == nil
let b: Bool = someOptional != nilInside SQL expressions, == nil and != nil are exactly what you want:
let isMissing = \User.$email == nil
// SQL: "users"."email" IS NULL
let isPresent = \User.$email != nil
// SQL: "users"."email" IS NOT NULLSQLQuery lets a normal Swift value own a reusable parameterized query:
struct UserQuery: SQLQuery {
let active: Bool
let email: String?
let roles: [String]?
var query: Query {
Select(\User.$id, \User.$email)
From(User.table)
Where {
\User.$active == active
if let email {
\User.$email == email
}
if let roles {
Or {
for role in roles {
\User.$role == role
}
}
}
}
}
}The call site stays small:
let users = UserQuery(
active: true,
email: email,
roles: roles
)
let prepared = users.prepare(.psql)Because SQLQuery is itself SQLable, it composes as an ordinary SQL value:
let source = From(
UserQuery(
active: true,
email: nil,
roles: ["admin", "moderator"]
)
.as("activeUsers")
)Query is the shorthand return type provided by SQLQuery.
SQL builds statements. Your driver executes them.
Prepare for the target database:
let postgres = query.prepare(.psql)
let mysql = query.prepare(.mysql)
let duck = query.prepare(.duck)For a plain rendered statement:
let sql = query.prepare(.psql).plainFor a statement with separated bind values:
let prepared = query.prepare(.psql).splitted
let sql = prepared.query
let values = prepared.valuesThat split is the normal boundary for a database driver or integration layer:
SQL / SQLable
β
prepare(dialect)
β
SQLPrepared
β
plain SQL
or
query + values
β
your driver / connection / executor
SQL does not manage connection pools, transaction policy, result decoding, or execution lifecycle.
The same model-backed references work for DML.
let insert = SQL
.insertInto(
User.table,
fields: \User.$email, \User.$name
)
.values("john@example.com", "John")Batch values can be appended through the same values surface.
let update = SQL
.update(User.table)
.set[items:
\User.$name == "Mike"
]
.where(\User.$id == userID)Schema-qualified model aliases remain available when you need them:
let vip = User.inSchema("VIP")
let update = SQL
.update(vip.table)
.set[items:
vip.$name == "Mike"
]let delete = SQL
.delete(from: User.table)
.where(\User.$id == userID)The broader DML surface also includes RETURNING and conflict-related composition where supported by the target SQL dialect.
SQL includes SQL-shaped builders for table DDL:
let createUsers = CreateTable("users") {
NewColumn("id", .uuid).primaryKey()
NewColumn("email", .text).unique().notNull()
}which gives:
CREATE TABLE "users" ("id" uuid PRIMARY KEY, "email" text UNIQUE NOT NULL)And one ALTER TABLE statement can own multiple actions:
let alterUsers = AlterTable("users") {
AddColumn("display_name", .text)
}Database migrations are historical records. They must not silently change because the current Swift model was renamed later.
So migration-facing schema, table, and column identifiers stay explicit:
CreateTable("users") {
NewColumn("email", .text)
}
AlterTable("users") {
AddColumn("display_name", .text)
}Do not derive historical migration identifiers from current User.table / \User.$email model metadata.
SQL only builds the DDL statement. Migration versions, history, transaction policy, and execution belong to the consuming application or database layer.
SQL includes database-facing civil and interval values:
let date = PureDate(year: 2026, month: 9, day: 4)!
let time = PureTime(
hour: 12,
minute: 34,
second: 56,
nanosecond: 123_456_789
)!
let dateTime = DateTime(
year: 2026,
month: 9,
day: 4,
hour: 12,
minute: 34,
second: 56,
nanosecond: 123_456_789
)!
let interval = Interval(
months: 2,
days: -3,
microseconds: 4
)
SQL
.select(date, time, dateTime, interval)
.prepare(.psql)
.plainPostgreSQL gives:
SELECT
DATE '2026-09-04',
TIME '12:34:56.123456789',
TIMESTAMP '2026-09-04 12:34:56.123456789',
INTERVAL '2 months -3 days 4 microseconds'These values model database semantics directly:
PureDateis a timezone-free civil date.PureTimeis a nanosecond-capable time of day.DateTimeis a timezone-free civil date + time, not an instant.Intervalpreserves independent months, days, and microseconds.
Rendering is dialect-aware and fails closed where a selected database cannot represent a value exactly.
The same query value can be prepared for the built-in dialects:
query.prepare(.psql)
query.prepare(.mysql)
query.prepare(.duck)Queries can share structure across PostgreSQL, MySQL, and DuckDB while each dialect owns its database-specific rendering.
Values, columns, expressions, predicates, functions, clauses, complete statements, and reusable SQLQuery values all participate through SQLable.
That means you can build SQL pieces wherever it is convenient, keep them in variables or constants, pass them through your own APIs, and combine them later:
let shiftedID: any SQLable = \User.$id + 1
let active: any SQLable = \User.$active == true
let hasEmail: any SQLable = \User.$email != nil
let predicate: any SQLable = active && hasEmail
let selection = Select(\User.$id, shiftedID)
let source = From(User.table)
let filter = Where(predicate)
let ordering = OrderBy(.asc(\User.$name))
let query = SQL {
selection
source
filter
ordering
}PostgreSQL gives:
SELECT "users"."id", "users"."id" + 1
FROM "users"
WHERE "users"."active" = TRUE AND "users"."email" IS NOT NULL
ORDER BY "users"."name" ASCA complete statement can itself become a fragment:
let users = SQL {
Select(\User.$id)
From(User.table)
}
let source = From(users.as("u"))SQLQuery values compose the same way:
let source = From(
UserQuery(
active: true,
email: nil,
roles: nil
)
.as("u")
)Nested queries, subqueries, set operations, and your own custom SQLable types all use the same composition model.
SQL grammar still matters. A scalar expression belongs where SQL expects an expression, a clause belongs where that clause is valid, and a statement becomes a nested statement when used as a source.
Use => for aliases and SQL casts.
Column alias:
SQL.select(
\User.$email => "email"
)Cast:
SQL.select(
\User.$email => .text
)Table aliases keep typed column access:
let u = User.as("u")
let query = SQL
.select(u.$id, u.$email)
.from(u.table)Normal Swift-looking operators map to SQL predicates:
| Swift | SQL |
|---|---|
> |
> |
>= |
>= |
< |
< |
<= |
<= |
== |
= |
== nil |
IS NULL |
!= |
!= / dialect equivalent |
!= nil |
IS NOT NULL |
&& |
AND |
| ` |
For example:
let predicate =
\User.$active == true &&
\User.$email != nilwhich gives:
"users"."active" = TRUE AND "users"."email" IS NOT NULLFunctions are ordinary SQL values too:
let activeCount =
Fn.count(\User.$id)
.filter(where: \User.$active == true)
=> "active_count"PostgreSQL-specific helpers remain available when you actually want PostgreSQL-specific SQL.
JSON object:
let json = PgJsonObject()
.field(key: "id", value: \User.$id)
.field(key: "email", value: \User.$email)Arrays:
PgArray()
PgArray(1, 2, 3)
PgArray() => .textArrayThe project also supports JSON paths, nested values, array/list operations, and other dialect-aware SQL families.
SQL includes a much broader surface than basic SELECT/INSERT/UPDATE/DELETE, including:
- joins, subqueries, CTEs, and set operations
- aggregates, FILTER, ordering, grouping, and analytical SQL
- JSON and nested values
- PostgreSQL arrays and related operators
- DuckDB LIST/lambda helpers and nested types
- PIVOT / UNPIVOT
- MERGE
- COPY
- DML + RETURNING
- DDL and schema operations
- sequences and macros
- table and file functions
- catalog/file-oriented DuckDB SQL
- custom SQL functions, operators, paths, and raw/static structure when a typed surface does not exist yet.
The examples above cover only part of the API. SQL is designed to stay close to the database language even as queries grow more complex.
SQL is built from composable values. Values that participate in SQL conform to SQLable and expose SQLPart values.
At a high level:
SQL.select(...)
SQL { ... }
Select(...) / Select { ... }
From(...) / From { ... }
Where(...) / Where { ... }
SQLQuery
custom SQLable values
β
composable SQLPart structure
β
selected SQLDialect
β
SQLPrepared
β
plain / splitted
These authoring styles can be mixed in the same query.
Functions, operators, clauses, and dialect-specific features are composable SQL values, so custom extensions fit the same model.
If you implement custom SQLable values by working with parts directly, preserve the existing structure instead of flattening it. See MIGRATION.md for the compatibility note.
SQL uses Swift 6 language mode and requires Swift 6.3+.
Query and bind values are not Sendable by default. When crossing an actor boundary, keep query construction/preparation on the originating isolation and send an application-owned Sendable snapshot containing the data your driver actually needs.
The project was renamed from SwifQL to SQL.
The common migration is:
was
import SwifQL
let query = SwifQL
.select(\User.$id, \User.$email)
.from(User.table)became
import SQL
let query = SQL
.select(\User.$id, \User.$email)
.from(User.table)Model-backed references remain first-class:
\User.$id
\User.$email
User.tableimport SwifQL is no longer supported. After import SQL, retained old SwifQL* symbol spellings may still exist as deprecated/renamed bridges where provided.
For a step-by-step guide to migrating from SwifQL v1 β SQL v2, see MIGRATION.md.
For current release details, see RELEASE_NOTES.md.
If you cannot find a function or SQL construct out of the box, check the existing source first β much of the library is built from small SQLable extensions.
If something is genuinely missing, issues and pull requests are welcome β€οΈ
Tests live under Tests/SQLTests.
And if SQL saves you some time, giving the project a βοΈ is always appreciated π
