Skip to content

Commit e56c2ae

Browse files
committed
Merge remote-tracking branch 'origin/main' into default-date-uuid-representations
2 parents 576637e + 6a7e966 commit e56c2ae

File tree

13 files changed

+272
-50
lines changed

13 files changed

+272
-50
lines changed

Sources/StructuredQueries/Macros.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,16 @@ import StructuredQueriesCore
1616
named(init(_:)),
1717
named(init(decoder:)),
1818
named(QueryValue),
19+
named(schemaName),
1920
named(tableName)
2021
)
2122
@attached(
2223
memberAttribute
2324
)
24-
public macro Table(_ name: String? = nil) =
25+
public macro Table(
26+
_ name: String? = nil,
27+
schema schemaName: String? = nil
28+
) =
2529
#externalMacro(
2630
module: "StructuredQueriesMacros",
2731
type: "TableMacro"

Sources/StructuredQueriesCore/Documentation.docc/Articles/QueryCookbook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ large and complex queries.
1212
* [Reusable column queries](#Reusable-column-queries)
1313
* [Default scopes](#Default-scopes)
1414
* [Custom selections](#Custom-selections)
15-
* [Pre-loading associations with JSON](#Pre-loading-associations-with-json)
15+
* [Pre-loading associations with JSON](#Pre-loading-associations-with-JSON)
1616

1717
### Reusable table queries
1818

Sources/StructuredQueriesCore/Documentation.docc/Articles/SafeSQLStrings.md

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,27 @@ could be written as a single invocation of the macro:
108108
All of the columns provided to trailing closures in the query builder are available statically on
109109
each table type, so you can freely interpolate this schema information into the SQL string.
110110

111+
> Important: _Always_ interpolate as much static schema information as possible into the SQL string
112+
> to better ensure that queries are correct and will successfully decode.
113+
>
114+
> For example:
115+
>
116+
> ```diff
117+
> -SELECT * FROM reminders
118+
> +SELECT \(Reminder.columns) FROM \(Reminder.self)
119+
> ```
120+
>
121+
> * Selecting "`*`" requires that the column order in the database matches the field order in the
122+
> Swift data type. Because StructuredQueries decodes columns in positional order, a query using
123+
> "`*`" will fail to decode unless the field order matches exactly. Instead of leaving this to
124+
> chance, prefer interpolating `Table.columns`, which will generate an explicit SQL column
125+
> selection that matches the order of fields in the Swift data type.
126+
> * Spelling out table and column names directly inside the query (_e.g._ "`reminders`") can lead
127+
> to runtime errors due to typos or stale queries that refer to schema columns that have been
128+
> renamed or removed. Instead, prefer interpolating `Table.columnName` to refer to a particular
129+
> column (_e.g._, `Reminder.isCompleted`), and `Table.self` to refer to a table (_e.g._,
130+
> `Reminder.self`).
131+
111132
Note that the query's represented type cannot be inferred here, and so the `as` parameter is used
112133
to let Swift know that we expect to decode the `Reminder` type when we execute the query.
113134
@@ -131,11 +152,12 @@ Values can be interpolated into `#sql` strings to produce dynamic queries:
131152
@Column {
132153
```swift
133154
let isCompleted = true
155+
134156
#sql(
135157
"""
136158
SELECT count(*)
137-
FROM reminders
138-
WHERE isCompleted = \(isCompleted)
159+
FROM \(Reminder.self)
160+
WHERE \(Reminder.isCompleted) = \(isCompleted)
139161
""",
140162
as: Reminder.self
141163
)
@@ -144,22 +166,22 @@ Values can be interpolated into `#sql` strings to produce dynamic queries:
144166
@Column {
145167
```sql
146168
SELECT count(*)
147-
FROM reminders
148-
WHERE isCompleted = ?
169+
FROM "reminders"
170+
WHERE "reminders"."isCompleted" = ?
149171
-- [1]
150172
```
151173
}
152174
}
153175

154-
Note that although it seems the literal value is being interpolated directly into the string, that
176+
Note that although it seems that `isCompleted` is being interpolated directly into the string, that
155177
is not what is happening. The interpolated value is captured as a separate statement binding in
156178
order to protect against SQL injection.
157179

158-
String bindings are handled in a special fashion to make it clear what the intended usage is. If
159-
you interpolate a string into a `#sql` string, you will get a deprecation warning:
180+
String bindings are handled in a special fashion to make it clear what the intended usage is. If you
181+
interpolate a string into a `#sql` string, you will get a deprecation warning:
160182

161183
```swift
162-
let searchText = "get"
184+
let searchText = "%get%"
163185
#sql(
164186
"""
165187
SELECT \(Reminder.columns)
@@ -178,7 +200,7 @@ If you mean to bind the string as a value, you can update the interpolation to u
178200
@Row {
179201
@Column {
180202
```swift
181-
let searchText = "get"
203+
let searchText = "%get%"
182204
#sql(
183205
"""
184206
SELECT \(Reminder.columns)
@@ -205,12 +227,12 @@ If you mean to interpolate the string directly into the SQL you can use
205227
@Row {
206228
@Column {
207229
```swift
208-
let searchText = "get"
230+
let searchText = "%get%"
209231
#sql(
210232
"""
211233
SELECT \(Reminder.columns)
212234
FROM \(Reminder.self)
213-
WHERE \(Reminder.title) COLLATE NOCASE LIKE '%\(raw: searchText)%'
235+
WHERE \(Reminder.title) COLLATE NOCASE LIKE '\(raw: searchText)'
214236
""",
215237
as: Reminder.self
216238
)

Sources/StructuredQueriesCore/QueryFragment.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,10 @@ extension QueryFragment: ExpressibleByStringInterpolation {
262262
///
263263
/// - Parameter table: A table.
264264
public mutating func appendInterpolation<T: Table>(_ table: T.Type) {
265+
if let schemaName = table.schemaName {
266+
appendInterpolation(quote: schemaName)
267+
appendLiteral(".")
268+
}
265269
appendInterpolation(quote: table.tableAlias ?? table.tableName)
266270
}
267271

Sources/StructuredQueriesCore/Statements/CommonTableExpression.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ public struct With<QueryValue>: Statement {
3939

4040
public struct CommonTableExpressionClause: QueryExpression {
4141
public typealias QueryValue = ()
42-
let tableName: String
42+
let tableName: QueryFragment
4343
let select: QueryFragment
4444
public var queryFragment: QueryFragment {
45-
"\(quote: tableName) AS (\(.newline)\(select.indented())\(.newline))"
45+
"\(tableName) AS (\(.newline)\(select.indented())\(.newline))"
4646
}
4747
}
4848

@@ -55,7 +55,7 @@ public enum CommonTableExpressionBuilder {
5555
public static func buildExpression<CTETable: Table>(
5656
_ expression: some PartialSelectStatement<CTETable>
5757
) -> CommonTableExpressionClause {
58-
CommonTableExpressionClause(tableName: CTETable.tableName, select: expression.query)
58+
CommonTableExpressionClause(tableName: "\(CTETable.self)", select: expression.query)
5959
}
6060

6161
public static func buildBlock(

Sources/StructuredQueriesCore/Statements/Delete.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,11 @@ extension Delete: Statement {
135135
public typealias QueryValue = Returning
136136

137137
public var query: QueryFragment {
138-
var query: QueryFragment = "DELETE FROM \(quote: From.tableName)"
138+
var query: QueryFragment = "DELETE FROM "
139+
if let schemaName = From.schemaName {
140+
query.append("\(quote: schemaName).")
141+
}
142+
query.append("\(quote: From.tableName)")
139143
if let tableAlias = From.tableAlias {
140144
query.append(" AS \(quote: tableAlias)")
141145
}

Sources/StructuredQueriesCore/Statements/Insert.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,7 +403,11 @@ extension Insert: Statement {
403403
if let conflictResolution {
404404
query.append(" OR \(conflictResolution)")
405405
}
406-
query.append(" INTO \(quote: Into.tableName)")
406+
query.append(" INTO ")
407+
if let schemaName = Into.schemaName {
408+
query.append("\(quote: schemaName).")
409+
}
410+
query.append("\(quote: Into.tableName)")
407411
if let tableAlias = Into.tableAlias {
408412
query.append(" AS \(quote: tableAlias)")
409413
}

Sources/StructuredQueriesCore/Statements/Select.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1410,7 +1410,11 @@ extension Select: SelectStatement {
14101410
query.append(" DISTINCT")
14111411
}
14121412
query.append(" \(columns.joined(separator: ", "))")
1413-
query.append("\(.newlineOrSpace)FROM \(quote: From.tableName)")
1413+
query.append("\(.newlineOrSpace)FROM ")
1414+
if let schemaName = From.schemaName {
1415+
query.append("\(quote: schemaName).")
1416+
}
1417+
query.append("\(quote: From.tableName)")
14141418
if let tableAlias = From.tableAlias {
14151419
query.append(" AS \(quote: tableAlias)")
14161420
}

Sources/StructuredQueriesCore/Statements/Update.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,14 @@ extension Update: Statement {
196196
guard !updates.isEmpty
197197
else { return "" }
198198

199-
var query: QueryFragment = "UPDATE"
199+
var query: QueryFragment = "UPDATE "
200200
if let conflictResolution {
201-
query.append(" OR \(conflictResolution)")
201+
query.append("OR \(conflictResolution) ")
202202
}
203-
query.append(" \(quote: From.tableName)")
203+
if let schemaName = From.schemaName {
204+
query.append("\(quote: schemaName).")
205+
}
206+
query.append("\(quote: From.tableName)")
204207
if let tableAlias = From.tableAlias {
205208
query.append(" AS \(quote: tableAlias)")
206209
}

Sources/StructuredQueriesCore/Table.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public protocol Table: QueryRepresentable where TableColumns.QueryValue == Self
2121
/// This property should always return `nil` unless called on a ``TableAlias``.
2222
static var tableAlias: String? { get }
2323

24+
/// The table schema's name.
25+
static var schemaName: String? { get }
26+
2427
/// A select statement for this table.
2528
///
2629
/// The default implementation of this property returns a fully unscoped query for the table
@@ -90,6 +93,10 @@ extension Table {
9093
nil
9194
}
9295

96+
public static var schemaName: String? {
97+
nil
98+
}
99+
93100
/// Returns a table column to the resulting value of a given key path.
94101
///
95102
/// Allows, _e.g._ `Reminder.columns.id` to be abbreviated `Reminder.id`, which is useful when

0 commit comments

Comments
 (0)