Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f2a3a59
Incorporate `@Selection` logic into `@Table`
stephencelis Sep 19, 2025
4b95802
wip
stephencelis Sep 20, 2025
1be6e0b
Support tuple operations with `Table` records
stephencelis Sep 20, 2025
3e15094
Support nested tables
stephencelis Sep 20, 2025
376ee57
fixes
stephencelis Sep 21, 2025
1d4e907
wip
stephencelis Sep 21, 2025
b34df28
wip
stephencelis Sep 21, 2025
922e5b3
wip
stephencelis Sep 21, 2025
fa8b00d
wip
stephencelis Sep 21, 2025
0723ede
wip
stephencelis Sep 21, 2025
1974bb0
wip
stephencelis Sep 21, 2025
3905f40
wip
stephencelis Sep 21, 2025
7800ea8
wip
stephencelis Sep 21, 2025
21e0c9a
wip
stephencelis Sep 21, 2025
c4c2d03
wip
stephencelis Sep 21, 2025
7d434e2
wip
stephencelis Sep 22, 2025
c7cac92
wip
stephencelis Sep 22, 2025
4958464
wip
stephencelis Sep 22, 2025
e07a4eb
wip
stephencelis Sep 22, 2025
9623521
Basic docs pass
stephencelis Sep 22, 2025
642fd4e
wip
stephencelis Sep 22, 2025
a69e5fb
Fixes
stephencelis Sep 22, 2025
a0a0fca
wip
stephencelis Sep 22, 2025
3d5d296
fixes
stephencelis Sep 23, 2025
ccb5151
wip
stephencelis Sep 23, 2025
f2e4981
wip
stephencelis Sep 23, 2025
f6394a3
db function support
stephencelis Sep 23, 2025
29fcd86
wip
stephencelis Sep 23, 2025
ac6f814
wip
stephencelis Sep 23, 2025
1018903
wip
stephencelis Sep 23, 2025
65f0294
wip
stephencelis Sep 23, 2025
df4b1a7
Merge remote-tracking branch 'origin/main' into table-expression
stephencelis Sep 26, 2025
4f44594
wip
stephencelis Sep 26, 2025
c424493
wip
stephencelis Sep 27, 2025
5345f66
wip
stephencelis Sep 27, 2025
9789c32
wip
stephencelis Sep 27, 2025
cdc589b
wip
stephencelis Sep 27, 2025
661cb7d
wip
stephencelis Sep 27, 2025
137b82f
wip
stephencelis Sep 27, 2025
f18feec
wip
stephencelis Sep 27, 2025
0324749
wip
stephencelis Sep 27, 2025
c477c11
wip
stephencelis Sep 27, 2025
7ee5a6b
wip
stephencelis Sep 27, 2025
4dc29da
Infer multi-column when possible
stephencelis Sep 28, 2025
cb6e29d
wip
stephencelis Sep 28, 2025
324ab9c
wip
stephencelis Sep 28, 2025
20148be
wip
stephencelis Sep 28, 2025
0465e23
wip
stephencelis Sep 28, 2025
93e4379
wip
stephencelis Sep 29, 2025
366138a
wip
stephencelis Sep 29, 2025
9bbc844
wip
stephencelis Sep 29, 2025
70ce95f
wip
stephencelis Sep 29, 2025
cdb99e1
wip
stephencelis Sep 29, 2025
c820b2b
wip
stephencelis Sep 30, 2025
cc1c9cd
Docs and tests
mbrandonw Oct 2, 2025
5ded480
wip
stephencelis Oct 2, 2025
6d06754
wip
stephencelis Oct 2, 2025
a5b1723
wip
stephencelis Oct 2, 2025
78d5081
wip
stephencelis Oct 2, 2025
d209480
wip
stephencelis Oct 2, 2025
3a795ea
Merge remote-tracking branch 'origin/main' into table-expression
stephencelis Oct 2, 2025
b82b66d
wip
mbrandonw Oct 2, 2025
0bc5a80
wip
stephencelis Oct 2, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ StructuredQueries also ships SQLite-specific helpers:

- ``Table(_:)``
- ``Column(_:as:primaryKey:)``
- ``Columns(primaryKey:)``
- ``Ephemeral()``
- ``Selection()``
- ``sql(_:as:)``
Expand Down
21 changes: 19 additions & 2 deletions Sources/StructuredQueries/Macros.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import StructuredQueriesCore
named(schemaName),
named(tableName)
)
@attached(member, names: named(Draft), named(TableColumns))
@attached(member, names: named(Draft), named(Selection), named(TableColumns))
@attached(memberAttribute)
public macro Table(
_ name: String = "",
Expand All @@ -36,7 +36,7 @@ public macro Table(
/// that don't have a single representation in SQL, like `Date` and `UUID`.
/// - generated: Allows to declare the column as a read-only database computed column, making it
/// available for queries but not for updates.
/// - primaryKey: The column is its table's auto-incrementing primary key.
/// - primaryKey: The column is its table's primary key.
@attached(peer)
public macro Column(
_ name: String = "",
Expand All @@ -49,6 +49,19 @@ public macro Column(
type: "ColumnMacro"
)

/// Customizes a group of columns generated by the ``/StructuredQueriesCore/Table`` protocol.
///
/// - Parameters primaryKey: These columns are the table's composite primary key.
@attached(peer)
public macro Columns(
// as representableType: (any QueryRepresentable.Type)? = nil,
primaryKey: Bool = false
) =
#externalMacro(
module: "StructuredQueriesMacros",
type: "ColumnsMacro"
)

/// Tells StructuredQueries not to consider the annotated property a column of the table.
///
/// Like SwiftData's `@Transient` macro, but for SQL.
Expand Down Expand Up @@ -96,6 +109,10 @@ public macro Ephemeral() =
named(init(decoder:))
)
@attached(member, names: named(Columns))
@available(iOS, deprecated: 9999, renamed: "Table")
@available(macOS, deprecated: 9999, renamed: "Table")
@available(tvOS, deprecated: 9999, renamed: "Table")
@available(watchOS, deprecated: 9999, renamed: "Table")
public macro Selection() =
#externalMacro(
module: "StructuredQueriesMacros",
Expand Down
2 changes: 1 addition & 1 deletion Sources/StructuredQueriesCore/Bind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
///
/// It is not common to interact with this type directly. A value of this type is returned from the
/// `#bind` macro.
public struct BindQueryExpression<QueryValue: QueryBindable>: QueryExpression {
public struct BindQueryExpression<QueryValue: QueryRepresentable & QueryExpression>: QueryExpression {
public let base: QueryValue

public init(
Expand Down
92 changes: 92 additions & 0 deletions Sources/StructuredQueriesCore/ColumnGroup.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/// A group of table columns.
///
/// Don't create instances of this value directly. Instead, use the `@Table` and `@Columns` macros
/// to generate values of this type.
@dynamicMemberLookup
public struct ColumnGroup<Root: Table, Values: Table>: _TableColumnExpression
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bikeshed opportunity: Should we call this TableColumnGroup for consistent prefixing?

where Values.QueryOutput == Values {
public typealias Value = Values

public var _names: [String] { Values.TableColumns.allColumns.map(\.name) }

public typealias QueryValue = Values

public static func allColumns(keyPath: KeyPath<Root, Values>) -> [any TableColumnExpression] {
return Values.TableColumns.allColumns.map { column in
func open<R, V>(
_ column: some TableColumnExpression<R, V>
) -> any TableColumnExpression {
let keyPath = keyPath.appending(
path: unsafeDowncast(column.keyPath, to: KeyPath<Values, V.QueryOutput>.self)
)
return TableColumn<Root, V>(
column.name,
keyPath: keyPath,
default: column.defaultValue
)
}
return open(column)
}
}

public static func writableColumns(
keyPath: KeyPath<Root, Values>
) -> [any WritableTableColumnExpression] {
return Values.TableColumns.writableColumns.map { column in
func open<R, V>(
_ column: some WritableTableColumnExpression<R, V>
) -> any WritableTableColumnExpression {
let keyPath = keyPath.appending(
path: unsafeDowncast(column.keyPath, to: KeyPath<Values, V.QueryOutput>.self)
)
return TableColumn<Root, V>(
column.name,
keyPath: keyPath,
default: column.defaultValue
)
}
return open(column)
}
}

public let keyPath: KeyPath<Root, Values>

public init(keyPath: KeyPath<Root, Values>) {
self.keyPath = keyPath
}

public var queryFragment: QueryFragment {
ColumnGroup.allColumns(keyPath: keyPath).map(\.queryFragment).joined(separator: ", ")
}

public subscript<Member>(
dynamicMember keyPath: KeyPath<Values.TableColumns, TableColumn<Values, Member>>
) -> TableColumn<Root, Member> {
let column = Values.columns[keyPath: keyPath]
return TableColumn<Root, Member>(
column.name,
keyPath: self.keyPath.appending(path: column.keyPath),
default: column.defaultValue
)
}

public subscript<Member>(
dynamicMember keyPath: KeyPath<Values.TableColumns, GeneratedColumn<Values, Member>>
) -> GeneratedColumn<Root, Member> {
let column = Values.columns[keyPath: keyPath]
return GeneratedColumn<Root, Member>(
column.name,
keyPath: self.keyPath.appending(path: column.keyPath),
default: column.defaultValue
)
}

public subscript<Member>(
dynamicMember keyPath: KeyPath<Values.TableColumns, ColumnGroup<Values, Member>>
) -> ColumnGroup<Root, Member> {
let column = Values.columns[keyPath: keyPath]
return ColumnGroup<Root, Member>(
keyPath: self.keyPath.appending(path: column.keyPath)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ or recursive queries of trees and graphs.
## Overview

One can build [common table expressions][] (commonly referred to as CTEs) by using the ``With``
statement, along with the `@Table` and `@Selection` macros. CTEs allow you to refactor complex
queries into smaller pieces, and they allow you to execute recursive queries that can traverse
tree-like and graph-like tables.
statement, along with the `@Table` macro. CTEs allow you to refactor complex queries into smaller
pieces, and they allow you to execute recursive queries that can traverse tree-like and graph-like
tables.

[common table expressions]: https://sqlite.org/lang_with.html

Expand Down Expand Up @@ -47,15 +47,17 @@ selecting only lists whose average priority of reminders is greater than 1.5 int
that can then be used in another query.

One can define a new type that represents the data you want to pre-compute as a CTE, and you
annotate the type with both `@Table` and `@Selection`:
annotate the type with the `@Table`:

```swift
@Table @Selection
@Table
struct HighPriorityRemindersList {
let id: RemindersList.ID
}
```

You can think of this as a "virtual table" of sorts, rather than an actual database table.

Then you can select into this table in the first trailing closure of ``With``:

```swift
Expand Down Expand Up @@ -171,7 +173,7 @@ As a simple example let's construct a query that selects the numbers from 1 to 1
by defining a CTE data type that holds the data we want to compute:

```swift
@Table @Selection
@Table
struct Counts {
let value: Int
}
Expand Down Expand Up @@ -263,13 +265,13 @@ This constructs the CTE table from which we can select and limit to the first 10
The previous query was a simple example of what is known as a "[recurrence relation][]."
Another example of a recurrence relation is the Fibonacci sequence, where each term in
the sequence is the sum of the previous two terms. We can construct a query to compute the
first 10 Fibonacci numbers by first defining a table selection data type that holds an index
and its corresponding Fibonacci number, as well as the previous Fibonacci number:
first 10 Fibonacci numbers by first defining a data type that holds an index and its corresponding
Fibonacci number, as well as the previous Fibonacci number:

[recurrence relation]: https://en.wikipedia.org/wiki/Recurrence_relation

```swift
@Table @Selection
@Table
private struct Fibonacci {
let n: Int
let prevFib: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -766,11 +766,11 @@ struct ReminderResult: QueryRepresentable {
)
```

There is also a way to streamline providing the ``QueryRepresentable`` conformance. You can apply the
`@Selection` macro to your type to generate that conformance for you automatically:
There is also a way to streamline providing the ``QueryRepresentable`` conformance. You can use the
`@Table` to describe the datatype you want to decode:

```swift
@Selection
@Table
struct ReminderResult {
let title: String
let isCompleted: Bool
Expand All @@ -784,4 +784,7 @@ struct ReminderResult {
)
```

The `@Table` macro can be used to describe any table-like entity. This includes virtual tables,
database views, common table expressions, and even groups of columns that you'd like to decode.

See <doc:SafeSQLStrings> for more information about the `#sql` macro.
Original file line number Diff line number Diff line change
Expand Up @@ -261,17 +261,21 @@ you can use the ``Table/unscoped`` property:

It will often be the case that you want to select very specific data from your database and then
decode that data into a custom Swift data type. For example, if you are displaying a list of
reminders and only need their titles for the list, it would be wasteful to decode an array of
all reminder data. The `@Selection` macro allows you to define a custom data type of only the
fields you want to decode:
reminders and only need their titles for the list, it would be wasteful to decode an array of all
reminder data. The `@Table` macro allows you to define a custom data type of only the fields you
want to decode:

```swift
@Selection
@Table
struct ReminderTitle {
let title: String
}
```

> Note: The `@Table` macro can be used to describe not only database tables, but a number of
> table-like things, including virtual tables, database views, common table expressions, and even
> simply groups of columns you'd like to decode together.

Then when selecting the columns for your query you can use this data type:

@Row {
Expand All @@ -297,10 +301,10 @@ Then when selecting the columns for your query you can use this data type:
}

As another example, consider the query that selects all reminders lists with the count of reminders
in each list. A selection data type can be defined like so:
in each list. A data type can be defined like so:

```swift
@Selection
@Table
struct RemindersListWithCount {
let remindersCount: Int
let remindersList: RemindersList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,10 @@ To add (or remove) a `DISTINCT` clause from a selection, use ``Select/distinct(_
}

To bundle selected columns up into a custom data type, you can annotate a struct of decoded results
with the `@Selection` macro:
with the `@Table` macro:

```swift
@Selection
@Table
struct ReminderResult {
let title: String
let isCompleted: Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@
- ``TableColumns``
- ``TableColumn``
- ``TableColumnExpression``
- ``ColumnGroup``
- ``TableDefinition``
- ``TableExpression``

### Scoping

Expand Down
6 changes: 6 additions & 0 deletions Sources/StructuredQueriesCore/Never.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ extension Never: Table {
public static var writableColumns: [any WritableTableColumnExpression] { [] }
}

public struct Selection: TableExpression {
public typealias QueryValue = Never

public var allColumns: [any QueryExpression] { [] }
}

public static var columns: TableColumns {
TableColumns()
}
Expand Down
22 changes: 9 additions & 13 deletions Sources/StructuredQueriesCore/Operators.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
extension QueryExpression where QueryValue: QueryBindable {
extension QueryExpression where QueryValue: _OptionalPromotable {
/// A predicate expression indicating whether two query expressions are equal.
///
/// ```swift
Expand Down Expand Up @@ -241,7 +241,7 @@ public func != <QueryValue: QueryBindable>(
SQLQueryExpression(lhs).isNot(rhs)
}

extension QueryExpression where QueryValue: QueryBindable {
extension QueryExpression where QueryValue: _OptionalPromotable {
/// Returns a predicate expression indicating whether the value of the first expression is less
/// than that of the second expression.
///
Expand Down Expand Up @@ -697,7 +697,7 @@ extension QueryExpression where QueryValue == String {
/// - Parameter collation: A collating sequence name.
/// - Returns: An expression that is compared using the given collating sequence.
public func collate(_ collation: Collation) -> some QueryExpression<QueryValue> {
BinaryOperator(lhs: self, operator: "COLLATE", rhs: collation)
SQLQueryExpression("\(self) COLLATE \(collation)")
}

/// A predicate expression from this string expression matched against another _via_ the `GLOB`
Expand Down Expand Up @@ -816,7 +816,7 @@ extension SQLQueryExpression<String> {
}
}

extension QueryExpression where QueryValue: QueryBindable {
extension QueryExpression where QueryValue: QueryExpression {
/// Returns a predicate expression indicating whether the expression is in a sequence.
///
/// - Parameter expression: A sequence of expressions.
Expand Down Expand Up @@ -850,11 +850,7 @@ extension QueryExpression where QueryValue: QueryBindable {
_ lowerBound: some QueryExpression<QueryValue>,
and upperBound: some QueryExpression<QueryValue>
) -> some QueryExpression<Bool> {
BinaryOperator(
lhs: self,
operator: "BETWEEN",
rhs: SQLQueryExpression("\(lowerBound) AND \(upperBound)")
)
SQLQueryExpression("\(self) BETWEEN \(lowerBound) AND \(upperBound)")
}
}

Expand Down Expand Up @@ -952,7 +948,7 @@ struct BinaryOperator<QueryValue>: QueryExpression {
}

var queryFragment: QueryFragment {
"(\(lhs) \(`operator`) \(rhs))"
"(\(lhs)) \(`operator`) (\(rhs))"
}
}

Expand All @@ -976,15 +972,15 @@ private struct LikeOperator<
}
}

extension Sequence where Element: QueryExpression, Element.QueryValue: QueryBindable {
extension Sequence where Element: QueryExpression, Element.QueryValue: QueryExpression {
fileprivate typealias Expression = _SequenceExpression<Self>
}

private struct _SequenceExpression<S: Sequence>: QueryExpression
where S.Element: QueryExpression, S.Element.QueryValue: QueryBindable {
where S.Element: QueryExpression, S.Element.QueryValue: QueryExpression {
typealias QueryValue = S
let queryFragment: QueryFragment
init(elements: S) {
queryFragment = "(\(elements.map(\.queryFragment).joined(separator: ", ")))"
queryFragment = elements.map { "(\($0.queryFragment))" }.joined(separator: ", ")
}
}
Loading