Skip to content

Commit 7666c70

Browse files
committed
Better custom value encodings
1 parent fea49c0 commit 7666c70

14 files changed

Lines changed: 674 additions & 438 deletions

Sources/Compiler/Gen/Language.swift

Lines changed: 124 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,13 @@ import SwiftSyntaxBuilder
1212
public protocol Language {
1313
init(options: GenerationOptions)
1414

15-
func queryTypeName(input: String, output: String) -> String
15+
var boolName: String { get }
1616

17-
func inputTypeName(input: BuiltinOrGenerated?) -> String
17+
func queryTypeName(input: String, output: String) -> String
1818

19-
func outputTypeName(
20-
output: BuiltinOrGenerated?,
21-
cardinality: Cardinality
22-
) -> String
19+
func typeName(for type: GenerationType) -> String
2320

24-
/// Returns the Language builtin for the given SQL type
25-
func builtinType(for type: Type) -> String
21+
func builtinType(named type: Substring) -> String
2622

2723
/// A file source code containing all of the generated tables, queries and migrations.
2824
func file(
@@ -94,8 +90,9 @@ extension Language {
9490
}
9591
}.joined()
9692

97-
let inputTypeName = inputTypeName(input: input)
98-
let outputTypeName = outputTypeName(output: output, cardinality: statement.outputCardinality)
93+
let inputTypeName = typeName(for: input)
94+
let outputTypeName = typeName(for: output)
95+
var startIndex = 1
9996

10097
return GeneratedQuery(
10198
name: definition.name.description,
@@ -109,25 +106,47 @@ extension Language {
109106
outputCardinality: statement.outputCardinality,
110107
sourceSql: sql,
111108
isReadOnly: statement.isReadOnly,
112-
usedTableNames: statement.usedTableNames.sorted()
109+
usedTableNames: statement.usedTableNames.sorted(),
110+
bindings: bindings(for: input, index: &startIndex)
113111
)
114112
}
115113

114+
private func bindings(
115+
for input: GenerationType,
116+
index: inout Int,
117+
owner: String? = nil
118+
) -> [GeneratedQuery.Binding] {
119+
var result: [GeneratedQuery.Binding] = []
120+
121+
switch input {
122+
case .void:
123+
break
124+
case .builtin, .optional:
125+
result.append(.value(index: index, name: "input", owner: owner))
126+
index += 1
127+
case .model(let model):
128+
for field in model.fields.values {
129+
result.append(contentsOf: bindings(for: field.type, index: &index, owner: field.name))
130+
}
131+
case .array(let values):
132+
result.append(.arrayStart(name: "input", elementName: "element"))
133+
result.append(contentsOf: bindings(for: values, index: &index, owner: "element"))
134+
result.append(.arrayEnd)
135+
case .encoded(_, _, let coder):
136+
result.append(.value(index: index, name: "input", owner: owner, coder: coder))
137+
index += 1
138+
}
139+
140+
return result
141+
}
142+
116143
private func model(for table: Table) -> GeneratedModel {
117144
GeneratedModel(
118145
name: table.name.name.capitalizedFirst,
119146
fields: table.columns.reduce(into: [:]) { fields, column in
120147
let name = column.key.description
121148
let type = column.value.type
122-
fields[name] = GeneratedField(
123-
name: name,
124-
type: .builtin(
125-
builtinType(for: type),
126-
isArray: false,
127-
encodedAs: builtinForAliasedType(for: type)
128-
),
129-
isArray: type.isRow
130-
)
149+
fields[name] = field(named: name, with: type)
131150
},
132151
isTable: true,
133152
nonOptionalIndices: table.columns.enumerated()
@@ -138,76 +157,89 @@ extension Language {
138157
)
139158
}
140159

141-
/// If the column type was aliased then this will return the `builtin`
142-
/// type for the root type of the alias.
143-
private func builtinForAliasedType(for type: Type) -> String? {
144-
guard case let .alias(root, _) = type else { return nil }
145-
return builtinType(for: root)
160+
private func generationType(for type: Type) -> GenerationType {
161+
switch type {
162+
case let .nominal(name):
163+
return .builtin(builtinType(named: name))
164+
case let .alias(root, alias):
165+
let alias = switch alias {
166+
case .explicit(let e): e.description
167+
case .hint(let hint):
168+
switch hint {
169+
case .bool: boolName
170+
}
171+
}
172+
173+
return .encoded(generationType(for: root), alias: alias, coder: "\(alias)DatabaseValueCoder")
174+
case let .optional(type):
175+
return .optional(generationType(for: type))
176+
case let .row(.unknown(type)):
177+
return .array(generationType(for: type))
178+
case .error, .fn, .row(.fixed), .var:
179+
fatalError("Upstream error not caught")
180+
}
146181
}
147182

183+
private func field(named name: String, with type: Type) -> GeneratedField {
184+
let type = generationType(for: type)
185+
return GeneratedField(name: name, type: type, typeName: typeName(for: type))
186+
}
187+
148188
private func inputTypeIfNeeded(
149189
statement: Statement,
150190
definition: Definition
151-
) -> BuiltinOrGenerated? {
152-
guard let firstParameter = statement.parameters.first else { return nil }
191+
) -> GenerationType {
192+
guard let firstParameter = statement.parameters.first else { return .void }
153193

154194
guard statement.parameters.count > 1 else {
155-
return .builtin(
156-
builtinType(for: firstParameter.type),
157-
isArray: firstParameter.type.isRow,
158-
encodedAs: builtinForAliasedType(for: firstParameter.type)
159-
)
195+
return generationType(for: firstParameter.type)
160196
}
161197

162198
let inputTypeName = definition.input?.description ?? "\(definition.name.capitalizedFirst)Input"
163199

164200
let model = GeneratedModel(
165201
name: inputTypeName,
166202
fields: statement.parameters.reduce(into: [:]) { fields, parameter in
167-
fields[parameter.name] = GeneratedField(
168-
name: parameter.name,
169-
type: .builtin(
170-
builtinType(for: parameter.type),
171-
isArray: false,
172-
encodedAs: builtinForAliasedType(for: parameter.type)
173-
),
174-
isArray: parameter.type.isRow
175-
)
203+
fields[parameter.name] = field(named: parameter.name, with: parameter.type)
176204
},
177205
isTable: false,
178206
nonOptionalIndices: []
179207
)
180208

181-
return .model(model, isOptional: false)
209+
return .model(model)
182210
}
183211

184212
private func outputTypeIfNeeded(
185213
statement: Statement,
186214
definition: Definition,
187215
tables: [Substring: GeneratedModel]
188-
) -> BuiltinOrGenerated? {
189-
guard let firstResultColumns = statement.resultColumns.chunks.first else { return nil }
216+
) -> GenerationType {
217+
guard let firstResultColumns = statement.resultColumns.chunks.first else { return .void }
218+
219+
// Will return an array if it returns many or optional if its a single result
220+
let singleOrMany: (GenerationType) -> GenerationType = {
221+
switch statement.outputCardinality {
222+
case .single: .optional($0)
223+
case .many: .array($0)
224+
}
225+
}
190226

191227
// Output can be mapped to a table struct
192228
if statement.resultColumns.chunks.count == 1,
193229
let tableName = firstResultColumns.table,
194230
let table = tables[tableName]
195231
{
196-
return .model(table, isOptional: false)
232+
return singleOrMany(.model(table))
197233
}
198234

199235
// Make sure there is at least one column else return void
200236
guard let firstColumn = firstResultColumns.columns.values.first?.type else {
201-
return nil
237+
return .void
202238
}
203239

204240
// Only one column returned, just use it's type
205241
guard statement.resultColumns.count > 1 else {
206-
return .builtin(
207-
builtinType(for: firstColumn),
208-
isArray: firstColumn.isRow,
209-
encodedAs: builtinForAliasedType(for: firstColumn)
210-
)
242+
return singleOrMany(generationType(for: firstColumn))
211243
}
212244

213245
let outputTypeName = definition.output?.description ?? "\(definition.name.capitalizedFirst)Output"
@@ -217,23 +249,20 @@ extension Language {
217249
fields: statement.resultColumns.chunks.reduce(into: [:]) { fields, chunk in
218250
if let tableName = chunk.table, let table = tables[tableName] {
219251
let name = tableName.description
252+
let type: GenerationType = chunk.isTableOptional ? .optional(.model(table)) : .model(table)
220253
fields[name] = GeneratedField(
221254
name: name,
222-
type: .model(table, isOptional: chunk.isTableOptional),
223-
isArray: false
255+
type: type,
256+
typeName: typeName(for: type)
224257
)
225258
} else {
226259
for column in chunk.columns {
227260
let name = column.key.description
228-
let type = column.value.type
261+
let type = generationType(for: column.value.type)
229262
fields[name] = GeneratedField(
230263
name: name,
231-
type: .builtin(
232-
builtinType(for: type),
233-
isArray: false,
234-
encodedAs: builtinForAliasedType(for: type)
235-
),
236-
isArray: type.isRow
264+
type: type,
265+
typeName: typeName(for: type)
237266
)
238267
}
239268
}
@@ -242,7 +271,7 @@ extension Language {
242271
nonOptionalIndices: []
243272
)
244273

245-
return .model(model, isOptional: false)
274+
return singleOrMany(.model(model))
246275
}
247276
}
248277

@@ -259,62 +288,69 @@ public struct GenerationOptions: Sendable {
259288
}
260289
}
261290

262-
public struct GeneratedModel {
291+
public struct GeneratedModel: Equatable {
263292
let name: String
264293
let fields: OrderedDictionary<String, GeneratedField>
265294
/// Whether or not this was generated for a table
266295
let isTable: Bool
267296
let nonOptionalIndices: [Int]
268297
}
269298

270-
public struct GeneratedField {
299+
public struct GeneratedField: Equatable {
271300
/// The column name
272301
let name: String
273302
/// The type of the field.
274303
/// If it is a `model` that means the user selected
275304
/// all columns from a table `foo.*`
276-
let type: BuiltinOrGenerated
277-
/// Whether or not it is an array. Some fields can take a list
278-
/// as an input for a query like `foo IN :bar`
279-
let isArray: Bool
280-
281-
/// The underlying storage type if it is aliased
282-
var encodedAsType: String? {
283-
guard case let .builtin(_, _, encodedAs) = type else { return nil }
284-
return encodedAs
285-
}
305+
let type: GenerationType
306+
/// The types name to use in the codegen.
307+
/// The name is accessed many times so we can just calculate
308+
/// it once and reuse it.
309+
let typeName: String
286310
}
287311

288312
public struct GeneratedQuery {
289313
let name: String
290314
let variableName: String
291315
let typeName: String
292316
let typealiasName: String
293-
let input: BuiltinOrGenerated?
317+
let input: GenerationType
294318
let inputName: String
295-
let output: BuiltinOrGenerated?
319+
let output: GenerationType
296320
let outputName: String
297321
let outputCardinality: Cardinality
298322
let sourceSql: String
299323
let isReadOnly: Bool
300324
let usedTableNames: [Substring]
325+
let bindings: [Binding]
326+
327+
public enum Binding {
328+
case value(index: Int, name: String, owner: String? = nil, coder: String? = nil)
329+
case arrayStart(name: String, elementName: String)
330+
case arrayEnd
331+
}
301332
}
302333

303-
public enum BuiltinOrGenerated: CustomStringConvertible {
304-
/// Types can be aliased. So `TEXT AS UUID`. `encodedAs`
305-
/// would be the `TEXT`. It will allow us to tell the
306-
/// `bind` functions to actually encode to the underlying
307-
/// type rather than just having `UUID` always go to `TEXT`
308-
/// when some users may want a `BLOB`.
309-
case builtin(String, isArray: Bool, encodedAs: String?)
310-
case model(GeneratedModel, isOptional: Bool)
334+
public enum GenerationType: Equatable {
335+
case void
336+
case builtin(String)
337+
case model(GeneratedModel)
338+
indirect case optional(Self)
339+
indirect case array(Self)
340+
indirect case encoded(Self, alias: String, coder: String)
311341

312-
public var description: String {
342+
var model: GeneratedModel? {
313343
switch self {
314-
case let .builtin(builtin, isArray, _):
315-
isArray ? "[\(builtin)]" : builtin
316-
case let .model(model, isOptional):
317-
isOptional ? "\(model.name)?" : model.name
344+
case .void, .builtin: nil
345+
case .model(let model): model
346+
case .optional(let optional): optional.model
347+
case .array(let array): array.model
348+
case .encoded(let encoded, _, _): encoded.model
318349
}
319350
}
320351
}
352+
353+
public struct RequiredCoder: Hashable {
354+
public let sourceType: String
355+
public let storage: String
356+
}

0 commit comments

Comments
 (0)