Skip to content

Commit 3e15094

Browse files
committed
Support nested tables
1 parent 1be6e0b commit 3e15094

File tree

11 files changed

+337
-101
lines changed

11 files changed

+337
-101
lines changed

Sources/StructuredQueries/Macros.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@ public macro Column(
4949
type: "ColumnMacro"
5050
)
5151

52+
@attached(peer)
53+
public macro Columns(
54+
// as representableType: (any QueryRepresentable.Type)? = nil,
55+
// primaryKey: Bool = false
56+
) =
57+
#externalMacro(
58+
module: "StructuredQueriesMacros",
59+
type: "ColumnsMacro"
60+
)
61+
5262
/// Tells StructuredQueries not to consider the annotated property a column of the table.
5363
///
5464
/// Like SwiftData's `@Transient` macro, but for SQL.

Sources/StructuredQueriesCore/Bind.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
///
33
/// It is not common to interact with this type directly. A value of this type is returned from the
44
/// `#bind` macro.
5-
public struct BindQueryExpression<QueryValue: QueryBindable>: QueryExpression {
5+
public struct BindQueryExpression<QueryValue: QueryRepresentable & QueryExpression>: QueryExpression {
66
public let base: QueryValue
77

88
public init(
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
@dynamicMemberLookup
2+
public struct ColumnGroup<Root: Table, Values: Table>: QueryExpression {
3+
public typealias QueryValue = Values
4+
5+
public static func allColumns(keyPath: KeyPath<Root, Values>) -> [any TableColumnExpression] {
6+
return Values.TableColumns.allColumns.map { column in
7+
func open<R, V>(
8+
_ column: some TableColumnExpression<R, V>
9+
) -> any TableColumnExpression {
10+
let keyPath = keyPath.appending(
11+
path: unsafeDowncast(column.keyPath, to: KeyPath<Values, V.QueryOutput>.self)
12+
)
13+
return TableColumn<Root, V>(
14+
column.name,
15+
keyPath: keyPath,
16+
default: column.defaultValue
17+
)
18+
}
19+
return open(column)
20+
}
21+
}
22+
23+
public static func writableColumns(
24+
keyPath: KeyPath<Root, Values>
25+
) -> [any WritableTableColumnExpression] {
26+
return Values.TableColumns.writableColumns.map { column in
27+
func open<R, V>(
28+
_ column: some WritableTableColumnExpression<R, V>
29+
) -> any WritableTableColumnExpression {
30+
let keyPath = keyPath.appending(
31+
path: unsafeDowncast(column.keyPath, to: KeyPath<Values, V.QueryOutput>.self)
32+
)
33+
return TableColumn<Root, V>(
34+
column.name,
35+
keyPath: keyPath,
36+
default: column.defaultValue
37+
)
38+
}
39+
return open(column)
40+
}
41+
}
42+
43+
let keyPath: KeyPath<Root, Values>
44+
45+
public init(keyPath: KeyPath<Root, Values>) {
46+
self.keyPath = keyPath
47+
}
48+
49+
public var queryFragment: QueryFragment {
50+
ColumnGroup.allColumns(keyPath: keyPath).map(\.queryFragment).joined(separator: ", ")
51+
}
52+
53+
public subscript<Member>(
54+
dynamicMember keyPath: KeyPath<Values.TableColumns, TableColumn<Values, Member>> & Sendable
55+
) -> TableColumn<Root, Member> {
56+
let column = Values.columns[keyPath: keyPath]
57+
return TableColumn<Root, Member>(
58+
column.name,
59+
keyPath: self.keyPath.appending(path: column.keyPath),
60+
default: column.defaultValue
61+
)
62+
}
63+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import SwiftSyntax
2+
import SwiftSyntaxMacros
3+
4+
public enum ColumnsMacro: PeerMacro {
5+
public static func expansion<D: DeclSyntaxProtocol, C: MacroExpansionContext>(
6+
of node: AttributeSyntax,
7+
providingPeersOf declaration: D,
8+
in context: C
9+
) throws -> [DeclSyntax] {
10+
[]
11+
}
12+
}

Sources/StructuredQueriesMacros/Plugin.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ struct StructuredQueriesPlugin: CompilerPlugin {
66
let providingMacros: [Macro.Type] = [
77
BindMacro.self,
88
ColumnMacro.self,
9+
ColumnsMacro.self,
910
EphemeralMacro.self,
1011
SelectionMacro.self,
1112
SQLMacro.self,

Sources/StructuredQueriesMacros/TableMacro.swift

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ extension TableMacro: ExtensionMacro {
119119
.map { $0.rewritten(selfRewriter) }
120120
var columnQueryOutputType = columnQueryValueType
121121
var isPrimaryKey = primaryKey == nil && identifier.text == "id"
122+
var isColumnGroup = false
122123
var isEphemeral = false
123124
var isGenerated = false
124125

@@ -127,9 +128,10 @@ extension TableMacro: ExtensionMacro {
127128
let attribute = attribute.as(AttributeSyntax.self),
128129
let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text
129130
else { continue }
131+
isColumnGroup = isColumnGroup || attributeName == "Columns"
130132
isEphemeral = isEphemeral || attributeName == "Ephemeral"
131133
guard
132-
attributeName == "Column" || isEphemeral,
134+
attributeName == "Column" || isEphemeral || isColumnGroup,
133135
case .argumentList(let arguments) = attribute.arguments
134136
else { continue }
135137

@@ -271,7 +273,18 @@ extension TableMacro: ExtensionMacro {
271273
}
272274

273275
let defaultValue = binding.initializer?.value.rewritten(selfRewriter)
274-
if isGenerated {
276+
if isColumnGroup {
277+
columnsProperties.append(
278+
"""
279+
public let \(identifier) = \(moduleName).ColumnGroup<\
280+
QueryValue, \
281+
\(raw: columnQueryValueType?.trimmedDescription ?? "_")\
282+
>(\
283+
keyPath: \\QueryValue.\(identifier)\
284+
)
285+
"""
286+
)
287+
} else if isGenerated {
275288
columnsProperties.append(
276289
"""
277290
public var \(identifier): \(moduleName).GeneratedColumn<\
@@ -624,8 +637,8 @@ extension TableMacro: MemberMacro {
624637
}
625638
let type = IdentifierTypeSyntax(name: declaration.name.trimmed)
626639
var allColumns: [(name: TokenSyntax, type: TypeSyntax?, default: ExprSyntax?)] = []
627-
var allColumnNames: [TokenSyntax] = []
628-
var writableColumns: [TokenSyntax] = []
640+
var allColumnNames: [Column] = []
641+
var writableColumns: [Column] = []
629642
var selectedColumns: [TokenSyntax] = []
630643
var columnsProperties: [DeclSyntax] = []
631644
var decodings: [String] = []
@@ -661,11 +674,12 @@ extension TableMacro: MemberMacro {
661674
StringLiteralExprSyntax(content: identifier.text.trimmingBackticks())
662675
)
663676
var columnQueryValueType =
664-
(binding.typeAnnotation?.type.trimmed
665-
?? binding.initializer?.value.literalType)
666-
.map { $0.rewritten(selfRewriter) }
677+
(binding.typeAnnotation?.type.trimmed
678+
?? binding.initializer?.value.literalType)
679+
.map { $0.rewritten(selfRewriter) }
667680
var columnQueryOutputType = columnQueryValueType
668681
var isPrimaryKey = primaryKey == nil && identifier.text == "id"
682+
var isColumnGroup = false
669683
var isEphemeral = false
670684
var isGenerated = false
671685

@@ -674,9 +688,10 @@ extension TableMacro: MemberMacro {
674688
let attribute = attribute.as(AttributeSyntax.self),
675689
let attributeName = attribute.attributeName.as(IdentifierTypeSyntax.self)?.name.text
676690
else { continue }
691+
isColumnGroup = isColumnGroup || attributeName == "Columns"
677692
isEphemeral = isEphemeral || attributeName == "Ephemeral"
678693
guard
679-
attributeName == "Column" || isEphemeral,
694+
attributeName == "Column" || isEphemeral || isColumnGroup,
680695
case .argumentList(let arguments) = attribute.arguments
681696
else { continue }
682697

@@ -768,7 +783,21 @@ extension TableMacro: MemberMacro {
768783
}
769784

770785
let defaultValue = binding.initializer?.value.rewritten(selfRewriter)
771-
if isGenerated {
786+
if isColumnGroup {
787+
columnsProperties.append(
788+
"""
789+
public let \(identifier) = \(moduleName).ColumnGroup<\
790+
QueryValue, \
791+
\(raw: columnQueryValueType?.trimmedDescription ?? "_")\
792+
>(\
793+
keyPath: \\QueryValue.\(identifier)\
794+
)
795+
"""
796+
)
797+
allColumns.append((identifier, columnQueryValueType, defaultValue))
798+
allColumnNames.append(.group(identifier))
799+
writableColumns.append(.group(identifier))
800+
} else if isGenerated {
772801
columnsProperties.append(
773802
"""
774803
public var \(identifier): \(moduleName).GeneratedColumn<\
@@ -786,7 +815,7 @@ extension TableMacro: MemberMacro {
786815
"""
787816
)
788817
allColumns.append((identifier, columnQueryValueType, defaultValue))
789-
allColumnNames.append(identifier)
818+
allColumnNames.append(.single(identifier))
790819
} else {
791820
columnsProperties.append(
792821
"""
@@ -800,8 +829,8 @@ extension TableMacro: MemberMacro {
800829
"""
801830
)
802831
allColumns.append((identifier, columnQueryValueType, defaultValue))
803-
allColumnNames.append(identifier)
804-
writableColumns.append(identifier)
832+
allColumnNames.append(.single(identifier))
833+
writableColumns.append(.single(identifier))
805834
}
806835
let decodedType = columnQueryValueType?.asNonOptionalType()
807836
if let defaultValue {
@@ -1049,10 +1078,10 @@ extension TableMacro: MemberMacro {
10491078
public typealias QueryValue = \(type.trimmed)
10501079
\(columnsProperties, separator: "\n")
10511080
public static var allColumns: [any \(moduleName).TableColumnExpression] { \
1052-
[\(allColumnNames.map { "QueryValue.columns.\($0)" as ExprSyntax }, separator: ", ")]
1081+
[\(allColumnNames.map { $0.columns(.all) }, separator: ", ")].flatMap(\\.self)
10531082
}
10541083
public static var writableColumns: [any \(moduleName).WritableTableColumnExpression] { \
1055-
[\(writableColumns.map { "QueryValue.columns.\($0)" as ExprSyntax }, separator: ", ")]
1084+
[\(writableColumns.map { $0.columns(.writable) }, separator: ", ")].flatMap(\\.self)
10561085
}
10571086
public var queryFragment: QueryFragment {
10581087
"\(raw: selectedColumns.map { #"\(self.\#($0))"# }.joined(separator: ", "))"
@@ -1066,7 +1095,7 @@ extension TableMacro: MemberMacro {
10661095
public init(
10671096
\(raw: selectionInitArguments)
10681097
) {
1069-
self.allColumns = [\(allColumnNames, separator: ", ")]
1098+
self.allColumns = [\(selectedColumns, separator: ", ")]
10701099
}
10711100
}
10721101
""",
@@ -1130,4 +1159,24 @@ extension TableMacro: MemberAttributeMacro {
11301159
"""
11311160
]
11321161
}
1162+
1163+
fileprivate enum Column {
1164+
case single(TokenSyntax)
1165+
case group(TokenSyntax)
1166+
1167+
enum Kind: String {
1168+
case all
1169+
case writable
1170+
}
1171+
1172+
func columns(_ kind: Kind) -> ExprSyntax {
1173+
switch self {
1174+
case .single(let column):
1175+
return "[QueryValue.columns.\(column)]"
1176+
1177+
case .group(let columns):
1178+
return #"\#(moduleName).ColumnGroup.\#(raw: kind)Columns(keyPath: \QueryValue.\#(columns))"#
1179+
}
1180+
}
1181+
}
11331182
}

Tests/StructuredQueriesMacrosTests/SelectionMacroTests.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,10 +276,10 @@ extension SnapshotTests {
276276
self.id
277277
}
278278
public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] {
279-
[QueryValue.columns.id, QueryValue.columns.title]
279+
[[QueryValue.columns.id], [QueryValue.columns.title]].flatMap(\.self)
280280
}
281281
public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] {
282-
[QueryValue.columns.id, QueryValue.columns.title]
282+
[[QueryValue.columns.id], [QueryValue.columns.title]].flatMap(\.self)
283283
}
284284
public var queryFragment: QueryFragment {
285285
"\(self.id), \(self.title)"
@@ -306,10 +306,10 @@ extension SnapshotTests {
306306
public let id = StructuredQueriesCore.TableColumn<QueryValue, Int?>("id", keyPath: \QueryValue.id)
307307
public let title = StructuredQueriesCore.TableColumn<QueryValue, Swift.String>("title", keyPath: \QueryValue.title, default: "")
308308
public static var allColumns: [any StructuredQueriesCore.TableColumnExpression] {
309-
[QueryValue.columns.id, QueryValue.columns.title]
309+
[[QueryValue.columns.id], [QueryValue.columns.title]].flatMap(\.self)
310310
}
311311
public static var writableColumns: [any StructuredQueriesCore.WritableTableColumnExpression] {
312-
[QueryValue.columns.id, QueryValue.columns.title]
312+
[[QueryValue.columns.id], [QueryValue.columns.title]].flatMap(\.self)
313313
}
314314
public var queryFragment: QueryFragment {
315315
"\(self.id), \(self.title)"

0 commit comments

Comments
 (0)