Skip to content

Commit 085a1ae

Browse files
committed
psql data refactor
1 parent efce070 commit 085a1ae

File tree

6 files changed

+169
-44
lines changed

6 files changed

+169
-44
lines changed

Package.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,17 @@ let package = Package(
1111
// Swift Promises, Futures, and Streams.
1212
.package(url: "https://github.com/vapor/async.git", .branch("beta")),
1313

14+
// Core extensions, type-aliases, and functions that facilitate common tasks.
15+
.package(url: "https://github.com/vapor/core.git", .branch("beta")),
16+
1417
// Swift ORM framework (queries, models, and relations) for building NoSQL and SQL database integrations.
1518
.package(url: "https://github.com/vapor/fluent.git", .branch("beta")),
1619

1720
// Pure Swift, async/non-blocking client for PostgreSQL.
1821
.package(url: "https://github.com/vapor/postgresql.git", .branch("beta")),
1922
],
2023
targets: [
21-
.target(name: "FluentPostgreSQL", dependencies: ["Async", "Fluent", "FluentSQL", "PostgreSQL"]),
24+
.target(name: "FluentPostgreSQL", dependencies: ["Async", "CodableKit", "Fluent", "FluentSQL", "PostgreSQL"]),
2225
.testTarget(name: "FluentPostgreSQLTests", dependencies: ["FluentBenchmark", "FluentPostgreSQL"]),
2326
]
2427
)

Sources/FluentPostgreSQL/PostgreSQLColumn.swift

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -35,41 +35,11 @@ extension UUID: PostgreSQLColumnStaticRepresentable {
3535
public static var postgreSQLColumn: PostgreSQLColumn { return .init(type: .uuid) }
3636
}
3737

38-
extension UUID: PostgreSQLDataCustomConvertible {
39-
/// See `PostgreSQLDataCustomConvertible.convertFromPostgreSQLData(from:)`
40-
public static func convertFromPostgreSQLData(from data: PostgreSQLData) throws -> UUID {
41-
switch data {
42-
case .uuid(let uuid): return uuid
43-
default: throw PostgreSQLError(identifier: "uuid", reason: "Could not convert data to UUID.")
44-
}
45-
}
46-
47-
/// See `PostgreSQLDataCustomConvertible.convertToPostgreSQLData()`
48-
public func convertToPostgreSQLData() throws -> PostgreSQLData {
49-
return .uuid(self)
50-
}
51-
}
52-
5338
extension Date: PostgreSQLColumnStaticRepresentable {
5439
/// See `PostgreSQLColumnStaticRepresentable.postgreSQLColumn`
5540
public static var postgreSQLColumn: PostgreSQLColumn { return .init(type: .timestamp) }
5641
}
5742

58-
extension Date: PostgreSQLDataCustomConvertible {
59-
/// See `PostgreSQLDataCustomConvertible.convertFromPostgreSQLData(from:)`
60-
public static func convertFromPostgreSQLData(from data: PostgreSQLData) throws -> Date {
61-
switch data {
62-
case .date(let date): return date
63-
default: throw PostgreSQLError(identifier: "date", reason: "Could not convert data to Date.")
64-
}
65-
}
66-
67-
/// See `PostgreSQLDataCustomConvertible.convertToPostgreSQLData()`
68-
public func convertToPostgreSQLData() throws -> PostgreSQLData {
69-
return .date(self)
70-
}
71-
}
72-
7343
extension FixedWidthInteger {
7444
/// See `PostgreSQLColumnStaticRepresentable.postgreSQLColumn`
7545
public static var postgreSQLColumn: PostgreSQLColumn {

Sources/FluentPostgreSQL/PostgreSQLDatabase+LogSupporting.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ extension PostgreSQLDatabase: LogSupporting {
88
extension DatabaseLogger: PostgreSQLLogger {
99
/// See `PostgreSQLLogger.log(query:parameters:)`
1010
public func log(query: String, parameters: [PostgreSQLData]) {
11-
let log = DatabaseLog(query: query, values: parameters.map { $0.description }, dbID: "postgresql", date: .init())
11+
let log = DatabaseLog(query: query, values: parameters.map { $0.data?.description ?? "nil" }, dbID: "postgresql", date: .init())
1212
self.record(log: log)
1313
}
1414
}

Sources/FluentPostgreSQL/PostgreSQLDatabase+QuerySupporting.swift

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Async
2+
import CodableKit
23
import FluentSQL
34
import Foundation
45

@@ -17,13 +18,46 @@ extension PostgreSQLDatabase: QuerySupporting {
1718
// Dictionary values should be added to the parameterized array.
1819
let modelData: [PostgreSQLData]
1920
if let model = query.data {
20-
let encoded = try PostgreSQLDataEncoder().encode(model)
21+
let encoded = try CodableDataEncoder().encode(model)
22+
print(encoded)
2123
switch encoded {
2224
case .dictionary(let dict):
2325
sqlQuery.columns += dict.keys.map { key in
2426
return DataColumn(table: query.entity, name: key)
2527
}
26-
modelData = .init(dict.values)
28+
modelData = try dict.values.map { codableData -> PostgreSQLData in
29+
switch codableData {
30+
case .bool(let value): return try value.convertToPostgreSQLData()
31+
case .int(let value): return try value.convertToPostgreSQLData()
32+
case .int8(let value): return try value.convertToPostgreSQLData()
33+
case .int16(let value): return try value.convertToPostgreSQLData()
34+
case .int32(let value): return try value.convertToPostgreSQLData()
35+
case .int64(let value): return try value.convertToPostgreSQLData()
36+
case .uint(let value): return try value.convertToPostgreSQLData()
37+
case .uint8(let value): return try value.convertToPostgreSQLData()
38+
case .uint16(let value): return try value.convertToPostgreSQLData()
39+
case .uint32(let value): return try value.convertToPostgreSQLData()
40+
case .uint64(let value): return try value.convertToPostgreSQLData()
41+
case .float(let value): return try value.convertToPostgreSQLData()
42+
case .double(let value): return try value.convertToPostgreSQLData()
43+
case .string(let value): return try value.convertToPostgreSQLData()
44+
case .null: return PostgreSQLData(type: .void)
45+
case .encodable(let encodable):
46+
guard let convertible = encodable as? PostgreSQLDataCustomConvertible else {
47+
let type = Swift.type(of: encodable)
48+
throw PostgreSQLError(
49+
identifier: "convertible",
50+
reason: "Unsupported encodable type: \(type)",
51+
suggestedFixes: [
52+
"Conform \(type) to PostgreSQLDataCustomConvertible"
53+
]
54+
)
55+
}
56+
return try convertible.convertToPostgreSQLData()
57+
case .array, .dictionary, .decoder:
58+
throw PostgreSQLError(identifier: "codable", reason: "Unsupported codable type: \(codableData)")
59+
}
60+
}
2761
default:
2862
throw PostgreSQLError(
2963
identifier: "queryData",
@@ -41,11 +75,18 @@ extension PostgreSQLDatabase: QuerySupporting {
4175
// Combine the query data with bind values from filters.
4276
// All bind values must come _after_ the columns section of the query.
4377
let parameters = try modelData + bindValues.map { bind in
44-
if let uuid = bind.encodable as? UUID {
45-
return .uuid(uuid)
46-
} else {
47-
return try PostgreSQLDataEncoder().encode(bind.encodable)
78+
let encodable = bind.encodable
79+
guard let convertible = encodable as? PostgreSQLDataCustomConvertible else {
80+
let type = Swift.type(of: encodable)
81+
throw PostgreSQLError(
82+
identifier: "convertible",
83+
reason: "Unsupported encodable type: \(type)",
84+
suggestedFixes: [
85+
"Conform \(type) to PostgreSQLDataCustomConvertible"
86+
]
87+
)
4888
}
89+
return try convertible.convertToPostgreSQLData()
4990
}
5091

5192
// Create a push stream to accept the psql output
@@ -55,8 +96,12 @@ extension PostgreSQLDatabase: QuerySupporting {
5596

5697
// Run the query
5798
return try connection.query(sqlString, parameters) { row in
99+
let codableDict = row.mapValues { psqlData -> CodableData in
100+
return .decoder(PostgreSQLDataDecoder(data: psqlData))
101+
}
102+
58103
do {
59-
let decoded = try PostgreSQLDataDecoder().decode(D.self, from: .dictionary(row))
104+
let decoded = try CodableDataDecoder().decode(D.self, from: .dictionary(codableDict))
60105
pushStream.push(decoded)
61106
} catch {
62107
pushStream.error(error)
@@ -87,7 +132,7 @@ extension PostgreSQLDatabase: QuerySupporting {
87132
case .didCreate:
88133
if M.ID.self == Int.self {
89134
return connection.simpleQuery("SELECT LASTVAL();").map(to: Void.self) { row in
90-
model.fluentID = row[0]["lastval"]?.int as? M.ID
135+
try! model.fluentID = row[0]["lastval"]?.decode(Int.self) as? M.ID
91136
}
92137
}
93138
default: break
@@ -96,3 +141,62 @@ extension PostgreSQLDatabase: QuerySupporting {
96141
return .done
97142
}
98143
}
144+
145+
146+
internal struct PostgreSQLDataDecoder: Decoder, SingleValueDecodingContainer {
147+
148+
var codingPath: [CodingKey]
149+
var userInfo: [CodingUserInfoKey: Any]
150+
let data: PostgreSQLData
151+
init(data: PostgreSQLData) {
152+
self.data = data
153+
self.userInfo = [:]
154+
self.codingPath = []
155+
}
156+
157+
func singleValueContainer() throws -> SingleValueDecodingContainer { return self }
158+
func decodeNil() -> Bool { return data.data == nil }
159+
func decode(_ type: Int.Type) throws -> Int { return try data.decode(Int.self) }
160+
func decode(_ type: Int8.Type) throws -> Int8 { return try data.decode(Int8.self) }
161+
func decode(_ type: Int16.Type) throws -> Int16 { return try data.decode(Int16.self) }
162+
func decode(_ type: Int32.Type) throws -> Int32 { return try data.decode(Int32.self) }
163+
func decode(_ type: Int64.Type) throws -> Int64 { return try data.decode(Int64.self) }
164+
func decode(_ type: UInt.Type) throws -> UInt { return try data.decode(UInt.self) }
165+
func decode(_ type: UInt8.Type) throws -> UInt8 { return try data.decode(UInt8.self) }
166+
func decode(_ type: UInt16.Type) throws -> UInt16 { return try data.decode(UInt16.self) }
167+
func decode(_ type: UInt32.Type) throws -> UInt32 { return try data.decode(UInt32.self) }
168+
func decode(_ type: UInt64.Type) throws -> UInt64 { return try data.decode(UInt64.self) }
169+
func decode(_ type: Double.Type) throws -> Double { return try data.decode(Double.self) }
170+
func decode(_ type: Float.Type) throws -> Float { return try data.decode(Float.self) }
171+
func decode(_ type: Bool.Type) throws -> Bool { return try data.decode(Bool.self) }
172+
func decode(_ type: String.Type) throws -> String { return try data.decode(String.self) }
173+
func decode<T>(_ type: T.Type) throws -> T where T: Decodable {
174+
guard let convertible = type as? PostgreSQLDataCustomConvertible.Type else {
175+
throw PostgreSQLError(
176+
identifier: "convertible",
177+
reason: "Unsupported decodable type: \(type)",
178+
suggestedFixes: [
179+
"Conform \(type) to PostgreSQLDataCustomConvertible"
180+
]
181+
)
182+
}
183+
return try convertible.convertFromPostgreSQLData(data) as! T
184+
}
185+
186+
// unsupported
187+
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
188+
throw PostgreSQLError(
189+
identifier: "decoding",
190+
reason: "Keyed decoding container not supported",
191+
suggestedFixes: ["Use a nested struct isntead"]
192+
)
193+
}
194+
195+
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
196+
throw PostgreSQLError(
197+
identifier: "decoding",
198+
reason: "Unkeyed decoding container not supported",
199+
suggestedFixes: ["Use a nested struct isntead"]
200+
)
201+
}
202+
}

Sources/FluentPostgreSQL/PostgreSQLDatabase+SchemaSupporting.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ extension PostgreSQLDatabase: SchemaSupporting {
2626
case .numeric: string = "NUMERIC"
2727
case .void: string = "VOID"
2828
case .uuid: string = "UUID"
29+
case .jsonb: string = "JSONB"
30+
case .json: string = "JSON"
2931
case .pg_node_tree: string = "pg_node_tree"
3032
default: string = "VOID" // FIXME: better error?
3133
}

Tests/FluentPostgreSQLTests/FluentPostgreSQLTests.swift

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import FluentPostgreSQL
55

66
class FluentPostgreSQLTests: XCTestCase {
77
var benchmarker: Benchmarker<PostgreSQLDatabase>!
8-
var worker: EventLoop!
8+
var eventLoop: EventLoop!
9+
var database: PostgreSQLDatabase!
910

1011
override func setUp() {
11-
self.worker = try! DefaultEventLoop(label: "codes.vapor.postgresql.test")
12-
let database = PostgreSQLDatabase(config: .default())
13-
benchmarker = Benchmarker(database, config: .init(), on: worker, onFail: XCTFail)
12+
eventLoop = try! DefaultEventLoop(label: "codes.vapor.postgresql.test")
13+
database = PostgreSQLDatabase(config: .default())
14+
benchmarker = Benchmarker(database, config: .init(), on: eventLoop, onFail: XCTFail)
1415
}
1516

1617
func testSchema() throws {
@@ -41,6 +42,21 @@ class FluentPostgreSQLTests: XCTestCase {
4142
try benchmarker.benchmarkAutoincrement_withSchema()
4243
}
4344

45+
// func testNestedStruct() throws {
46+
// database.logger = DatabaseLogger { print($0) }
47+
// let conn = try database.makeConnection(using: .init(), on: eventLoop).await(on: eventLoop)
48+
// try! User.prepare(on: conn).await(on: eventLoop)
49+
//
50+
// let user = try! User(id: nil, name: "Tanner", pet: Pet(name: "Zizek"))
51+
// .save(on: conn).await(on: eventLoop)
52+
//
53+
// let fetched = try! User.query(on: conn).first().await(on: eventLoop)
54+
// print(fetched?.pet)
55+
//
56+
// try User.revert(on: conn).await(on: eventLoop)
57+
// conn.close()
58+
// }
59+
4460
static let allTests = [
4561
("testSchema", testSchema),
4662
("testModels", testModels),
@@ -51,3 +67,33 @@ class FluentPostgreSQLTests: XCTestCase {
5167
("testAutoincrement", testAutoincrement),
5268
]
5369
}
70+
//
71+
//struct Pet: Codable, PostgreSQLColumnStaticRepresentable, PostgreSQLDataCustomConvertible {
72+
// static let postgreSQLColumn = PostgreSQLColumn(type: .jsonb)
73+
// var name: String
74+
//
75+
// func convertToPostgreSQLData() throws -> PostgreSQLData {
76+
// return try .data(JSONEncoder().encode(self))
77+
// }
78+
//
79+
// static func convertFromPostgreSQLData(from data: PostgreSQLData) throws -> Pet {
80+
// switch data {
81+
// case .data(let data): return try JSONDecoder().decode(Pet.self, from: data)
82+
// default: fatalError()
83+
// }
84+
// }
85+
//}
86+
//
87+
//final class User: PostgreSQLModel, Migration {
88+
// static let idKey = \User.id
89+
// var id: Int?
90+
// var name: String
91+
// var pet: Pet
92+
//
93+
// init(id: Int? = nil, name: String, pet: Pet) {
94+
// self.id = id
95+
// self.name = name
96+
// self.pet = pet
97+
// }
98+
//}
99+

0 commit comments

Comments
 (0)