Skip to content

Commit 043f7e7

Browse files
committed
Generation tests
1 parent 7af70d8 commit 043f7e7

8 files changed

Lines changed: 297 additions & 35 deletions

File tree

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ let package = Package(
6666
.testTarget(
6767
name: "CompilerTests",
6868
dependencies: ["Compiler"],
69-
resources: [.process("Compiler"), .process("Parser")]
69+
resources: [.process("Compiler"), .process("Parser"), .process("Gen")]
7070
),
7171
]
7272
)

Sources/Compiler/Diagnostic.swift

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,28 +46,6 @@ extension Diagnostic: CustomStringConvertible {
4646
}
4747

4848
extension Diagnostic {
49-
static func incorrectType(
50-
_ actual: TypeNameSyntax,
51-
expected: TypeNameSyntax,
52-
at location: SourceLocation
53-
) -> Diagnostic {
54-
Diagnostic(
55-
"Incorrect type, expected '\(expected.name)' got '\(actual.name)'",
56-
at: location,
57-
suggestion: .replace(expected.name.description)
58-
)
59-
}
60-
61-
static func expectedNumber(
62-
_ actual: TypeNameSyntax,
63-
at location: SourceLocation
64-
) -> Diagnostic {
65-
Diagnostic(
66-
"Incorrect type, expected number got '\(actual.name)'",
67-
at: location
68-
)
69-
}
70-
7149
static func ambiguous(
7250
_ identifier: Substring,
7351
at location: SourceLocation

Sources/Compiler/Gen/SwiftLanguage.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -390,19 +390,19 @@ public struct SwiftLanguage: Language {
390390
writer.write(line: "let ", field.name, ": ", field.type.description)
391391
}
392392

393-
if addDynamicLookup {
394-
for (fieldName, table) in dynamicLookupTables {
395-
dynamicMemberLookup(fieldName: fieldName, typeName: table.name)
396-
}
397-
}
398-
399393
if isOutput {
400394
writer.blankLine()
401395
rowDecodableInit(for: model)
402396
writer.blankLine()
403397
memberWiseInit(for: model)
404398
}
405399

400+
if addDynamicLookup {
401+
for (fieldName, table) in dynamicLookupTables {
402+
dynamicMemberLookup(fieldName: fieldName, typeName: table.name)
403+
}
404+
}
405+
406406
writer.unindent()
407407
writer.write(line: "}")
408408
writer.blankLine()

Sources/Compiler/Syntax/Expressions/PrefixExprSyntax.swift

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,11 @@
66
//
77

88
/// https://www.sqlite.org/lang_expr.html
9-
struct PrefixExprSyntax: ExprSyntax, CustomStringConvertible {
9+
struct PrefixExprSyntax: ExprSyntax {
1010
let id: SyntaxId
1111
let `operator`: OperatorSyntax
1212
let rhs: any ExprSyntax
13-
14-
var description: String {
15-
return "(\(`operator`)\(rhs))"
16-
}
17-
13+
1814
var location: SourceLocation {
1915
return `operator`.location.spanning(rhs.location)
2016
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
CREATE TABLE user (
2+
id INTEGER PRIMARY KEY AUTOINCREMENT,
3+
firstName TEXT NOT NULL,
4+
lastName TEXT NOT NULL,
5+
fullName TEXT NOT NULL GENERATED ALWAYS AS (firstName || ' ' || lastName)
6+
);
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
selectUsers:
2+
SELECT * FROM user;
3+
4+
selectUserById:
5+
SELECT * FROM user WHERE id = ?;
6+
7+
selectUserByIds:
8+
SELECT * FROM user WHERE id IN ?;
9+
10+
selectUserByName:
11+
SELECT * FROM user WHERE fullName LIKE ?;
12+
13+
selectUserWithManyInputs:
14+
SELECT *, 1 AS favoriteNumber FROM user WHERE id = ? AND firstName = ?;
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
import Foundation
2+
import Otter
3+
4+
struct User: Hashable, Sendable, Identifiable, RowDecodable {
5+
let id: Int
6+
let firstName: String
7+
let lastName: String
8+
let fullName: String
9+
10+
init(
11+
row: borrowing Otter.Row,
12+
startingAt start: Int32
13+
) throws(Otter.OtterError) {
14+
self.id = try row.value(at: start + 0)
15+
self.firstName = try row.value(at: start + 1)
16+
self.lastName = try row.value(at: start + 2)
17+
self.fullName = try row.value(at: start + 3)
18+
}
19+
20+
init(
21+
id: Int,
22+
firstName: String,
23+
lastName: String,
24+
fullName: String
25+
) {
26+
self.id = id
27+
self.firstName = firstName
28+
self.lastName = lastName
29+
self.fullName = fullName
30+
}
31+
}
32+
33+
struct SelectUserWithManyInputsInput: Hashable, Sendable, Identifiable {
34+
let id: Int
35+
let firstName: String
36+
}
37+
38+
@dynamicMemberLookup
39+
struct SelectUserWithManyInputsOutput: Hashable, Sendable, RowDecodable {
40+
let user: User
41+
let favoriteNumber: Int
42+
43+
init(
44+
row: borrowing Otter.Row,
45+
startingAt start: Int32
46+
) throws(Otter.OtterError) {
47+
self.user = try User(row: row, startingAt: start + 0)
48+
self.favoriteNumber = try row.value(at: start + 4)
49+
}
50+
51+
init(
52+
user: User,
53+
favoriteNumber: Int
54+
) {
55+
self.user = user
56+
self.favoriteNumber = favoriteNumber
57+
}
58+
59+
subscript<Value>(dynamicMember dynamicMember: KeyPath<User, Value>) -> Value {
60+
self.user[keyPath: dynamicMember]
61+
}
62+
}
63+
64+
protocol QueriesQueries {
65+
associatedtype SelectUsers: SelectUsersQuery
66+
var selectUsers: SelectUsers { get }
67+
associatedtype SelectUserById: SelectUserByIdQuery
68+
var selectUserById: SelectUserById { get }
69+
associatedtype SelectUserByIds: SelectUserByIdsQuery
70+
var selectUserByIds: SelectUserByIds { get }
71+
associatedtype SelectUserByName: SelectUserByNameQuery
72+
var selectUserByName: SelectUserByName { get }
73+
associatedtype SelectUserWithManyInputs: SelectUserWithManyInputsQuery
74+
var selectUserWithManyInputs: SelectUserWithManyInputs { get }
75+
}
76+
77+
struct QueriesQueriesNoop: QueriesQueries {
78+
let selectUsers: AnyQuery<(), [User]>
79+
let selectUserById: AnyQuery<Int, User?>
80+
let selectUserByIds: AnyQuery<[Int], [User]>
81+
let selectUserByName: AnyQuery<String, [User]>
82+
let selectUserWithManyInputs: AnyQuery<SelectUserWithManyInputsInput, SelectUserWithManyInputsOutput?>
83+
84+
init(
85+
selectUsers: any SelectUsersQuery = Queries.Just(),
86+
selectUserById: any SelectUserByIdQuery = Queries.Just(),
87+
selectUserByIds: any SelectUserByIdsQuery = Queries.Just(),
88+
selectUserByName: any SelectUserByNameQuery = Queries.Just(),
89+
selectUserWithManyInputs: any SelectUserWithManyInputsQuery = Queries.Just()
90+
) {
91+
self.selectUsers = selectUsers.eraseToAnyQuery()
92+
self.selectUserById = selectUserById.eraseToAnyQuery()
93+
self.selectUserByIds = selectUserByIds.eraseToAnyQuery()
94+
self.selectUserByName = selectUserByName.eraseToAnyQuery()
95+
self.selectUserWithManyInputs = selectUserWithManyInputs.eraseToAnyQuery()
96+
}
97+
}
98+
99+
struct QueriesQueriesImpl: QueriesQueries {
100+
let connection: any Connection
101+
102+
var selectUsers: AnyDatabaseQuery<(), [User]> {
103+
AnyDatabaseQuery<(), [User]>(
104+
.read,
105+
in: connection,
106+
watchingTables: ["user"]
107+
) { input, tx in
108+
let statement = try Otter.Statement(
109+
"""
110+
SELECT * FROM user
111+
""",
112+
transaction: tx
113+
)
114+
return try statement.fetchAll()
115+
}
116+
}
117+
118+
var selectUserById: AnyDatabaseQuery<Int, User?> {
119+
AnyDatabaseQuery<Int, User?>(
120+
.read,
121+
in: connection,
122+
watchingTables: ["user"]
123+
) { input, tx in
124+
var statement = try Otter.Statement(
125+
"""
126+
SELECT * FROM user WHERE id = ?
127+
""",
128+
transaction: tx
129+
)
130+
try statement.bind(value: input)
131+
return try statement.fetchOne()
132+
}
133+
}
134+
135+
var selectUserByIds: AnyDatabaseQuery<[Int], [User]> {
136+
AnyDatabaseQuery<[Int], [User]>(
137+
.read,
138+
in: connection,
139+
watchingTables: ["user"]
140+
) { input, tx in
141+
var statement = try Otter.Statement(
142+
"""
143+
SELECT * FROM user WHERE id IN (\(input.sqlQuestionMarks))
144+
""",
145+
transaction: tx
146+
)
147+
for element in input {
148+
try statement.bind(value: element)
149+
}
150+
return try statement.fetchAll()
151+
}
152+
}
153+
154+
var selectUserByName: AnyDatabaseQuery<String, [User]> {
155+
AnyDatabaseQuery<String, [User]>(
156+
.read,
157+
in: connection,
158+
watchingTables: ["user"]
159+
) { input, tx in
160+
var statement = try Otter.Statement(
161+
"""
162+
SELECT * FROM user WHERE fullName LIKE ?
163+
""",
164+
transaction: tx
165+
)
166+
try statement.bind(value: input)
167+
return try statement.fetchAll()
168+
}
169+
}
170+
171+
var selectUserWithManyInputs: AnyDatabaseQuery<SelectUserWithManyInputsInput, SelectUserWithManyInputsOutput?> {
172+
AnyDatabaseQuery<SelectUserWithManyInputsInput, SelectUserWithManyInputsOutput?>(
173+
.read,
174+
in: connection,
175+
watchingTables: ["user"]
176+
) { input, tx in
177+
var statement = try Otter.Statement(
178+
"""
179+
SELECT *, 1 AS favoriteNumber FROM user WHERE id = ? AND firstName = ?
180+
""",
181+
transaction: tx
182+
)
183+
try statement.bind(value: input.id)
184+
try statement.bind(value: input.firstName)
185+
return try statement.fetchOne()
186+
}
187+
}
188+
}
189+
190+
struct DB: Database{
191+
let connection: any Otter.Connection
192+
193+
static var migrations: [String] {
194+
return [
195+
"""
196+
CREATE TABLE user (
197+
id INTEGER PRIMARY KEY AUTOINCREMENT,
198+
firstName TEXT NOT NULL,
199+
lastName TEXT NOT NULL,
200+
fullName TEXT NOT NULL GENERATED ALWAYS AS (firstName || ' ' || lastName)
201+
);;
202+
"""
203+
]
204+
}
205+
var queriesQueries: QueriesQueriesImpl {
206+
QueriesQueriesImpl(connection: connection)
207+
}
208+
}
209+
210+
typealias SelectUsersQuery = Query<(), [User]>
211+
typealias SelectUserByIdQuery = Query<Int, User?>
212+
typealias SelectUserByIdsQuery = Query<[Int], [User]>
213+
typealias SelectUserByNameQuery = Query<String, [User]>
214+
typealias SelectUserWithManyInputsQuery = Query<SelectUserWithManyInputsInput, SelectUserWithManyInputsOutput?>
215+
extension Query where Input == SelectUserWithManyInputsInput {func execute(id: Int, firstName: String) async throws -> Output {
216+
try await execute(with: SelectUserWithManyInputsInput(id: id, firstName: firstName))
217+
}
218+
}

Tests/CompilerTests/GenTests.swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//
2+
// GenTests.swift
3+
// Otter
4+
//
5+
// Created by Wes Wickwire on 6/23/25.
6+
//
7+
8+
import Testing
9+
import Foundation
10+
11+
@testable import Compiler
12+
13+
@Suite
14+
struct GenTests {
15+
@Test func generation() throws {
16+
var compiler = Compiler()
17+
let migrations = try compiler.compile(migration: load(file: "Migrations"))
18+
let queries = try compiler.compile(queries: load(file: "Queries"))
19+
20+
let language = SwiftLanguage(options: GenerationOptions())
21+
let rawOutput = try language.generate(
22+
migrations: migrations.0.map(\.sanitizedSource),
23+
queries: [("Queries", queries.0)],
24+
schema: compiler.schema
25+
)
26+
27+
print(rawOutput)
28+
let expected = try load(file: "Swift", ext: "output")
29+
.split(separator: "\n")
30+
.filter{ !$0.isEmpty }
31+
32+
let output = rawOutput
33+
.split(separator: "\n")
34+
.filter{ !$0.isEmpty }
35+
36+
for (expected, output) in zip(expected, output) {
37+
#expect(expected == output)
38+
}
39+
40+
#expect(output.count == expected.count)
41+
}
42+
43+
private func load(file: String, ext: String = "sql") throws -> String {
44+
guard let url = Bundle.module.url(forResource: file, withExtension: ext) else {
45+
struct NotFound: Error {}
46+
throw NotFound()
47+
}
48+
return try String(contentsOf: url, encoding: .utf8)
49+
}
50+
}

0 commit comments

Comments
 (0)