Skip to content

Commit 6a7e966

Browse files
authored
Add support for explicit schema names (pointfreeco#43)
* Add support for explicit schema names * Update Sources/StructuredQueriesTestSupport/AssertQuery.swift
1 parent 6899254 commit 6a7e966

File tree

11 files changed

+238
-38
lines changed

11 files changed

+238
-38
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/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

Sources/StructuredQueriesMacros/TableMacro.swift

Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,32 +49,56 @@ extension TableMacro: ExtensionMacro {
4949
let selfRewriter = SelfRewriter(
5050
selfEquivalent: type.as(IdentifierTypeSyntax.self)?.name ?? "QueryValue"
5151
)
52-
let tableName: ExprSyntax
53-
if case let .argumentList(arguments) = node.arguments,
54-
let expression = arguments.first?.expression
55-
{
56-
if node.attributeName.identifier == "_Draft" {
57-
let memberAccess = expression.cast(MemberAccessExprSyntax.self)
58-
let base = memberAccess.base!
59-
draftTableType = TypeSyntax("\(base)")
60-
tableName = "\(base).tableName"
61-
} else {
62-
if !expression.isNonEmptyStringLiteral {
63-
diagnostics.append(
64-
Diagnostic(
65-
node: expression,
66-
message: MacroExpansionErrorMessage("Argument must be a non-empty string literal")
67-
)
68-
)
52+
var schemaName: ExprSyntax?
53+
var tableName = ExprSyntax(
54+
StringLiteralExprSyntax(
55+
content: declaration.name.trimmed.text.lowerCamelCased().pluralized()
56+
)
57+
)
58+
if case let .argumentList(arguments) = node.arguments {
59+
for argumentIndex in arguments.indices {
60+
let argument = arguments[argumentIndex]
61+
switch argument.label {
62+
case nil:
63+
if node.attributeName.identifier == "_Draft" {
64+
let memberAccess = argument.expression.cast(MemberAccessExprSyntax.self)
65+
let base = memberAccess.base!
66+
draftTableType = TypeSyntax("\(base)")
67+
tableName = "\(base).tableName"
68+
} else {
69+
if !argument.expression.isNonEmptyStringLiteral {
70+
diagnostics.append(
71+
Diagnostic(
72+
node: argument.expression,
73+
message: MacroExpansionErrorMessage("Argument must be a non-empty string literal")
74+
)
75+
)
76+
}
77+
tableName = argument.expression.trimmed
78+
}
79+
80+
case let .some(label) where label.text == "schema":
81+
if node.attributeName.identifier == "_Draft" {
82+
let memberAccess = argument.expression.cast(MemberAccessExprSyntax.self)
83+
let base = memberAccess.base!
84+
draftTableType = TypeSyntax("\(base)")
85+
schemaName = "\(base).schemaName"
86+
} else {
87+
if !argument.expression.isNonEmptyStringLiteral {
88+
diagnostics.append(
89+
Diagnostic(
90+
node: argument.expression,
91+
message: MacroExpansionErrorMessage("Argument must be a non-empty string literal")
92+
)
93+
)
94+
}
95+
schemaName = argument.expression.trimmed
96+
}
97+
98+
case let argument?:
99+
fatalError("Unexpected argument: \(argument)")
69100
}
70-
tableName = expression.trimmed
71101
}
72-
} else {
73-
tableName = ExprSyntax(
74-
StringLiteralExprSyntax(
75-
content: declaration.name.trimmed.text.lowerCamelCased().pluralized()
76-
)
77-
)
78102
}
79103
for member in declaration.memberBlock.members {
80104
guard
@@ -545,6 +569,12 @@ extension TableMacro: ExtensionMacro {
545569
}
546570

547571
var typeAliases: [DeclSyntax] = []
572+
var letSchemaName: DeclSyntax?
573+
if let schemaName {
574+
letSchemaName = """
575+
public static let schemaName: Swift.String? = \(schemaName)
576+
"""
577+
}
548578
var initDecoder: DeclSyntax?
549579
if declaration.hasMacroApplication("Selection") {
550580
conformances.append("\(moduleName).PartialSelectStatement")
@@ -579,7 +609,7 @@ extension TableMacro: ExtensionMacro {
579609
}
580610
}\(draft)\(typeAliases, separator: "\n")
581611
public static let columns = TableColumns()
582-
public static let tableName = \(tableName)\(initDecoder)\(initFromOther)
612+
public static let tableName = \(tableName)\(letSchemaName)\(initDecoder)\(initFromOther)
583613
}
584614
"""
585615
)

Tests/StructuredQueriesMacrosTests/TableMacroTests.swift

Lines changed: 80 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,15 +198,15 @@ extension SnapshotTests {
198198
@Test func tableNameEmpty() {
199199
assertMacro {
200200
"""
201-
@Table(nil)
201+
@Table("")
202202
struct Foo {
203203
var bar: Int
204204
}
205205
"""
206206
} diagnostics: {
207207
"""
208-
@Table(nil)
209-
┬─
208+
@Table("")
209+
┬─
210210
╰─ 🛑 Argument must be a non-empty string literal
211211
struct Foo {
212212
var bar: Int
@@ -215,6 +215,83 @@ extension SnapshotTests {
215215
}
216216
}
217217

218+
@Test func schemaName() {
219+
assertMacro {
220+
"""
221+
@Table("bar", schema: "foo")
222+
struct Bar {
223+
var baz: Int
224+
}
225+
"""
226+
} expansion: {
227+
#"""
228+
struct Bar {
229+
var baz: Int
230+
}
231+
232+
extension Bar: StructuredQueries.Table {
233+
public struct TableColumns: StructuredQueries.TableDefinition {
234+
public typealias QueryValue = Bar
235+
public let baz = StructuredQueries.TableColumn<QueryValue, Int>("baz", keyPath: \QueryValue.baz)
236+
public static var allColumns: [any StructuredQueries.TableColumnExpression] {
237+
[QueryValue.columns.baz]
238+
}
239+
}
240+
public static let columns = TableColumns()
241+
public static let tableName = "bar"
242+
public static let schemaName: Swift.String? = "foo"
243+
public init(decoder: inout some StructuredQueries.QueryDecoder) throws {
244+
let baz = try decoder.decode(Int.self)
245+
guard let baz else {
246+
throw QueryDecodingError.missingRequiredColumn
247+
}
248+
self.baz = baz
249+
}
250+
}
251+
"""#
252+
}
253+
}
254+
255+
@Test func schemaNameNil() {
256+
assertMacro {
257+
"""
258+
@Table(schema: nil)
259+
struct Foo {
260+
var bar: Int
261+
}
262+
"""
263+
} diagnostics: {
264+
"""
265+
@Table(schema: nil)
266+
┬──
267+
╰─ 🛑 Argument must be a non-empty string literal
268+
struct Foo {
269+
var bar: Int
270+
}
271+
"""
272+
}
273+
}
274+
275+
@Test func schemaNameEmpty() {
276+
assertMacro {
277+
"""
278+
@Table(schema: "")
279+
struct Foo {
280+
var bar: Int
281+
}
282+
"""
283+
} diagnostics: {
284+
"""
285+
@Table(schema: "")
286+
┬─
287+
╰─ 🛑 Argument must be a non-empty string literal
288+
struct Foo {
289+
var bar: Int
290+
}
291+
"""
292+
}
293+
}
294+
218295
@Test func literals() {
219296
assertMacro {
220297
"""

0 commit comments

Comments
 (0)