Skip to content

Releases: swiftstream/SQL

🚀 SQL 2.0.0

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 06 Oct 00:29

SwifQL is now SQL 🚀

The project moved to the SwiftStream organization and the package, product, module, and query root now share one name: SQL.

Install 2.0.0 with:

.package(
    url: "https://github.com/SwiftStream/SQL",
    from: "2.0.0"
)

and use product SQL:

.product(name: "SQL", package: "SQL")

Breaking change: SwifQL → SQL

was

import SwifQL

let query = SwifQL
    .select(\User.$id)  // or Path.Column("id")
    .from(User.table)   // or Path.Table("users")

became

import SQL

let query = SQL         // <-- SwifQL replaced with SQL
    .select(\User.$id)
    .from(User.table)

There is no compatibility module named SwifQL. After import SQL, retained old SwifQL* symbols may still be available as deprecated/renamed bridges where provided.

Declarative SQL

You can keep the direct fluent style:

let query = SQL
    .select(\User.$id, \User.$email)
    .from(User.table)
    .where(\User.$email == "john@example.com")
    .limit(10)

Result Builder DSL

Brand new gorgeous way to write your queries

let query = SQL {
    Select {
        \User.$id
        \User.$email
    }
    // or Select(\User.$id, \User.$email)

    From {
        User.table
    }
    // or From(User.table)

    Where {
        \User.$email == "john@example.com"
    }
    // you still can use
    // Where(\User.$email == "john@example.com")
    // for simple predicates

    Limit(10)
}

The classic declarative and result-builder approaches use the same composable parts, preparation, and binding pipeline.

Select { ... } can be easily replaced with declarative Select(...) form, same for From, Where and other parts.

The biggest advantage of a result-builder DSL, aside from its elegance, is that you can use if/else/for and other conditional constructs.

Reusable queries

SQLQuery lets a reusable value hide conditional SQL while the call site stays small:

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
                    }
                }
            }
        }
    }
}

Outside the type, it is just a normal composable value:

let users = UserQuery(
    active: true,
    email: email,
    roles: roles
)

let prepared = users.prepare(.psql)

Because SQLQuery is SQLable, UserQuery(...) can also be nested directly in FROM/JOIN/subquery positions without manually unwrapping .query.

Declarative table DDL

let createUsers = CreateTable("users") {
    NewColumn("id", .uuid).primaryKey()
    NewColumn("email", .text).unique().notNull()
}

createUsers.prepare(.psql).plain

will give:

CREATE TABLE "users" ("id" uuid PRIMARY KEY, "email" text UNIQUE NOT NULL)

SQL only builds the statement. Migration history, transaction policy, and execution stay in your application or database layer.

More SQL

The 2.0 line significantly expands the SQL surface, including analytics, JSON and nested values, joins and set operations, PIVOT/UNPIVOT, MERGE, COPY, DML/RETURNING, DDL, sequences, macros, and table/file functions.

PostgreSQL, MySQL, and DuckDB use the same preparation model:

query.prepare(.psql)
query.prepare(.mysql)
query.prepare(.duck)

Shared semantic values

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).plain

will give:

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'

Swift 6

SQL 2.0.0 requires Swift 6.3+ and uses Swift 6 language mode.

The query/bind graph is intentionally not hidden behind unchecked Sendable conformances. Build/prepare on the originating isolation and move an application-owned Sendable snapshot across actor boundaries when needed.

Advanced migration notes

If you have extensions that manually concatenate SQLable.parts, use the structural composition helpers so statement/subquery ownership is preserved.

Predefined Fn.Name values are immutable.

was

Fn.Name.coalesce = .custom("my_coalesce")

became

let name = Fn.Name.custom("my_coalesce")
let fn = Fn.build(name)

Normal calls such as Fn.coalesce(...) stay the same.

Validation

The final candidate passed 737 tests / 63 suites on Swiftly 6.3.3, Xcode 26.6 / Swift 6.3.3, and Xcode 27 / Swift 6.4. Fresh normal-import clients also passed on all three toolchains with the Query shorthand, explicit SQLContent, all 37 direct-root representatives, PostgreSQL/MySQL/DuckDB bind checks, and deprecated SwifQL fluent/builder/unary parity.

For the short v1 → v2 path and full compatibility details see:

🔌 Shared Semantic Values

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 05 Sep 12:09

SwifQL now has shared semantic values for database-facing civil dates, times, civil date-times, and structural intervals: PureDate, PureTime, DateTime, and Interval.

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
)

SwifQL
    .select(date, time, dateTime, interval)
    .prepare(.psql)
    .plain

will give:

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'

Shared semantic values

PureDate is a timezone-free proleptic-Gregorian civil date. It supports astronomical years, including year zero and extended years, together with explicit positive and negative infinity states.

PureTime is a nanosecond-capable time of day, not an elapsed duration. Its domain includes the distinct 24:00:00 endpoint.

DateTime combines a civil date and time without introducing timezone or instant semantics. Exact 24:00:00 input canonicalizes to the following day's midnight.

Interval preserves independent signed months, days, and microseconds. It is structural rather than a flattened duration and intentionally does not conform to Comparable.

PureDate and DateTime can bridge to Foundation.Date with an explicit Gregorian Calendar and TimeZone. That conversion is exact and failable rather than silently normalizing unsupported values.

Binding and schema inference

All four values use the ordinary SwifQL value/binding pipeline, so prepared queries keep the original Swift values and their traversal order.

Automatic inference remains intentionally conservative:

PureDate        -> .date
PureTime        -> .time
Foundation.Date -> .timestamptz
DateTime        -> .text
Interval        -> .text

Use explicit .timestamp or .interval schema types when those contracts are intended for DateTime or Interval.

Dialect behavior

PostgreSQL renders the shared values as native DATE, TIME, TIMESTAMP, and INTERVAL expressions while preserving their civil semantics.

Duck uses its exact supported forms, including TIME_NS and TIMESTAMP_NS for nanosecond lexical precision. Positive extended dates adapt only their parser spelling, while the Swift value keeps its canonical identity. TIMESTAMP_NS still has a finite physical range, and shared interval infinity is not advertised as native Duck interval infinity.

MySQL follows an exact-or-hard-fail policy. Supported finite dates and date-times render as DATE / DATETIME, and time fractions must be exactly representable at MySQL microsecond precision. Unsupported years, special states, or non-microsecond nanoseconds are not silently rounded or coerced.

Compatibility

This release is additive. Existing SwifQL query source does not need to migrate to use the existing APIs.

🚀 Move to Swift 6 and expand SQL support

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 29 Aug 00:15

SwifQL 2 now runs in Swift 6 language mode, with a much broader SQL surface and safer query composition.

Install it with:

.package(
    url: "https://github.com/SwifQL/SwifQL",
    exact: "2.0.0-beta.5.0.0"
)

Swift 6

SwifQL now uses:

// swift-tools-version:6.0

and:

swiftLanguageModes: [.v6]

The macOS deployment target is still 10.15.

Normal query source stays familiar:

let query = SwifQL
    .select(\User.email, \User.name)
    .from(User.table)
    .where(\User.email == "john@gmail.com")
    .orderBy(.asc(\User.name))
    .limit(10)

More SQL

A lot of new SQL can now be expressed directly through the same SwifQL DSL.

PIVOT

let cities = Path.Table("cities")

let query = SwifQL
    .pivot(cities)
    .on(cities.column("year"), in: 2000, 2010)
    .using(Fn.sum(cities.column("population")) => "total")
    .groupBy(cities.column("country"))
    .orderBy(.desc(cities.column("country")))
    .limit(2)

will give:

PIVOT "cities" ON "year" IN (2000, 2010) USING sum("population") as "total" GROUP BY "country" ORDER BY "country" DESC LIMIT 2

MERGE

let target = Path.Table("merge_target")
let source = Path.Table("merge_source")

SwifQL.merge(
    into: target,
    using: source,
    on: target.column("id") == source.column("id")
)

will give:

MERGE INTO "merge_target" USING "merge_source" ON "merge_target"."id" = "merge_source"."id"

The incremental form works too:

SwifQL
    .merge(into: target)
    .using(source)
    .on(target.column("id") == source.column("id"))

COPY

let events = Path.Table("events")

SwifQL.copy(
    events,
    to: "events.csv",
    options: .format("csv"), .header
)

will give:

COPY "events" TO 'events.csv' (FORMAT 'csv', HEADER)

There is much more in this beta: SELECT analytics, JSON, nested types/values, LIST/lambda helpers, joins, set operations, star/COLUMNS, UNPIVOT, DML/RETURNING, common DDL, sequences, macros, ATTACH/DETACH/USE, table/file functions, and more.

Dialects

SwifQL 2 keeps the same preparation model across PostgreSQL, MySQL, and DuckDB:

query.prepare(.psql)
query.prepare(.mysql)
query.prepare(.duck)

and:

SQLDialect.all // [.psql, .mysql, .duck]

Breaking change: structural parts

This affects advanced extensions that manually concatenate SwifQLable.parts.

was

extension SwifQLable {
    func appendingMyFragment(_ fragment: SwifQLable) -> SwifQLable {
        SwifQLableParts(parts: self.parts + fragment.parts)
    }
}

became

extension SwifQLable {
    func appendingMyFragment(_ fragment: SwifQLable) -> SwifQLable {
        structurallyAppending(fragment)
    }
}

Real statement/subquery/set-result regions are now preserved structurally, so query ownership survives type erasure, copied parts, nested SQL, builders, set operations, PIVOT/UNPIVOT, and other composition paths.

Normal SQL-shaped query source does not need to manipulate those frames.

Breaking change: predefined Fn.Name values are immutable

was

Fn.Name.coalesce = .custom("my_coalesce")

became

let name = Fn.Name.custom("my_coalesce")
let fn = Fn.build(name)

Normal function calls stay the same:

SwifQL.select(Fn.coalesce("hello", "world"))

Strict concurrency and actors

SwifQL is clean under Swift 6 strict concurrency, but query/bind graphs are intentionally not marked Sendable when that would be untrue.

For actor-based wrappers use this shape:

build/prepare SwifQL on caller isolation
        ↓
convert to your own Sendable snapshot
        ↓
send the snapshot to the actor

Do not send SwifQLable, SwifQLSplittedQuery, or arbitrary [Encodable] binds across actors by hiding them behind unchecked conformances.

A complete example is in MIGRATION.md.

raw(_:) fix

Static raw now uses the text you pass:

SwifQLableParts.raw("TAIL").prepare(.psql).plain
// " TAIL"

"TAIL".raw.prepare(.psql).plain
// "TAIL"

Validation

Apple Swift 6.3.3
448 tests / 42 suites
concurrency errors: 0
concurrency warnings: 0

The current DuckDB SQL support was also validated against DuckDB v1.5.5.

For migration details see MIGRATION.md, and for the full current overview see RELEASE_NOTES.md.

🐼 Order by random using new `SwifQLHybridOperator` by @tierracero #53

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 31 May 00:28
bc0a1ad

This allows to use ORDER BY random() operator with one handler that adapts to the proper syntax of psql and mysql
e.g. .orderBy(.random) will be processed as: ORDER BY .rand() in MySQL, and ORDER BY .random() in Postgres

by @tierracero #53

👨‍🔧Implement SwifQLNull

Pre-release

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 31 Oct 03:59

By default when you compare SwifQLable with nil it uses IS NULL

"hello" == nil
// produce: 'hello' IS NULL

to reach exact = NULL use this way

"hello" == SwifQLNull
// produce: 'hello' = NULL

⚡️Conform `Operator` to `SwifQLable`

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 01 Jun 21:18
1e94765

So now you will be able to use operators right in query like this

SwifQL.select(Operator.null)

🪚 Implement `KeypathEncodable`

Pre-release

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 09 May 00:24
2ffcc11

In case if you use @column names with underscores but variable names are in camelCase you may experience problems when while encoding fetched data you're getting names with underscores.

To solve this problem now we have KeyPathEncodable protocol.

Simply conform your table to KeyPathEncodable and it will always be encoded with property names the same as variable names declared in your model.

Examples

To get as-is

final class User: Table {
    @Column("id")
    var id: Int
    
    @Column("first_name")
    var firstName: String
}

this will be encoded as

{
    "id": 1,
    "first_name": "John"
}

To get with variable names

final class User: Table, KeyPathEncodable {
    @Column("id")
    var id: Int
    
    @Column("first_name")
    var firstName: String
}

this will be encoded as

{
    "id": 1,
    "firstName": "John"
}

🪚 Column, Alias, Table, and builders

Pre-release

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 22 Apr 23:31
fea1745

Breaking changes

SwifQLAlias renamed to TableAlias

Now we're able to build table models

final class User: Table {
    @Column("id")
    var id: UUID

    @Column("email")
    var email: String
}

and access table columns through keyPath like this \User.$id and \User.$email

New @Alias property wrapper

struct Result: Aliasable {
    @Alias("anything") var emailAddress: String
}
let query = SwifQL.select(\User.$email => \Result.$emailAddress).from(User.table)
// SELECT "User"."email" as "anything" FROM "User"

🍾 Implement schema support

Pre-release

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 13 Apr 00:32
0f284aa

Breaking change

Aliases syntax has been changed

was

let u = User.as("u")
u~\.$id

became

let u = User.as("u")
u.$id

Schema

schema for postgres is the same as database for mysql.

How to declare some schema

struct Deleted: Schemable {
    static var schemaName: String { "deleted" }
}

or right into your model (example for Bridges)

final class DeletedUser: Table, Schemable {
    static var schemaName: String { "deleted" }
    static var tableName: String { "user" }
    
    @Column("id")
    var id: UUID

    @Column("email")
    var email: String
}

Usage examples

// if `User` model conforms to `Schemable` it will use `public` schema all the time
\User.$id // result: "public"."user"."id"

// otherwise it will be printed simply without any schema
\User.$id // result: "user"."id"

// `DeletedUser` model conforms to `Schemable` and has custom schema name
\DeletedUser.$id // result: "deleted"."user"."id"

// Alternatively we can wrap any model to any schema
User.inSchema("deleted").$id // result: "deleted"."user"."id"

// also we can use aliases with tables with schemas
DeletedUser.as("du") // result: "deleted"."user" as "du"
// or
User.inSchema("deleted").as("du") // result: "deleted"."user" as "du"

New alias features

let u = User.as("u")

// to reach \User.$id we now can simply call
u.$id // result: "u"."id"

// the same for aliases with schemas
// declare aliases easily
let hu = User.inSchema("hello").as("hu")
// or even
struct HelloSchema: Schemable {
    static var schemaName: String { "hello" }
}
let hu = User.inSchema(HelloSchema.self).as("hu")

// use alias as table
hu.table // result: "hello"."user" as "hu"

// or simply call its columns
hu.$id // result: "hu"."id"

Custom aliases for subqueries

let u = SwifQLAlias("u")
let subQueryWithAlias = |SwifQL.select(User.table.*).from(User.table)| => u
// result: (SELECT "hello"."user".* FROM "hello"."user") as "u"

// and then also use its paths easily
u.id // result: "u"."id"
// even with any fantasy columns
u.heeeeey // result: "u"."heeeeey"

🪚 Add UNION and WITH (#19)

Choose a tag to compare

@MihaelIsaev MihaelIsaev released this 17 Jan 07:34

union functionality

let table1 = Table("Table1")
let table2 = Table("Table2")
let table3 = Table("Table3")

let sql = Union(
    SwifQL.select(table1.*).from(table1),
    SwifQL.select(table2.*).from(table2),
    SwifQL.select(table3.*).from(table3)
)

will give

(SELECT "Table1".* FROM "Table1")
UNION
(SELECT "Table2".* FROM "Table2")
UNION
(SELECT "Table3".* FROM "Table3"

with support

let with = With(
    Table("Table1"),
    SwifQL.select(Table("Table2").*).from(Table("Table2"))
)
SwifQL.with(with)
      .select(Table("Table1").*)
      .from(Table("Table1"))

will give

WITH
    "Table1" as (SELECT "Table2".* FROM "Table2")
SELECT "Table1".* 
FROM "Table1"

Thanks to @hiimtmac