Skip to content

Commit 81a309a

Browse files
1.0.0 gm (#97)
* update docs * update versions * add explicit check for binary for array encoding, #92 * Prepared Statement API (#15) * Added an API for prepared queries # Conflicts: # Sources/PostgresNIO/Connection/PostgresDatabase+PreparedQuery.swift # Tests/NIOPostgresTests/NIOPostgresTests.swift * Updated testPreparedQuery to close the connection # Conflicts: # Tests/NIOPostgresTests/NIOPostgresTests.swift * Added a DEALLOCATE api * Added a closure based API * Implemented log(to:) * Made prepared query a strcut * Spacing fixes * Implement 'Close' command for prepared statements * copy data row column slice (#96) * copy data row column slice * fix double read Co-authored-by: Thomas Bartelmess <[email protected]>
1 parent a528171 commit 81a309a

File tree

9 files changed

+287
-8
lines changed

9 files changed

+287
-8
lines changed
File renamed without changes.

README.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
<a href="LICENSE">
1010
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
1111
</a>
12-
<a href="https://circleci.com/gh/vapor/postgres-nio">
13-
<img src="https://circleci.com/gh/vapor/postgres-nio.svg?style=shield" alt="Continuous Integration">
12+
<a href="https://github.com/vapor/postgres-nio/actions">
13+
<img src="https://github.com/vapor/postgres-nio/workflows/test/badge.svg" alt="Continuous Integration">
1414
</a>
1515
<a href="https://swift.org">
16-
<img src="http://img.shields.io/badge/swift-5-brightgreen.svg" alt="Swift 5">
16+
<img src="http://img.shields.io/badge/swift-5.2-brightgreen.svg" alt="Swift 5.2">
1717
</a>
1818
<br>
1919
<br>
@@ -24,9 +24,9 @@
2424

2525
The table below shows a list of PostgresNIO major releases alongside their compatible NIO and Swift versions.
2626

27-
Version | NIO | Swift | SPM
28-
--- | --- | --- | ---
29-
1.0 (alpha) | 2.0+ | 5.0+ | `from: "1.0.0-alpha"`
27+
|Version|NIO|Swift|SPM|
28+
|-|-|-|-|
29+
|1.0|2.0+|5.2+|`from: "1.0.0"`|
3030

3131
Use the SPM string to easily include the dependendency in your `Package.swift` file.
3232

Sources/PostgresNIO/Connection/PostgresConnection+Database.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ final class PostgresRequestContext {
2929
}
3030
}
3131

32-
final class PostgresRequestHandler: ChannelDuplexHandler {
32+
class PostgresRequestHandler: ChannelDuplexHandler {
3333
typealias InboundIn = PostgresMessage
3434
typealias OutboundIn = PostgresRequestContext
3535
typealias OutboundOut = PostgresMessage
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import NIO
2+
3+
4+
/// PostgreSQL request to close a prepared statement or portal.
5+
final class CloseRequest: PostgresRequest {
6+
7+
/// Name of the prepared statement or portal to close.
8+
let name: String
9+
10+
/// Close
11+
let target: PostgresMessage.Close.Target
12+
13+
init(name: String, closeType: PostgresMessage.Close.Target) {
14+
self.name = name
15+
self.target = closeType
16+
}
17+
18+
func respond(to message: PostgresMessage) throws -> [PostgresMessage]? {
19+
if message.identifier != .closeComplete {
20+
fatalError("Unexpected PostgreSQL message \(message)")
21+
}
22+
return nil
23+
}
24+
25+
func start() throws -> [PostgresMessage] {
26+
let close = try PostgresMessage.Close(target: target, name: name).message()
27+
let sync = try PostgresMessage.Sync().message()
28+
return [close, sync]
29+
}
30+
31+
func log(to logger: Logger) {
32+
logger.debug("Requesting Close of \(name)")
33+
}
34+
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import Foundation
2+
3+
extension PostgresDatabase {
4+
public func prepare(query: String) -> EventLoopFuture<PreparedQuery> {
5+
let name = "nio-postgres-\(UUID().uuidString)"
6+
let prepare = PrepareQueryRequest(query, as: name)
7+
return self.send(prepare, logger: self.logger).map { () -> (PreparedQuery) in
8+
let prepared = PreparedQuery(database: self, name: name, rowDescription: prepare.rowLookupTable!)
9+
return prepared
10+
}
11+
}
12+
13+
public func prepare(query: String, handler: @escaping (PreparedQuery) -> EventLoopFuture<[[PostgresRow]]>) -> EventLoopFuture<[[PostgresRow]]> {
14+
prepare(query: query)
15+
.flatMap { preparedQuery in
16+
handler(preparedQuery)
17+
.flatMap { results in
18+
preparedQuery.deallocate().map { results }
19+
}
20+
}
21+
}
22+
}
23+
24+
25+
public struct PreparedQuery {
26+
let database: PostgresDatabase
27+
let name: String
28+
let rowLookupTable: PostgresRow.LookupTable
29+
30+
init(database: PostgresDatabase, name: String, rowDescription: PostgresRow.LookupTable) {
31+
self.database = database
32+
self.name = name
33+
self.rowLookupTable = rowDescription
34+
}
35+
36+
public func execute(_ binds: [PostgresData] = []) -> EventLoopFuture<[PostgresRow]> {
37+
var rows: [PostgresRow] = []
38+
return self.execute(binds) { rows.append($0) }.map { rows }
39+
}
40+
41+
public func execute(_ binds: [PostgresData] = [], _ onRow: @escaping (PostgresRow) throws -> ()) -> EventLoopFuture<Void> {
42+
let handler = ExecutePreparedQuery(query: self, binds: binds, onRow: onRow)
43+
return database.send(handler, logger: database.logger)
44+
}
45+
46+
public func deallocate() -> EventLoopFuture<Void> {
47+
database.send(CloseRequest(name: self.name,
48+
closeType: .preparedStatement),
49+
logger:database.logger)
50+
51+
}
52+
}
53+
54+
55+
private final class PrepareQueryRequest: PostgresRequest {
56+
let query: String
57+
let name: String
58+
var rowLookupTable: PostgresRow.LookupTable?
59+
var resultFormatCodes: [PostgresFormatCode]
60+
var logger: Logger?
61+
62+
init(_ query: String, as name: String) {
63+
self.query = query
64+
self.name = name
65+
self.resultFormatCodes = [.binary]
66+
}
67+
68+
func respond(to message: PostgresMessage) throws -> [PostgresMessage]? {
69+
switch message.identifier {
70+
case .rowDescription:
71+
let row = try PostgresMessage.RowDescription(message: message)
72+
self.rowLookupTable = PostgresRow.LookupTable(
73+
rowDescription: row,
74+
resultFormat: self.resultFormatCodes
75+
)
76+
return []
77+
case .parseComplete, .parameterDescription:
78+
return []
79+
case .readyForQuery:
80+
return nil
81+
default:
82+
fatalError("Unexpected message: \(message)")
83+
}
84+
85+
}
86+
87+
func start() throws -> [PostgresMessage] {
88+
let parse = PostgresMessage.Parse(
89+
statementName: self.name,
90+
query: self.query,
91+
parameterTypes: []
92+
)
93+
let describe = PostgresMessage.Describe(
94+
command: .statement,
95+
name: self.name
96+
)
97+
return try [parse.message(), describe.message(), PostgresMessage.Sync().message()]
98+
}
99+
100+
101+
func log(to logger: Logger) {
102+
self.logger = logger
103+
logger.debug("\(self.query) prepared as \(self.name)")
104+
}
105+
}
106+
107+
108+
private final class ExecutePreparedQuery: PostgresRequest {
109+
let query: PreparedQuery
110+
let binds: [PostgresData]
111+
var onRow: (PostgresRow) throws -> ()
112+
var resultFormatCodes: [PostgresFormatCode]
113+
var logger: Logger?
114+
115+
init(query: PreparedQuery, binds: [PostgresData], onRow: @escaping (PostgresRow) throws -> ()) {
116+
self.query = query
117+
self.binds = binds
118+
self.onRow = onRow
119+
self.resultFormatCodes = [.binary]
120+
}
121+
122+
func respond(to message: PostgresMessage) throws -> [PostgresMessage]? {
123+
switch message.identifier {
124+
case .bindComplete:
125+
return []
126+
case .dataRow:
127+
let data = try PostgresMessage.DataRow(message: message)
128+
let row = PostgresRow(dataRow: data, lookupTable: query.rowLookupTable)
129+
try onRow(row)
130+
return []
131+
case .noData:
132+
return []
133+
case .commandComplete:
134+
return []
135+
case .readyForQuery:
136+
return nil
137+
default: throw PostgresError.protocol("Unexpected message during query: \(message)")
138+
}
139+
}
140+
141+
func start() throws -> [PostgresMessage] {
142+
143+
let bind = PostgresMessage.Bind(
144+
portalName: "",
145+
statementName: query.name,
146+
parameterFormatCodes: self.binds.map { $0.formatCode },
147+
parameters: self.binds.map { .init(value: $0.value) },
148+
resultFormatCodes: self.resultFormatCodes
149+
)
150+
let execute = PostgresMessage.Execute(
151+
portalName: "",
152+
maxRows: 0
153+
)
154+
155+
let sync = PostgresMessage.Sync()
156+
return try [bind.message(), execute.message(), sync.message()]
157+
}
158+
159+
func log(to logger: Logger) {
160+
self.logger = logger
161+
logger.debug("Execute Prepared Query: \(query.name)")
162+
}
163+
164+
}

Sources/PostgresNIO/Data/PostgresData+Array.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ extension PostgresData {
6262
}
6363

6464
public var array: [PostgresData]? {
65+
guard case .binary = self.formatCode else {
66+
return nil
67+
}
6568
guard var value = self.value else {
6669
return nil
6770
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import NIO
2+
3+
extension PostgresMessage {
4+
/// Identifies the message as a Close Command
5+
public struct Close: PostgresMessageType {
6+
public static var identifier: PostgresMessage.Identifier {
7+
return .close
8+
}
9+
10+
/// Close Target. Determines if the Close command should close a prepared statement
11+
/// or portal.
12+
public enum Target: Int8 {
13+
case preparedStatement = 0x53 // 'S' - prepared statement
14+
case portal = 0x50 // 'P' - portal
15+
}
16+
17+
/// Determines if the `name` identifes a portal or a prepared statement
18+
public var target: Target
19+
20+
/// The name of the prepared statement or portal to describe
21+
/// (an empty string selects the unnamed prepared statement or portal).
22+
public var name: String
23+
24+
25+
/// See `CustomStringConvertible`.
26+
public var description: String {
27+
switch target {
28+
case .preparedStatement: return "Statement(\(name))"
29+
case .portal: return "Portal(\(name))"
30+
}
31+
}
32+
33+
/// Serializes this message into a byte buffer.
34+
public func serialize(into buffer: inout ByteBuffer) throws {
35+
buffer.writeInteger(target.rawValue)
36+
buffer.write(nullTerminated: name)
37+
}
38+
}
39+
}

Sources/PostgresNIO/Message/PostgresMessage+DataRow.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,13 @@ extension PostgresMessage {
2727
/// Parses an instance of this message type from a byte buffer.
2828
public static func parse(from buffer: inout ByteBuffer) throws -> DataRow {
2929
guard let columns = buffer.read(array: Column.self, { buffer in
30-
return .init(value: buffer.readNullableBytes())
30+
if var slice = buffer.readNullableBytes() {
31+
var copy = ByteBufferAllocator().buffer(capacity: slice.readableBytes)
32+
copy.writeBuffer(&slice)
33+
return .init(value: copy)
34+
} else {
35+
return .init(value: nil)
36+
}
3137
}) else {
3238
throw PostgresError.protocol("Could not parse data row columns")
3339
}

Tests/PostgresNIOTests/PostgresNIOTests.swift

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,6 +818,38 @@ final class PostgresNIOTests: XCTestCase {
818818
))
819819
}
820820

821+
func testPreparedQuery() throws {
822+
let conn = try PostgresConnection.test(on: eventLoop).wait()
823+
824+
defer { try! conn.close().wait() }
825+
let prepared = try conn.prepare(query: "SELECT 1 as one;").wait()
826+
let rows = try prepared.execute().wait()
827+
828+
829+
XCTAssertEqual(rows.count, 1)
830+
let value = rows[0].column("one")
831+
XCTAssertEqual(value?.int, 1)
832+
}
833+
834+
func testPrepareQueryClosure() throws {
835+
let conn = try PostgresConnection.test(on: eventLoop).wait()
836+
837+
defer { try! conn.close().wait() }
838+
let x = conn.prepare(query: "SELECT $1::text as foo;", handler: { query in
839+
let a = query.execute(["a"])
840+
let b = query.execute(["b"])
841+
let c = query.execute(["c"])
842+
return EventLoopFuture.whenAllSucceed([a, b, c], on: conn.eventLoop)
843+
844+
})
845+
let rows = try x.wait()
846+
XCTAssertEqual(rows.count, 3)
847+
XCTAssertEqual(rows[0][0].column("foo")?.string, "a")
848+
XCTAssertEqual(rows[1][0].column("foo")?.string, "b")
849+
XCTAssertEqual(rows[2][0].column("foo")?.string, "c")
850+
}
851+
852+
821853
// https://github.com/vapor/postgres-nio/issues/71
822854
func testChar1Serialization() throws {
823855
let conn = try PostgresConnection.test(on: eventLoop).wait()
@@ -872,6 +904,7 @@ final class PostgresNIOTests: XCTestCase {
872904
XCTAssertEqual(rows.metadata.oid, nil)
873905
XCTAssertEqual(rows.metadata.rows, 0)
874906
}
907+
875908
}
876909

877910
let isLoggingConfigured: Bool = {

0 commit comments

Comments
 (0)