Skip to content

Commit b6fa58b

Browse files
authored
Merge pull request #2 from vapor/data-refactor
Data refactor
2 parents efce070 + 50b9d47 commit b6fa58b

9 files changed

+256
-54
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: 19 additions & 18 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,19 +18,12 @@ 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-
switch encoded {
22-
case .dictionary(let dict):
23-
sqlQuery.columns += dict.keys.map { key in
24-
return DataColumn(table: query.entity, name: key)
25-
}
26-
modelData = .init(dict.values)
27-
default:
28-
throw PostgreSQLError(
29-
identifier: "queryData",
30-
reason: "Unsupported PostgreSQLData (dictionary required) created by query data: \(model)"
31-
)
21+
let encoder = PostgreSQLRowEncoder()
22+
try model.encode(to: encoder)
23+
sqlQuery.columns += encoder.data.keys.map { key in
24+
return DataColumn(table: query.entity, name: key)
3225
}
26+
modelData = .init(encoder.data.values)
3327
} else {
3428
modelData = []
3529
}
@@ -41,11 +35,18 @@ extension PostgreSQLDatabase: QuerySupporting {
4135
// Combine the query data with bind values from filters.
4236
// All bind values must come _after_ the columns section of the query.
4337
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)
38+
let encodable = bind.encodable
39+
guard let convertible = encodable as? PostgreSQLDataCustomConvertible else {
40+
let type = Swift.type(of: encodable)
41+
throw PostgreSQLError(
42+
identifier: "convertible",
43+
reason: "Unsupported encodable type: \(type)",
44+
suggestedFixes: [
45+
"Conform \(type) to PostgreSQLDataCustomConvertible"
46+
]
47+
)
4848
}
49+
return try convertible.convertToPostgreSQLData()
4950
}
5051

5152
// Create a push stream to accept the psql output
@@ -56,7 +57,7 @@ extension PostgreSQLDatabase: QuerySupporting {
5657
// Run the query
5758
return try connection.query(sqlString, parameters) { row in
5859
do {
59-
let decoded = try PostgreSQLDataDecoder().decode(D.self, from: .dictionary(row))
60+
let decoded = try D.init(from: PostgreSQLRowDecoder(row: row))
6061
pushStream.push(decoded)
6162
} catch {
6263
pushStream.error(error)
@@ -87,7 +88,7 @@ extension PostgreSQLDatabase: QuerySupporting {
8788
case .didCreate:
8889
if M.ID.self == Int.self {
8990
return connection.simpleQuery("SELECT LASTVAL();").map(to: Void.self) { row in
90-
model.fluentID = row[0]["lastval"]?.int as? M.ID
91+
try! model.fluentID = row[0]["lastval"]?.decode(Int.self) as? M.ID
9192
}
9293
}
9394
default: break

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
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
internal final class PostgreSQLRowDecoder: Decoder {
2+
var codingPath: [CodingKey]
3+
var userInfo: [CodingUserInfoKey: Any]
4+
let data: [String: PostgreSQLData]
5+
init(row: [String: PostgreSQLData]) {
6+
self.data = row
7+
self.codingPath = []
8+
self.userInfo = [:]
9+
}
10+
11+
func container<Key>(keyedBy type: Key.Type) throws -> KeyedDecodingContainer<Key> where Key : CodingKey {
12+
let container = PostgreSQLRowKeyedDecodingContainer<Key>(decoder: self)
13+
return KeyedDecodingContainer(container)
14+
}
15+
16+
func unkeyedContainer() throws -> UnkeyedDecodingContainer {
17+
throw unsupported()
18+
}
19+
20+
func singleValueContainer() throws -> SingleValueDecodingContainer {
21+
throw unsupported()
22+
}
23+
24+
func require(key: CodingKey) throws -> PostgreSQLData {
25+
guard let data = self.data[key.stringValue] else {
26+
throw PostgreSQLError(identifier: "decode", reason: "No value found at key: \(key)")
27+
}
28+
return data
29+
}
30+
31+
}
32+
33+
private func unsupported() -> PostgreSQLError {
34+
return PostgreSQLError(
35+
identifier: "rowDecode",
36+
reason: "PostgreSQL rows only support a flat, keyed structure `[String: T]`",
37+
suggestedFixes: [
38+
"You can conform nested types to `PostgreSQLJSONType` or `PostgreSQLArrayType`. (Nested types must be `PostgreSQLDataCustomConvertible`.)"
39+
]
40+
)
41+
}
42+
43+
44+
fileprivate struct PostgreSQLRowKeyedDecodingContainer<K>: KeyedDecodingContainerProtocol
45+
where K: CodingKey
46+
{
47+
var allKeys: [K]
48+
typealias Key = K
49+
var codingPath: [CodingKey]
50+
let decoder: PostgreSQLRowDecoder
51+
init(decoder: PostgreSQLRowDecoder) {
52+
self.decoder = decoder
53+
codingPath = []
54+
allKeys = self.decoder.data.keys.flatMap { K(stringValue: $0) }
55+
}
56+
func contains(_ key: K) -> Bool { return decoder.data.keys.contains(key.stringValue) }
57+
func decodeNil(forKey key: K) -> Bool { return decoder.data[key.stringValue]?.data == nil }
58+
func decode(_ type: Int.Type, forKey key: K) throws -> Int { return try decoder.require(key: key).decode(Int.self) }
59+
func decode(_ type: Int8.Type, forKey key: K) throws -> Int8 { return try decoder.require(key: key).decode(Int8.self) }
60+
func decode(_ type: Int16.Type, forKey key: K) throws -> Int16 { return try decoder.require(key: key).decode(Int16.self) }
61+
func decode(_ type: Int32.Type, forKey key: K) throws -> Int32 { return try decoder.require(key: key).decode(Int32.self) }
62+
func decode(_ type: Int64.Type, forKey key: K) throws -> Int64 { return try decoder.require(key: key).decode(Int64.self) }
63+
func decode(_ type: UInt.Type, forKey key: K) throws -> UInt { return try decoder.require(key: key).decode(UInt.self) }
64+
func decode(_ type: UInt8.Type, forKey key: K) throws -> UInt8 { return try decoder.require(key: key).decode(UInt8.self) }
65+
func decode(_ type: UInt16.Type, forKey key: K) throws -> UInt16 { return try decoder.require(key: key).decode(UInt16.self) }
66+
func decode(_ type: UInt32.Type, forKey key: K) throws -> UInt32 { return try decoder.require(key: key).decode(UInt32.self) }
67+
func decode(_ type: UInt64.Type, forKey key: K) throws -> UInt64 { return try decoder.require(key: key).decode(UInt64.self) }
68+
func decode(_ type: Double.Type, forKey key: K) throws -> Double { return try decoder.require(key: key).decode(Double.self) }
69+
func decode(_ type: Float.Type, forKey key: K) throws -> Float { return try decoder.require(key: key).decode(Float.self) }
70+
func decode(_ type: Bool.Type, forKey key: K) throws -> Bool { return try decoder.require(key: key).decode(Bool.self) }
71+
func decode(_ type: String.Type, forKey key: K) throws -> String { return try decoder.require(key: key).decode(String.self) }
72+
func decode<T>(_ type: T.Type, forKey key: K) throws -> T where T: Decodable {
73+
guard let convertible = type as? PostgreSQLDataCustomConvertible.Type else {
74+
throw PostgreSQLError(
75+
identifier: "convertible",
76+
reason: "Unsupported decodable type: \(type)",
77+
suggestedFixes: [
78+
"Conform \(type) to PostgreSQLDataCustomConvertible"
79+
]
80+
)
81+
}
82+
return try convertible.convertFromPostgreSQLData(decoder.require(key: key)) as! T
83+
}
84+
85+
func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: K) throws -> KeyedDecodingContainer<NestedKey> where NestedKey : CodingKey {
86+
return try decoder.container(keyedBy: NestedKey.self)
87+
}
88+
89+
func nestedUnkeyedContainer(forKey key: K) throws -> UnkeyedDecodingContainer {
90+
return try decoder.unkeyedContainer()
91+
}
92+
93+
func superDecoder() throws -> Decoder { return decoder }
94+
func superDecoder(forKey key: K) throws -> Decoder { return decoder }
95+
96+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
internal final class PostgreSQLRowEncoder: Encoder {
2+
var codingPath: [CodingKey]
3+
var userInfo: [CodingUserInfoKey : Any]
4+
var data: [String: PostgreSQLData]
5+
init() {
6+
self.codingPath = []
7+
self.userInfo = [:]
8+
self.data = [:]
9+
}
10+
11+
func container<Key>(keyedBy type: Key.Type) -> KeyedEncodingContainer<Key> where Key : CodingKey {
12+
let container = PostgreSQLRowKeyedEncodingContainer<Key>(encoder: self)
13+
return KeyedEncodingContainer(container)
14+
}
15+
16+
func unkeyedContainer() -> UnkeyedEncodingContainer {
17+
unsupported()
18+
}
19+
20+
func singleValueContainer() -> SingleValueEncodingContainer {
21+
unsupported()
22+
}
23+
24+
}
25+
26+
private func unsupported() -> Never {
27+
fatalError("""
28+
PostgreSQL rows only support a flat, keyed structure `[String: T]`.
29+
30+
Query data must be an encodable dictionary, struct, or class.
31+
32+
You can also conform nested types to `PostgreSQLJSONType` or `PostgreSQLArrayType`. (Nested types must be `PostgreSQLDataCustomConvertible`.)
33+
""")
34+
}
35+
36+
fileprivate struct PostgreSQLRowKeyedEncodingContainer<K>: KeyedEncodingContainerProtocol
37+
where K: CodingKey
38+
{
39+
var codingPath: [CodingKey]
40+
let encoder: PostgreSQLRowEncoder
41+
init(encoder: PostgreSQLRowEncoder) {
42+
self.encoder = encoder
43+
self.codingPath = []
44+
}
45+
46+
mutating func encodeNil(forKey key: K) throws { encoder.data[key.stringValue] = PostgreSQLData(type: .void) }
47+
mutating func encode(_ value: Bool, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
48+
mutating func encode(_ value: Int, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
49+
mutating func encode(_ value: Int16, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
50+
mutating func encode(_ value: Int32, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
51+
mutating func encode(_ value: Int64, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
52+
mutating func encode(_ value: UInt, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
53+
mutating func encode(_ value: UInt8, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
54+
mutating func encode(_ value: UInt16, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
55+
mutating func encode(_ value: UInt32, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
56+
mutating func encode(_ value: UInt64, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
57+
mutating func encode(_ value: Double, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
58+
mutating func encode(_ value: Float, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
59+
mutating func encode(_ value: String, forKey key: K) throws { encoder.data[key.stringValue] = try value.convertToPostgreSQLData() }
60+
mutating func superEncoder() -> Encoder { return encoder }
61+
mutating func superEncoder(forKey key: K) -> Encoder { return encoder }
62+
mutating func encode<T>(_ value: T, forKey key: K) throws where T : Encodable {
63+
guard let convertible = value as? PostgreSQLDataCustomConvertible else {
64+
let type = Swift.type(of: value)
65+
throw PostgreSQLError(
66+
identifier: "convertible",
67+
reason: "Unsupported encodable type: \(type)",
68+
suggestedFixes: [
69+
"Conform \(type) to PostgreSQLDataCustomConvertible"
70+
]
71+
)
72+
}
73+
encoder.data[key.stringValue] = try convertible.convertToPostgreSQLData()
74+
}
75+
mutating func nestedContainer<NestedKey>(keyedBy keyType: NestedKey.Type, forKey key: K) -> KeyedEncodingContainer<NestedKey> where NestedKey : CodingKey {
76+
return encoder.container(keyedBy: NestedKey.self)
77+
}
78+
79+
mutating func nestedUnkeyedContainer(forKey key: K) -> UnkeyedEncodingContainer {
80+
return encoder.unkeyedContainer()
81+
}
82+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/// A type that is compatible with PostgreSQL schema and data.
2+
public protocol PostgreSQLType: PostgreSQLColumnStaticRepresentable, PostgreSQLDataCustomConvertible { }
3+
4+
/// A type that is supports being represented as JSONB in a PostgreSQL database.
5+
public protocol PostgreSQLJSONType: PostgreSQLType, PostgreSQLJSONCustomConvertible { }
6+
7+
extension PostgreSQLJSONType {
8+
/// The `PostgreSQLColumn` type that best represents this type.
9+
public static var postgreSQLColumn: PostgreSQLColumn { return .init(type: .jsonb) }
10+
}

0 commit comments

Comments
 (0)