Skip to content

Commit 0d9f13b

Browse files
authored
Add PostgresDynamicTypeThrowingEncodable and PostgresDynamicTypeEncodable (#365)
1 parent 689e4aa commit 0d9f13b

File tree

5 files changed

+128
-25
lines changed

5 files changed

+128
-25
lines changed

Sources/PostgresNIO/New/Data/Array+PostgresCodable.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,10 @@ extension Array: PostgresEncodable where Element: PostgresArrayEncodable {
136136
}
137137
}
138138

139+
// explicitly conforming to PostgresThrowingDynamicTypeEncodable because of:
140+
// https://github.com/apple/swift/issues/54132
141+
extension Array: PostgresThrowingDynamicTypeEncodable where Element: PostgresArrayEncodable {}
142+
139143
extension Array: PostgresNonThrowingEncodable where Element: PostgresArrayEncodable & PostgresNonThrowingEncodable {
140144
public static var psqlType: PostgresDataType {
141145
Element.psqlArrayType
@@ -173,6 +177,9 @@ extension Array: PostgresNonThrowingEncodable where Element: PostgresArrayEncoda
173177
}
174178
}
175179

180+
// explicitly conforming to PostgresDynamicTypeEncodable because of:
181+
// https://github.com/apple/swift/issues/54132
182+
extension Array: PostgresDynamicTypeEncodable where Element: PostgresArrayEncodable & PostgresNonThrowingEncodable {}
176183

177184
extension Array: PostgresDecodable where Element: PostgresArrayDecodable, Element == Element._DecodableType {
178185
public init<JSONDecoder: PostgresJSONDecoder>(

Sources/PostgresNIO/New/Data/Range+PostgresCodable.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@ extension PostgresRange: PostgresEncodable & PostgresNonThrowingEncodable where
191191
}
192192
}
193193

194+
// explicitly conforming to PostgresDynamicTypeEncodable and PostgresThrowingDynamicTypeEncodable because of:
195+
// https://github.com/apple/swift/issues/54132
196+
extension PostgresRange: PostgresThrowingDynamicTypeEncodable & PostgresDynamicTypeEncodable
197+
where Bound: PostgresRangeEncodable {}
198+
194199
extension PostgresRange where Bound: Comparable {
195200
@inlinable
196201
init(range: Range<Bound>) {
@@ -227,6 +232,11 @@ extension Range: PostgresEncodable where Bound: PostgresRangeEncodable {
227232

228233
extension Range: PostgresNonThrowingEncodable where Bound: PostgresRangeEncodable {}
229234

235+
// explicitly conforming to PostgresDynamicTypeEncodable and PostgresThrowingDynamicTypeEncodable because of:
236+
// https://github.com/apple/swift/issues/54132
237+
extension Range: PostgresDynamicTypeEncodable & PostgresThrowingDynamicTypeEncodable
238+
where Bound: PostgresRangeEncodable {}
239+
230240
extension Range: PostgresDecodable where Bound: PostgresRangeDecodable {
231241
@inlinable
232242
public init<JSONDecoder: PostgresJSONDecoder>(
@@ -249,7 +259,7 @@ extension Range: PostgresDecodable where Bound: PostgresRangeDecodable {
249259
else {
250260
throw PostgresDecodingError.Code.failure
251261
}
252-
262+
253263
self = lowerBound..<upperBound
254264
}
255265
}
@@ -270,8 +280,16 @@ extension ClosedRange: PostgresEncodable where Bound: PostgresRangeEncodable {
270280
}
271281
}
272282

283+
// explicitly conforming to PostgresThrowingDynamicTypeEncodable because of:
284+
// https://github.com/apple/swift/issues/54132
285+
extension ClosedRange: PostgresThrowingDynamicTypeEncodable where Bound: PostgresRangeEncodable {}
286+
273287
extension ClosedRange: PostgresNonThrowingEncodable where Bound: PostgresRangeEncodable {}
274288

289+
// explicitly conforming to PostgresDynamicTypeEncodable because of:
290+
// https://github.com/apple/swift/issues/54132
291+
extension ClosedRange: PostgresDynamicTypeEncodable where Bound: PostgresRangeEncodable {}
292+
275293
extension ClosedRange: PostgresDecodable where Bound: PostgresRangeDecodable {
276294
@inlinable
277295
public init<JSONDecoder: PostgresJSONDecoder>(
@@ -301,7 +319,7 @@ extension ClosedRange: PostgresDecodable where Bound: PostgresRangeDecodable {
301319
if lowerBound > upperBound {
302320
throw PostgresDecodingError.Code.failure
303321
}
304-
322+
305323
self = lowerBound...upperBound
306324
}
307325
}

Sources/PostgresNIO/New/PostgresCodable.swift

Lines changed: 53 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,62 @@ import NIOCore
22
import class Foundation.JSONEncoder
33
import class Foundation.JSONDecoder
44

5+
/// A type that can encode itself to a Postgres wire binary representation.
6+
/// Dynamic types are types that don't have a well-known Postgres type OID at compile time.
7+
/// For example, custom types created at runtime, such as enums, or extension types whose OID is not stable between
8+
/// databases.
9+
public protocol PostgresThrowingDynamicTypeEncodable {
10+
/// The data type encoded into the `byteBuffer` in ``encode(into:context:)``
11+
var psqlType: PostgresDataType { get }
12+
13+
/// The Postgres encoding format used to encode the value into `byteBuffer` in ``encode(into:context:)``.
14+
var psqlFormat: PostgresFormat { get }
15+
16+
/// Encode the entity into ``byteBuffer`` in the format specified by ``psqlFormat``,
17+
/// using the provided ``context`` as needed, without setting the byte count.
18+
///
19+
/// This method is called by ``PostgresBindings``.
20+
func encode<JSONEncoder: PostgresJSONEncoder>(
21+
into byteBuffer: inout ByteBuffer,
22+
context: PostgresEncodingContext<JSONEncoder>
23+
) throws
24+
}
25+
26+
/// A type that can encode itself to a Postgres wire binary representation.
27+
/// Dynamic types are types that don't have a well-known Postgres type OID at compile time.
28+
/// For example, custom types created at runtime, such as enums, or extension types whose OID is not stable between
29+
/// databases.
30+
///
31+
/// This is the non-throwing alternative to ``PostgresThrowingDynamicTypeEncodable``. It allows users
32+
/// to create ``PostgresQuery``s via `ExpressibleByStringInterpolation` without having to spell `try`.
33+
public protocol PostgresDynamicTypeEncodable: PostgresThrowingDynamicTypeEncodable {
34+
/// Encode the entity into ``byteBuffer`` in the format specified by ``psqlFormat``,
35+
/// using the provided ``context`` as needed, without setting the byte count.
36+
///
37+
/// This method is called by ``PostgresBindings``.
38+
func encode<JSONEncoder: PostgresJSONEncoder>(
39+
into byteBuffer: inout ByteBuffer,
40+
context: PostgresEncodingContext<JSONEncoder>
41+
)
42+
}
43+
544
/// A type that can encode itself to a postgres wire binary representation.
6-
public protocol PostgresEncodable {
45+
public protocol PostgresEncodable: PostgresThrowingDynamicTypeEncodable {
746
// TODO: Rename to `PostgresThrowingEncodable` with next major release
847

9-
/// identifies the data type that we will encode into `byteBuffer` in `encode`
48+
/// The data type encoded into the `byteBuffer` in ``encode(into:context:)``.
1049
static var psqlType: PostgresDataType { get }
1150

12-
/// identifies the postgres format that is used to encode the value into `byteBuffer` in `encode`
51+
/// The Postgres encoding format used to encode the value into `byteBuffer` in ``encode(into:context:)``.
1352
static var psqlFormat: PostgresFormat { get }
14-
15-
/// Encode the entity into the `byteBuffer` in Postgres binary format, without setting
16-
/// the byte count. This method is called from the ``PostgresBindings``.
17-
func encode<JSONEncoder: PostgresJSONEncoder>(into byteBuffer: inout ByteBuffer, context: PostgresEncodingContext<JSONEncoder>) throws
1853
}
1954

2055
/// A type that can encode itself to a postgres wire binary representation. It enforces that the
2156
/// ``PostgresEncodable/encode(into:context:)-1jkcp`` does not throw. This allows users
22-
/// to create ``PostgresQuery``s using the `ExpressibleByStringInterpolation` without
57+
/// to create ``PostgresQuery``s via `ExpressibleByStringInterpolation` without
2358
/// having to spell `try`.
24-
public protocol PostgresNonThrowingEncodable: PostgresEncodable {
59+
public protocol PostgresNonThrowingEncodable: PostgresEncodable, PostgresDynamicTypeEncodable {
2560
// TODO: Rename to `PostgresEncodable` with next major release
26-
27-
func encode<JSONEncoder: PostgresJSONEncoder>(into byteBuffer: inout ByteBuffer, context: PostgresEncodingContext<JSONEncoder>)
2861
}
2962

3063
/// A type that can decode itself from a postgres wire binary representation.
@@ -84,6 +117,14 @@ extension PostgresDecodable {
84117
public typealias PostgresCodable = PostgresEncodable & PostgresDecodable
85118

86119
extension PostgresEncodable {
120+
@inlinable
121+
public var psqlType: PostgresDataType { Self.psqlType }
122+
123+
@inlinable
124+
public var psqlFormat: PostgresFormat { Self.psqlFormat }
125+
}
126+
127+
extension PostgresThrowingDynamicTypeEncodable {
87128
@inlinable
88129
func encodeRaw<JSONEncoder: PostgresJSONEncoder>(
89130
into buffer: inout ByteBuffer,
@@ -103,7 +144,7 @@ extension PostgresEncodable {
103144
}
104145
}
105146

106-
extension PostgresNonThrowingEncodable {
147+
extension PostgresDynamicTypeEncodable {
107148
@inlinable
108149
func encodeRaw<JSONEncoder: PostgresJSONEncoder>(
109150
into buffer: inout ByteBuffer,

Sources/PostgresNIO/New/PostgresQuery.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -44,13 +44,13 @@ extension PostgresQuery {
4444
}
4545

4646
@inlinable
47-
public mutating func appendInterpolation<Value: PostgresEncodable>(_ value: Value) throws {
47+
public mutating func appendInterpolation<Value: PostgresThrowingDynamicTypeEncodable>(_ value: Value) throws {
4848
try self.binds.append(value, context: .default)
4949
self.sql.append(contentsOf: "$\(self.binds.count)")
5050
}
5151

5252
@inlinable
53-
public mutating func appendInterpolation<Value: PostgresEncodable>(_ value: Optional<Value>) throws {
53+
public mutating func appendInterpolation<Value: PostgresThrowingDynamicTypeEncodable>(_ value: Optional<Value>) throws {
5454
switch value {
5555
case .none:
5656
self.binds.appendNull()
@@ -62,13 +62,13 @@ extension PostgresQuery {
6262
}
6363

6464
@inlinable
65-
public mutating func appendInterpolation<Value: PostgresNonThrowingEncodable>(_ value: Value) {
65+
public mutating func appendInterpolation<Value: PostgresDynamicTypeEncodable>(_ value: Value) {
6666
self.binds.append(value, context: .default)
6767
self.sql.append(contentsOf: "$\(self.binds.count)")
6868
}
6969

7070
@inlinable
71-
public mutating func appendInterpolation<Value: PostgresNonThrowingEncodable>(_ value: Optional<Value>) {
71+
public mutating func appendInterpolation<Value: PostgresDynamicTypeEncodable>(_ value: Optional<Value>) {
7272
switch value {
7373
case .none:
7474
self.binds.appendNull()
@@ -80,7 +80,7 @@ extension PostgresQuery {
8080
}
8181

8282
@inlinable
83-
public mutating func appendInterpolation<Value: PostgresEncodable, JSONEncoder: PostgresJSONEncoder>(
83+
public mutating func appendInterpolation<Value: PostgresThrowingDynamicTypeEncodable, JSONEncoder: PostgresJSONEncoder>(
8484
_ value: Value,
8585
context: PostgresEncodingContext<JSONEncoder>
8686
) throws {
@@ -136,8 +136,8 @@ public struct PostgresBindings: Sendable, Hashable {
136136
}
137137

138138
@inlinable
139-
init<Value: PostgresEncodable>(value: Value, protected: Bool) {
140-
self.init(dataType: Value.psqlType, format: Value.psqlFormat, protected: protected)
139+
init<Value: PostgresThrowingDynamicTypeEncodable>(value: Value, protected: Bool) {
140+
self.init(dataType: value.psqlType, format: value.psqlFormat, protected: protected)
141141
}
142142
}
143143

@@ -168,12 +168,12 @@ public struct PostgresBindings: Sendable, Hashable {
168168
}
169169

170170
@inlinable
171-
public mutating func append<Value: PostgresEncodable>(_ value: Value) throws {
171+
public mutating func append<Value: PostgresThrowingDynamicTypeEncodable>(_ value: Value) throws {
172172
try self.append(value, context: .default)
173173
}
174174

175175
@inlinable
176-
public mutating func append<Value: PostgresEncodable, JSONEncoder: PostgresJSONEncoder>(
176+
public mutating func append<Value: PostgresThrowingDynamicTypeEncodable, JSONEncoder: PostgresJSONEncoder>(
177177
_ value: Value,
178178
context: PostgresEncodingContext<JSONEncoder>
179179
) throws {
@@ -182,12 +182,12 @@ public struct PostgresBindings: Sendable, Hashable {
182182
}
183183

184184
@inlinable
185-
public mutating func append<Value: PostgresNonThrowingEncodable>(_ value: Value) {
185+
public mutating func append<Value: PostgresDynamicTypeEncodable>(_ value: Value) {
186186
self.append(value, context: .default)
187187
}
188188

189189
@inlinable
190-
public mutating func append<Value: PostgresNonThrowingEncodable, JSONEncoder: PostgresJSONEncoder>(
190+
public mutating func append<Value: PostgresDynamicTypeEncodable, JSONEncoder: PostgresJSONEncoder>(
191191
_ value: Value,
192192
context: PostgresEncodingContext<JSONEncoder>
193193
) {

Tests/PostgresNIOTests/New/PostgresQueryTests.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,27 @@ final class PostgresQueryTests: XCTestCase {
3131
XCTAssertEqual(query.binds.bytes, expected)
3232
}
3333

34+
func testStringInterpolationWithDynamicType() {
35+
let type = PostgresDataType(16435)
36+
let format = PostgresFormat.binary
37+
let dynamicString = DynamicString(value: "Hello world", psqlType: type, psqlFormat: format)
38+
39+
let query: PostgresQuery = """
40+
INSERT INTO foo (dynamicType) SET (\(dynamicString));
41+
"""
42+
43+
XCTAssertEqual(query.sql, "INSERT INTO foo (dynamicType) SET ($1);")
44+
45+
var expectedBindsBytes = ByteBuffer()
46+
expectedBindsBytes.writeInteger(Int32(dynamicString.value.utf8.count))
47+
expectedBindsBytes.writeString(dynamicString.value)
48+
49+
let expectedMetadata: [PostgresBindings.Metadata] = [.init(dataType: type, format: format, protected: true)]
50+
51+
XCTAssertEqual(query.binds.bytes, expectedBindsBytes)
52+
XCTAssertEqual(query.binds.metadata, expectedMetadata)
53+
}
54+
3455
func testStringInterpolationWithCustomJSONEncoder() {
3556
struct Foo: Codable, PostgresCodable {
3657
var helloWorld: String
@@ -89,3 +110,19 @@ final class PostgresQueryTests: XCTestCase {
89110
XCTAssertEqual(query.binds.bytes, expected)
90111
}
91112
}
113+
114+
extension PostgresQueryTests {
115+
struct DynamicString: PostgresDynamicTypeEncodable {
116+
let value: String
117+
118+
var psqlType: PostgresDataType
119+
var psqlFormat: PostgresFormat
120+
121+
func encode<JSONEncoder>(
122+
into byteBuffer: inout ByteBuffer,
123+
context: PostgresNIO.PostgresEncodingContext<JSONEncoder>
124+
) where JSONEncoder: PostgresJSONEncoder {
125+
byteBuffer.writeString(value)
126+
}
127+
}
128+
}

0 commit comments

Comments
 (0)