Skip to content

Commit cae697b

Browse files
committed
Generate queries as types instead
1 parent dacfd93 commit cae697b

8 files changed

Lines changed: 518 additions & 458 deletions

File tree

Sources/Compiler/Gen/Generator.swift

Lines changed: 5 additions & 349 deletions
Original file line numberDiff line numberDiff line change
@@ -88,10 +88,11 @@ extension Language2 {
8888
}.joined()
8989

9090
return GeneratedQuery(
91-
name: name.description,
91+
name: "\(name.capitalizedFirst)Query",
9292
type: type,
9393
input: input,
9494
output: output,
95+
outputCardinality: statement.outputCardinality,
9596
sourceSql: sql,
9697
isReadOnly: statement.isReadOnly
9798
)
@@ -126,7 +127,7 @@ extension Language2 {
126127
)
127128
}
128129

129-
let inputTypeName = "\(name.capitalizedFirst)Input"
130+
let inputTypeName = "Input"
130131

131132
let model = GeneratedModel(
132133
name: inputTypeName,
@@ -163,7 +164,7 @@ extension Language2 {
163164
return .builtin(builtinType(for: firstColumn), isArray: firstColumn.isRow)
164165
}
165166

166-
let outputTypeName = "\(name.capitalizedFirst)Output"
167+
let outputTypeName = "Row"
167168

168169
let model = GeneratedModel(
169170
name: outputTypeName,
@@ -183,352 +184,6 @@ extension Language2 {
183184
}
184185
}
185186

186-
public struct SwiftLanguage: Language2 {
187-
public static func interpolatedQuestionMarks(for param: String) -> String {
188-
return "\\(\(param).sqlQuestionMarks)"
189-
}
190-
191-
public static func builtinType(for type: Type) -> String {
192-
return switch type {
193-
case let .nominal(name):
194-
switch name.uppercased() {
195-
case "REAL": "Double"
196-
case "INT": "Int"
197-
case "INTEGER": "Int"
198-
case "TEXT": "String"
199-
default: "Any"
200-
}
201-
case let .optional(ty): "\(builtinType(for: ty))?"
202-
case let .row(.unknown(ty)): "[\(builtinType(for: ty))]"
203-
case .var, .fn, .row, .error: "Any"
204-
case .alias(_, let alias): alias.description
205-
}
206-
}
207-
208-
public static func file(
209-
migrations: [String],
210-
tables: [GeneratedModel],
211-
queries: [GeneratedQuery],
212-
options: GenerationOptions
213-
) throws -> String {
214-
let file = try SourceFileSyntax {
215-
try ImportDeclSyntax("import Foundation")
216-
try ImportDeclSyntax("import Feather")
217-
218-
for table in tables {
219-
try declaration(for: table, isOutput: true, options: options)
220-
}
221-
222-
try EnumDeclSyntax("enum DB") {
223-
try declaration(for: migrations, options: options)
224-
225-
for query in queries {
226-
if case let .model(input) = query.input, !input.isTable {
227-
try declaration(for: input, isOutput: false, options: options)
228-
}
229-
230-
if case let .model(output) = query.output, !output.isTable {
231-
try declaration(for: output, isOutput: true, options: options)
232-
}
233-
234-
try declaration(for: query, options: options)
235-
}
236-
}
237-
238-
for query in queries {
239-
try typealiasFor(query: query)
240-
241-
if case let .model(input) = query.input {
242-
try extensionForInput(query: query, input: input)
243-
}
244-
}
245-
}
246-
247-
return file.formatted().description
248-
}
249-
250-
public static func queryType(
251-
for cardinality: Cardinality?,
252-
input: BuiltinOrGenerated?,
253-
output: BuiltinOrGenerated?
254-
) -> String {
255-
let input = input?.description ?? "()"
256-
let output = output?.description ?? "()"
257-
258-
return switch cardinality {
259-
case .single: "FetchSingleQuery<\(input), \(output)>"
260-
case .many: "FetchManyQuery<\(input), \(output)>"
261-
default: "VoidQuery<\(input)>"
262-
}
263-
}
264-
265-
private static func declaration(
266-
for migrations: [String],
267-
options: GenerationOptions
268-
) throws -> DeclSyntax {
269-
let variable = try VariableDeclSyntax("static var migrations: [String]") {
270-
ArrayExprSyntax(
271-
expressions: migrations.map { source in
272-
SwiftSyntax.ExprSyntax(stringLiteral(of: source, multiline: true)
273-
.with(\.trailingTrivia, .newline))
274-
}
275-
)
276-
}
277-
278-
return DeclSyntax(variable)
279-
}
280-
281-
private static func declaration(
282-
for query: GeneratedQuery,
283-
options: GenerationOptions
284-
) throws -> DeclSyntax {
285-
let query = try VariableDeclSyntax("static var \(raw: query.name): \(raw: query.type)") {
286-
FunctionCallExprSyntax(
287-
calledExpression: DeclReferenceExprSyntax(
288-
baseName: .identifier(query.type)
289-
),
290-
leftParen: .leftParenToken(),
291-
arguments: LabeledExprListSyntax {
292-
LabeledExprSyntax(
293-
label: nil,
294-
colon: nil,
295-
expression: DeclReferenceExprSyntax(
296-
baseName: query.isReadOnly ? ".read" : ".write"
297-
),
298-
trailingComma: nil
299-
)
300-
},
301-
rightParen: .rightParenToken(),
302-
trailingClosure: ClosureExprSyntax(
303-
signature: ClosureSignatureSyntax(
304-
parameterClause: .simpleInput(.init {
305-
ClosureShorthandParameterSyntax(name: "input")
306-
ClosureShorthandParameterSyntax(name: "transaction")
307-
})
308-
)
309-
) {
310-
let sql = stringLiteral(of: query.sourceSql, multiline: true)
311-
let statementBinding: TokenSyntax = .keyword(query.input == nil ? .let : .var)
312-
"\(statementBinding) statement = try Feather.Statement(\(sql), \ntransaction: transaction\n)"
313-
314-
if let input = query.input {
315-
switch input {
316-
case let .builtin(_, isArray):
317-
bind(field: nil, isArray: isArray)
318-
case .model(let model):
319-
for field in model.fields.values {
320-
bind(field: field.name, isArray: field.isArray)
321-
}
322-
}
323-
}
324-
325-
"return statement"
326-
}
327-
)
328-
}
329-
330-
return DeclSyntax(query)
331-
}
332-
333-
private static func declaration(
334-
for model: GeneratedModel,
335-
isOutput: Bool,
336-
options: GenerationOptions
337-
) throws -> DeclSyntax {
338-
let inheretance = InheritanceClauseSyntax {
339-
InheritedTypeSyntax(type: TypeSyntax("Hashable"))
340-
341-
if model.fields["id"] != nil {
342-
InheritedTypeSyntax(type: TypeSyntax("Identifiable"))
343-
}
344-
345-
if isOutput {
346-
InheritedTypeSyntax(type: TypeSyntax("RowDecodable"))
347-
}
348-
}
349-
350-
let strct = StructDeclSyntax(
351-
name: TokenSyntax.identifier(model.name),
352-
inheritanceClause: inheretance
353-
) {
354-
for field in model.fields.values {
355-
variableDecl(name: field.name, type: field.type)
356-
}
357-
358-
if isOutput {
359-
rowDecodableInit(for: model)
360-
memberwiseInit(for: model)
361-
}
362-
}
363-
364-
return DeclSyntax(strct)
365-
}
366-
367-
private static func rowDecodableInit(
368-
for model: GeneratedModel
369-
) -> InitializerDeclSyntax {
370-
return InitializerDeclSyntax(
371-
signature: FunctionSignatureSyntax(
372-
parameterClause: FunctionParameterClauseSyntax(
373-
parameters: [
374-
FunctionParameterSyntax(
375-
firstName: "row",
376-
type: IdentifierTypeSyntax(name: "borrowing Feather.Row")
377-
)
378-
]
379-
),
380-
effectSpecifiers: FunctionEffectSpecifiersSyntax(
381-
throwsClause: ThrowsClauseSyntax(
382-
throwsSpecifier: TokenSyntax.keyword(.throws),
383-
leftParen: TokenSyntax.leftParenToken(),
384-
type: TypeSyntax("FeatherError"),
385-
rightParen: TokenSyntax.rightParenToken()
386-
)
387-
)
388-
)
389-
) {
390-
"var columns = row.columnIterator()"
391-
392-
for field in model.fields.values {
393-
"self.\(raw: field.name) = try columns.next()"
394-
}
395-
}
396-
}
397-
398-
/// Generates a memberwise initializer for the model
399-
private static func memberwiseInit(
400-
for model: GeneratedModel
401-
) -> InitializerDeclSyntax {
402-
return InitializerDeclSyntax(
403-
signature: FunctionSignatureSyntax(
404-
parameterClause: FunctionParameterClauseSyntax(
405-
parameters: FunctionParameterListSyntax(
406-
model.fields.values.positional()
407-
.map { (position, field) in
408-
FunctionParameterSyntax(
409-
firstName: .identifier(field.name),
410-
type: IdentifierTypeSyntax(name: .identifier(field.type)),
411-
trailingComma: position == .last ? nil : TokenSyntax.commaToken()
412-
)
413-
}
414-
)
415-
)
416-
)
417-
) {
418-
for field in model.fields.values {
419-
"self.\(raw: field.name) = \(raw: field.name)"
420-
}
421-
}
422-
}
423-
424-
/// Generates a typealias for the query so the Query<Input, Output> does
425-
/// not have to be typed everytime it's referenced since it can get quite long
426-
/// and repetitive.
427-
private static func typealiasFor(query: GeneratedQuery) throws -> TypeAliasDeclSyntax {
428-
let name = "\(query.name.capitalizedFirst)Query"
429-
let input = query.input.map{ "DB.\($0.description)" } ?? "()"
430-
let output = query.output.map{ "DB.\($0.description)" } ?? "()"
431-
return try TypeAliasDeclSyntax("typealias \(raw: name) = any Query<\(raw: input), \(raw: output)>")
432-
}
433-
434-
/// Creates an extension that has the input struct fields deconstructed so the
435-
/// input struct does not have to be constructed every time.
436-
private static func extensionForInput(
437-
query: GeneratedQuery,
438-
input: GeneratedModel
439-
) throws -> ExtensionDeclSyntax {
440-
try ExtensionDeclSyntax("extension Query where Input == DB.\(raw: input.name)") {
441-
let parameters = input.fields.map { parameter in
442-
"\(parameter.key): \(parameter.value.type)"
443-
}.joined(separator: ", ")
444-
445-
let args = input.fields.map { parameter in
446-
"\(parameter.key): \(parameter.key)"
447-
}.joined(separator: ", ")
448-
449-
"""
450-
func execute(\(raw: parameters)) async throws -> Output {
451-
try await execute(with: DB.\(raw: input.name)(\(raw: args)))
452-
}
453-
"""
454-
}
455-
}
456-
457-
private static func variableDecl(
458-
binding: Keyword = .let,
459-
name: String,
460-
type: String
461-
) -> VariableDeclSyntax {
462-
VariableDeclSyntax(
463-
.let,
464-
name: "\(raw: name)",
465-
type: TypeAnnotationSyntax(
466-
type: IdentifierTypeSyntax(name: .identifier(type))
467-
)
468-
)
469-
}
470-
471-
private static func stringLiteral(
472-
of contents: String,
473-
multiline: Bool = false
474-
) -> StringLiteralExprSyntax {
475-
let openingQuote: TokenSyntax = multiline
476-
? .multilineStringQuoteToken(trailingTrivia: .newline)
477-
: .singleQuoteToken()
478-
479-
let closingQuote: TokenSyntax = multiline
480-
? .multilineStringQuoteToken(leadingTrivia: .newline)
481-
: .singleQuoteToken()
482-
483-
let segments: StringLiteralSegmentListSyntax
484-
if multiline {
485-
let lines = contents.split(separator: "\n")
486-
487-
segments = StringLiteralSegmentListSyntax(
488-
lines
489-
.enumerated()
490-
.map { (i, s) in
491-
.stringSegment(StringSegmentSyntax(
492-
content: .stringSegment(s.description),
493-
trailingTrivia: i == lines.count - 1 ? nil : .newline
494-
))
495-
}
496-
)
497-
} else {
498-
segments = [.stringSegment(.init(content: .stringSegment(contents)))]
499-
}
500-
501-
return StringLiteralExprSyntax(
502-
leadingTrivia: multiline ? Trivia.newline : nil,
503-
openingQuote: openingQuote,
504-
segments: segments,
505-
closingQuote: closingQuote,
506-
trailingTrivia: nil // Seems like a newline is added automatically?
507-
)
508-
}
509-
510-
@CodeBlockItemListBuilder
511-
private static func bind(
512-
field: String?,
513-
isArray: Bool
514-
) -> CodeBlockItemListSyntax {
515-
let paramName = if let field {
516-
"input.\(field)"
517-
} else {
518-
"input"
519-
}
520-
521-
if isArray {
522-
"""
523-
for element in \(raw: paramName) {
524-
try statement.bind(value: element)
525-
}
526-
"""
527-
} else {
528-
"try statement.bind(value: \(raw: paramName))"
529-
}
530-
}
531-
}
532187

533188
public typealias GenerationOptions = Set<GenerationOption>
534189

@@ -555,6 +210,7 @@ public struct GeneratedQuery {
555210
let type: String
556211
let input: BuiltinOrGenerated?
557212
let output: BuiltinOrGenerated?
213+
let outputCardinality: Cardinality
558214
let sourceSql: String
559215
let isReadOnly: Bool
560216
}

0 commit comments

Comments
 (0)