Skip to content

Commit fd465c4

Browse files
committed
Custom encoders
1 parent 28edf2c commit fd465c4

14 files changed

Lines changed: 135 additions & 26 deletions

Sources/Compiler/Gen/Language.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ extension Language {
174174
switch type {
175175
case let .nominal(name):
176176
return .builtin(builtinType(named: name))
177-
case let .alias(root, alias):
177+
case let .alias(root, alias, coder):
178178
let alias = switch alias {
179179
case .explicit(let e): e.description
180180
case .hint(let hint):
@@ -183,7 +183,11 @@ extension Language {
183183
}
184184
}
185185

186-
return .encoded(generationType(for: root), alias: alias, coder: "\(alias)DatabaseValueCoder")
186+
return .encoded(
187+
generationType(for: root),
188+
alias: alias,
189+
coder: "\(coder?.description ?? alias)DatabaseValueCoder"
190+
)
187191
case let .optional(type):
188192
return .optional(generationType(for: type))
189193
case let .row(.unknown(type)):

Sources/Compiler/Gen/Rewriter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,15 @@ extension Rewriter: StmtSyntaxVisitor {
109109
func visit(_ stmt: CreateTableStmtSyntax) -> [Range<Substring.Index>] {
110110
switch stmt.kind {
111111
case let .columns(columns, _, _):
112-
return columns.values.compactMap { $0.type.alias?.location.range }
112+
return columns.values.compactMap { $0.type.alias?.name.location.range }
113113
case .select:
114114
return []
115115
}
116116
}
117117

118118
func visit(_ stmt: AlterTableStmtSyntax) -> [Range<Substring.Index>] {
119119
return switch stmt.kind {
120-
case let .addColumn(c): c.type.alias.map { [$0.location.range] } ?? []
120+
case let .addColumn(c): c.type.alias.map { [$0.name.location.range] } ?? []
121121
default: []
122122
}
123123
}

Sources/Compiler/Gen/SwiftLanguage.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public struct SwiftLanguage: Language {
3131
"Float16DatabaseValueCoder",
3232
"UUIDDatabaseValueCoder",
3333
"DecimalDatabaseValueCoder",
34+
"DateDatabaseValueCoder",
3435
]
3536
}
3637

Sources/Compiler/Parse/Parsers.swift

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1293,7 +1293,7 @@ enum Parsers {
12931293
if state.take(if: .comma) {
12941294
let second = signedNumber(state: &state)
12951295
state.skip(.closeParen)
1296-
let alias = maybeAlias(state: &state)
1296+
let alias = typeNameAlias(state: &state)
12971297
return TypeNameSyntax(
12981298
id: state.nextId(),
12991299
name: name,
@@ -1304,7 +1304,7 @@ enum Parsers {
13041304
)
13051305
} else {
13061306
state.skip(.closeParen)
1307-
let alias = maybeAlias(state: &state)
1307+
let alias = typeNameAlias(state: &state)
13081308
return TypeNameSyntax(
13091309
id: state.nextId(),
13101310
name: name,
@@ -1315,7 +1315,7 @@ enum Parsers {
13151315
)
13161316
}
13171317
} else {
1318-
let alias = maybeAlias(state: &state)
1318+
let alias = typeNameAlias(state: &state)
13191319
return TypeNameSyntax(
13201320
id: state.nextId(),
13211321
name: name,
@@ -1327,6 +1327,23 @@ enum Parsers {
13271327
}
13281328
}
13291329

1330+
static func typeNameAlias(state: inout ParserState) -> TypeNameSyntax.Alias? {
1331+
guard state.current.kind == .as else { return nil }
1332+
1333+
let start = state.take()
1334+
let ident = identifier(state: &state)
1335+
let name = AliasSyntax(
1336+
id: state.nextId(),
1337+
identifier: ident,
1338+
location: state.location(from: start)
1339+
)
1340+
1341+
return TypeNameSyntax.Alias(
1342+
name: name,
1343+
using: state.take(if: .using) ? identifier(state: &state) : nil
1344+
)
1345+
}
1346+
13301347
/// https://www.sqlite.org/lang_altertable.html
13311348
static func alterStmt(state: inout ParserState) throws -> AlterTableStmtSyntax {
13321349
let alter = state.take(.alter)

Sources/Compiler/Sema/InferenceState.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ struct InferenceState {
123123
for type: Type,
124124
at location: SourceLocation
125125
) {
126-
unify(type, with: .alias(type, .hint(hint)), at: location)
126+
unify(type, with: .alias(type, .hint(hint), coder: nil), at: location)
127127
}
128128

129129
/// Gets the final type from the solution for the type if its a ty var.
@@ -141,8 +141,8 @@ struct InferenceState {
141141
return tv.defaultType
142142
case let .optional(ty):
143143
return .optional(solution(for: ty, defaultIfTyVar: true))
144-
case let .alias(ty, alias):
145-
return .alias(solution(for: ty, defaultIfTyVar: true), alias)
144+
case let .alias(ty, alias, coder):
145+
return .alias(solution(for: ty, defaultIfTyVar: true), alias, coder: coder)
146146
case let .row(row):
147147
if let type = row.first, row.count == 1, !row.isUnknown {
148148
return solution(for: type, defaultIfTyVar: true)
@@ -254,10 +254,10 @@ extension InferenceState {
254254
case let (.row(rhs), .row(lhs)) where lhs.count == rhs.count:
255255
return unify(rhs.types, with: lhs.types, at: location)
256256

257-
case let (.alias(t1, _), t2):
257+
case let (.alias(t1, _, _), t2):
258258
return unify(t1, with: t2, at: location)
259259

260-
case let (t1, .alias(t2, _)):
260+
case let (t1, .alias(t2, _, _)):
261261
return unify(t2, with: t1, at: location)
262262

263263
default:
@@ -327,7 +327,7 @@ extension InferenceState {
327327
at location: SourceLocation
328328
) {
329329
switch type {
330-
case let .alias(t, _):
330+
case let .alias(t, _, _):
331331
return validateCanUnify(type: t, with: tvKind, at: location)
332332
case let .optional(t):
333333
return validateCanUnify(type: t, with: tvKind, at: location)

Sources/Compiler/Sema/StmtTypeChecker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1304,7 +1304,7 @@ extension StmtTypeChecker {
13041304
let nominal: Type = .nominal(column.type.name.value)
13051305

13061306
let type: Type = if let alias = column.type.alias {
1307-
.alias(nominal, .explicit(alias.identifier.value))
1307+
.alias(nominal, .explicit(alias.name.identifier.value), coder: alias.using?.value)
13081308
} else {
13091309
nominal
13101310
}

Sources/Compiler/Sema/Type.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public enum Type: Equatable, CustomStringConvertible, Sendable {
2424
/// A type that has been aliased. These are not in SQL by default
2525
/// but are from the layer on top that we are adding so a user
2626
/// can replace a `INTEGER` with a `Bool`
27-
indirect case alias(Type, Alias)
27+
indirect case alias(Type, Alias, coder: Substring?)
2828
/// There was an error somewhere in the analysis. We can just return
2929
/// an `error` type and continue the analysis. So if the user makes up
3030
/// 3 columns, they can get all 3 errors at once.
@@ -43,7 +43,7 @@ public enum Type: Equatable, CustomStringConvertible, Sendable {
4343
static let real: Type = .nominal("REAL")
4444
static let blob: Type = .nominal("BLOB")
4545
static let any: Type = .nominal("ANY")
46-
static let boolean: Type = .alias(.integer, .hint(.bool))
46+
static let boolean: Type = .alias(.integer, .hint(.bool), coder: nil)
4747

4848
static let validTypeNames: Set<Substring> = [
4949
"TEXT", "INT", "INTEGER", "REAL", "BLOB", "ANY",
@@ -89,15 +89,15 @@ public enum Type: Equatable, CustomStringConvertible, Sendable {
8989
case let .unknown(ty): "(\(ty)...)"
9090
}
9191
case let .optional(ty): "\(ty)?"
92-
case let .alias(t, a): "(\(t) AS \(a))"
92+
case let .alias(t, a, _): "(\(t) AS \(a))"
9393
case .error: "<<error>>"
9494
}
9595
}
9696

9797
/// The underlying root inner type
9898
var root: Type {
9999
return switch self {
100-
case let .alias(t, _): t.root
100+
case let .alias(t, _, _): t.root
101101
case let .optional(t): t.root
102102
default: self
103103
}
@@ -115,7 +115,7 @@ public enum Type: Equatable, CustomStringConvertible, Sendable {
115115
var isOptional: Bool {
116116
switch self {
117117
case .nominal, .error, .row, .fn, .var: false
118-
case let .alias(t, _): t.isOptional
118+
case let .alias(t, _, _): t.isOptional
119119
case .optional: true
120120
}
121121
}
@@ -129,7 +129,7 @@ public enum Type: Equatable, CustomStringConvertible, Sendable {
129129
/// The alias if there is one
130130
var alias: Alias? {
131131
switch self {
132-
case .alias(_, let a): a
132+
case .alias(_, let a, _): a
133133
case .optional(let t): t.alias
134134
default: nil
135135
}
@@ -165,8 +165,8 @@ public enum Type: Equatable, CustomStringConvertible, Sendable {
165165
return .row(tys.apply(s))
166166
case let .optional(ty):
167167
return .optional(ty.apply(s))
168-
case let .alias(t, a):
169-
return .alias(t.apply(s), a)
168+
case let .alias(t, a, c):
169+
return .alias(t.apply(s), a, coder: c)
170170
case .nominal, .error:
171171
// Literals can't be substituted for.
172172
return self

Sources/Compiler/Syntax/TypeNameSyntax.swift

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,21 @@ struct TypeNameSyntax: Syntax, CustomStringConvertible, Sendable {
1010
let name: IdentifierSyntax
1111
let arg1: SignedNumberSyntax?
1212
let arg2: SignedNumberSyntax?
13-
let alias: AliasSyntax?
13+
let alias: Alias?
1414
let location: SourceLocation
15+
16+
struct Alias: CustomStringConvertible {
17+
let name: AliasSyntax
18+
let using: IdentifierSyntax?
19+
20+
var description: String {
21+
if let using {
22+
return "\(name) USING \(using)"
23+
} else {
24+
return name.description
25+
}
26+
}
27+
}
1528

1629
var description: String {
1730
let type = if let arg1, let arg2 {

Sources/Otter/DatabaseValueCoder.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,45 @@ public enum DecimalDatabaseValueCoder: DatabaseValueCoder {
234234
try .double(encodeToDouble(value: value))
235235
}
236236
}
237+
238+
public enum DateDatabaseValueCoder: DatabaseValueCoder {
239+
@usableFromInline static nonisolated(unsafe) let formatter: ISO8601DateFormatter = {
240+
// Note: In the future might want to move off of this and have a custom
241+
// date parser cause I don't think it's performance is the best.
242+
let formatter = ISO8601DateFormatter()
243+
formatter.formatOptions = [.withInternetDateTime]
244+
return formatter
245+
}()
246+
247+
@inlinable public static func decode(from primitive: Int) throws(OtterError) -> Date {
248+
return Date(timeIntervalSince1970: TimeInterval(primitive))
249+
}
250+
251+
@inlinable public static func decode(from primitive: Double) throws(OtterError) -> Date {
252+
return Date(timeIntervalSince1970: primitive)
253+
}
254+
255+
@inlinable public static func decode(from primitive: String) throws(OtterError) -> Date {
256+
guard let date = formatter.date(from: primitive) else {
257+
throw .cannotDecode(Date.self, from: String.self, reason: "Invalid date string: '\(primitive)'")
258+
}
259+
return date
260+
}
261+
262+
@inlinable public static func encodeToInt(value: Date) throws(OtterError) -> Int {
263+
Int(value.timeIntervalSince1970)
264+
}
265+
266+
@inlinable public static func encodeToDouble(value: Date) throws(OtterError) -> Double {
267+
value.timeIntervalSince1970
268+
}
269+
270+
@inlinable public static func encodeToString(value: Date) throws(OtterError) -> String {
271+
return formatter.string(from: value)
272+
}
273+
274+
@inlinable public static func encodeToAny(value: Date) throws(OtterError) -> SQLAny {
275+
// By default just go to double since its going to be the fastest
276+
try .double(encodeToDouble(value: value))
277+
}
278+
}

Tests/CompilerTests/Gen/Migrations.sql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ CREATE TABLE user (
55
preference INTEGER AS Bool,
66
favoriteNumber INTEGER,
77
randomValue ANY,
8+
bornOn STRING AS Date USING CustomDate NOT NULL,
89
fullName TEXT NOT NULL GENERATED ALWAYS AS (firstName || ' ' || lastName)
910
);
1011

0 commit comments

Comments
 (0)