Skip to content

Commit dfc03db

Browse files
authored
Merge pull request vapor#52 from vapor/dbkit-gm
dbkit 1.0.0 gm
2 parents 3883464 + 3fb6e2f commit dfc03db

12 files changed

+292
-38
lines changed

Package.swift

+8-5
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,25 @@ let package = Package(
88
],
99
dependencies: [
1010
// 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging.
11-
.package(url: "https://github.com/vapor/core.git", from: "3.0.0-rc.2"),
11+
.package(url: "https://github.com/vapor/core.git", from: "3.0.0"),
1212

1313
// 🔑 Hashing (BCrypt, SHA, HMAC, etc), encryption, and randomness.
14-
.package(url: "https://github.com/vapor/crypto.git", from: "3.0.0-rc.2"),
14+
.package(url: "https://github.com/vapor/crypto.git", from: "3.0.0"),
1515

1616
// 🗄 Core services for creating database integrations.
17-
.package(url: "https://github.com/vapor/database-kit.git", from: "1.0.0-rc.2"),
17+
.package(url: "https://github.com/vapor/database-kit.git", from: "1.0.0"),
1818

1919
// 📦 Dependency injection / inversion of control framework.
20-
.package(url: "https://github.com/vapor/service.git", from: "1.0.0-rc.2"),
20+
.package(url: "https://github.com/vapor/service.git", from: "1.0.0"),
21+
22+
// *️⃣ Build SQL queries in Swift.
23+
.package(url: "https://github.com/vapor/sql.git", from: "1.0.0"),
2124

2225
// Event-driven network application framework for high performance protocol servers & clients, non-blocking.
2326
.package(url: "https://github.com/apple/swift-nio.git", from: "1.0.0"),
2427
],
2528
targets: [
26-
.target(name: "PostgreSQL", dependencies: ["Async", "Bits", "Core", "Crypto", "DatabaseKit", "NIO", "Service"]),
29+
.target(name: "PostgreSQL", dependencies: ["Async", "Bits", "Core", "Crypto", "DatabaseKit", "NIO", "Service", "SQL"]),
2730
.testTarget(name: "PostgreSQLTests", dependencies: ["Core", "PostgreSQL"]),
2831
]
2932
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import Async
2+
3+
4+
extension PostgreSQLConnection {
5+
/// Note: after calling `listen'` on a connection, it can no longer handle other database operations. Do not try to send other SQL commands through this connection afterwards.
6+
/// IAlso, notifications will only be sent for as long as this connection remains open; you are responsible for opening a new connection to listen on when this one closes.
7+
internal func listen(_ channelName: String, handler: @escaping (String) throws -> ()) throws -> Future<Void> {
8+
closeHandlers.append({ conn in
9+
let query = PostgreSQLQuery(query: "UNLISTEN \"\(channelName)\";")
10+
return conn.send([.query(query)], onResponse: { _ in })
11+
})
12+
13+
notificationHandlers[channelName] = { message in
14+
try handler(message)
15+
}
16+
let query = PostgreSQLQuery(query: "LISTEN \"\(channelName)\";")
17+
return queue.enqueue([.query(query)], onInput: { message in
18+
switch message {
19+
case let .notificationResponse(notification):
20+
try self.notificationHandlers[notification.channel]?(notification.message)
21+
default:
22+
break
23+
}
24+
return false
25+
})
26+
}
27+
28+
internal func notify(_ channelName: String, message: String) throws -> Future<Void> {
29+
let query = PostgreSQLQuery(query: "NOTIFY \"\(channelName)\", '\(message)';")
30+
return send([.query(query)]).map(to: Void.self, { _ in })
31+
}
32+
33+
internal func unlisten(_ channelName: String, unlistenHandler: (() -> Void)? = nil) throws -> Future<Void> {
34+
notificationHandlers.removeValue(forKey: channelName)
35+
let query = PostgreSQLQuery(query: "UNLISTEN \"\(channelName)\";")
36+
return send([.query(query)], onResponse: { _ in unlistenHandler?() })
37+
}
38+
}

Sources/PostgreSQL/Connection/PostgreSQLConnection+Query.swift

+20-2
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ extension PostgreSQLConnection {
55
public func query(
66
_ string: String,
77
_ parameters: [PostgreSQLDataConvertible] = []
8-
) throws -> Future<[[PostgreSQLColumn: PostgreSQLData]]> {
8+
) -> Future<[[PostgreSQLColumn: PostgreSQLData]]> {
99
var rows: [[PostgreSQLColumn: PostgreSQLData]] = []
10-
return try query(string, parameters) { row in
10+
return query(string, parameters) { row in
1111
rows.append(row)
1212
}.map(to: [[PostgreSQLColumn: PostgreSQLData]].self) {
1313
return rows
@@ -21,8 +21,26 @@ extension PostgreSQLConnection {
2121
_ parameters: [PostgreSQLDataConvertible] = [],
2222
resultFormat: PostgreSQLResultFormat = .binary(),
2323
onRow: @escaping ([PostgreSQLColumn: PostgreSQLData]) throws -> ()
24+
) -> Future<Void> {
25+
return operation {
26+
do {
27+
return try self._query(string, parameters, resultFormat: resultFormat, onRow: onRow)
28+
} catch {
29+
return self.eventLoop.newFailedFuture(error: error)
30+
}
31+
}
32+
}
33+
34+
/// Non-operation bounded query.
35+
private func _query(
36+
_ string: String,
37+
_ parameters: [PostgreSQLDataConvertible] = [],
38+
resultFormat: PostgreSQLResultFormat = .binary(),
39+
onRow: @escaping ([PostgreSQLColumn: PostgreSQLData]) throws -> ()
2440
) throws -> Future<Void> {
2541
let parameters = try parameters.map { try $0.convertToPostgreSQLData() }
42+
logger?.record(query: string, values: parameters.map { $0.description })
43+
2644
let parse = PostgreSQLParseRequest(
2745
statementName: "",
2846
query: string,

Sources/PostgreSQL/Connection/PostgreSQLConnection+SimpleQuery.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ extension PostgreSQLConnection {
1010
return rows
1111
}
1212
}
13-
1413
/// Sends a simple PostgreSQL query command, returning the parsed results to
1514
/// the supplied closure.
1615
public func simpleQuery(_ string: String, onRow: @escaping ([PostgreSQLColumn: PostgreSQLData]) -> ()) -> Future<Void> {
17-
logger?.log(query: string, parameters: [])
16+
return operation { self._simpleQuery(string, onRow: onRow) }
17+
}
18+
19+
/// Non-operation bounded simple query.
20+
private func _simpleQuery(_ string: String, onRow: @escaping ([PostgreSQLColumn: PostgreSQLData]) -> ()) -> Future<Void> {
21+
logger?.record(query: string)
1822
var currentRow: PostgreSQLRowDescription?
1923
let query = PostgreSQLQuery(query: string)
2024
return send([.query(query)]) { message in

Sources/PostgreSQL/Connection/PostgreSQLConnection.swift

+97-17
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
1-
import Async
21
import Crypto
32
import NIO
43

54
/// A PostgreSQL frontend client.
6-
public final class PostgreSQLConnection {
5+
public final class PostgreSQLConnection: DatabaseConnection, BasicWorker {
6+
/// See `BasicWorker`.
77
public var eventLoop: EventLoop {
88
return channel.eventLoop
99
}
1010

1111
/// Handles enqueued redis commands and responses.
12-
private let queue: QueueHandler<PostgreSQLMessage, PostgreSQLMessage>
12+
internal let queue: QueueHandler<PostgreSQLMessage, PostgreSQLMessage>
1313

1414
/// The channel
1515
private let channel: Channel
1616

1717
/// If non-nil, will log queries.
18-
public var logger: PostgreSQLLogger?
18+
public var logger: DatabaseLogger?
19+
20+
/// See `DatabaseConnection`.
21+
public var isClosed: Bool
22+
23+
/// See `Extendable`.
24+
public var extend: Extend
1925

2026
/// Returns a new unique portal name.
2127
internal var nextPortalName: String {
@@ -32,21 +38,63 @@ public final class PostgreSQLConnection {
3238
/// A unique identifier for this connection, used to generate statment and portal names
3339
private var uniqueNameCounter: UInt8
3440

41+
/// In-flight `send(...)` futures.
42+
private var currentSend: Promise<Void>?
43+
44+
/// The current query running, if one exists.
45+
private var pipeline: Future<Void>
46+
47+
/// Block type to be called on close of connection
48+
internal typealias CloseHandler = ((PostgreSQLConnection) -> Future<Void>)
49+
/// Called on close of the connection
50+
internal var closeHandlers = [CloseHandler]()
51+
/// Handler type for Notifications
52+
internal typealias NotificationHandler = (String) throws -> Void
53+
/// Handlers to be stored by channel name
54+
internal var notificationHandlers: [String: NotificationHandler] = [:]
55+
3556
/// Creates a new Redis client on the provided data source and sink.
3657
init(queue: QueueHandler<PostgreSQLMessage, PostgreSQLMessage>, channel: Channel) {
3758
self.queue = queue
3859
self.channel = channel
3960
self.uniqueNameCounter = 0
61+
self.isClosed = false
62+
self.extend = [:]
63+
self.pipeline = channel.eventLoop.newSucceededFuture(result: ())
64+
channel.closeFuture.always {
65+
self.isClosed = true
66+
if let current = self.currentSend {
67+
current.fail(error: closeError)
68+
}
69+
}
4070
}
41-
42-
deinit {
43-
close()
71+
/// Sends `PostgreSQLMessage` to the server.
72+
func send(_ message: [PostgreSQLMessage]) -> Future<[PostgreSQLMessage]> {
73+
var responses: [PostgreSQLMessage] = []
74+
return send(message) { response in
75+
responses.append(response)
76+
}.map(to: [PostgreSQLMessage].self) {
77+
return responses
78+
}
4479
}
4580

4681
/// Sends `PostgreSQLMessage` to the server.
4782
func send(_ messages: [PostgreSQLMessage], onResponse: @escaping (PostgreSQLMessage) throws -> ()) -> Future<Void> {
83+
// if currentSend is not nil, previous send has not completed
84+
assert(currentSend == nil, "Attempting to call `send(...)` again before previous invocation has completed.")
85+
86+
// ensure the connection is not closed
87+
guard !isClosed else {
88+
return eventLoop.newFailedFuture(error: closeError)
89+
}
90+
91+
// create a new promise and store it
92+
let promise = eventLoop.newPromise(Void.self)
93+
currentSend = promise
94+
95+
// cascade this enqueue to the newly created promise
4896
var error: Error?
49-
return queue.enqueue(messages) { message in
97+
queue.enqueue(messages) { message in
5098
switch message {
5199
case .readyForQuery:
52100
if let e = error { throw e }
@@ -56,17 +104,28 @@ public final class PostgreSQLConnection {
56104
default: try onResponse(message)
57105
}
58106
return false // request until ready for query
59-
}
107+
}.cascade(promise: promise)
108+
109+
// when the promise completes, remove the reference to it
110+
promise.futureResult.always { self.currentSend = nil }
111+
112+
// return the promise's future result (same as `queue.enqueue`)
113+
return promise.futureResult
60114
}
61115

62-
/// Sends `PostgreSQLMessage` to the server.
63-
func send(_ message: [PostgreSQLMessage]) -> Future<[PostgreSQLMessage]> {
64-
var responses: [PostgreSQLMessage] = []
65-
return send(message) { response in
66-
responses.append(response)
67-
}.map(to: [PostgreSQLMessage].self) {
68-
return responses
116+
/// Submits an async task to be pipelined.
117+
internal func operation(_ work: @escaping () -> Future<Void>) -> Future<Void> {
118+
/// perform this work when the current pipeline future is completed
119+
let new = pipeline.then(work)
120+
121+
/// append this work to the pipeline, discarding errors as the pipeline
122+
//// does not care about them
123+
pipeline = new.catchMap { err in
124+
return ()
69125
}
126+
127+
/// return the newly enqueued work's future result
128+
return new
70129
}
71130

72131
/// Authenticates the `PostgreSQLClient` using a username with no password.
@@ -134,8 +193,29 @@ public final class PostgreSQLConnection {
134193
}
135194
}
136195

196+
137197
/// Closes this client.
138198
public func close() {
139-
channel.close(promise: nil)
199+
_ = executeCloseHandlersThenClose()
140200
}
201+
202+
203+
private func executeCloseHandlersThenClose() -> Future<Void> {
204+
if let beforeClose = closeHandlers.popLast() {
205+
return beforeClose(self).then { _ in
206+
self.executeCloseHandlersThenClose()
207+
}
208+
} else {
209+
return channel.close(mode: .all)
210+
}
211+
}
212+
213+
214+
/// Called when this class deinitializes.
215+
deinit {
216+
close()
217+
}
218+
141219
}
220+
221+
private let closeError = PostgreSQLError(identifier: "closed", reason: "Connection is closed.", source: .capture())

Sources/PostgreSQL/Database/PostgreSQLDatabase.swift

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,27 @@
11
import Async
22

33
/// Creates connections to an identified PostgreSQL database.
4-
public final class PostgreSQLDatabase: Database {
4+
public final class PostgreSQLDatabase: Database, LogSupporting {
5+
/// See `LogSupporting`
6+
public static func enableLogging(_ logger: DatabaseLogger, on conn: PostgreSQLConnection) {
7+
conn.logger = logger
8+
}
9+
510
/// This database's configuration.
611
public let config: PostgreSQLDatabaseConfig
712

8-
/// If non-nil, will log queries.
9-
public var logger: PostgreSQLLogger?
10-
1113
/// Creates a new `PostgreSQLDatabase`.
1214
public init(config: PostgreSQLDatabaseConfig) {
1315
self.config = config
1416
}
1517

1618
/// See `Database.makeConnection()`
17-
public func makeConnection(on worker: Worker) -> Future<PostgreSQLConnection> {
19+
public func newConnection(on worker: Worker) -> Future<PostgreSQLConnection> {
1820
let config = self.config
1921
return Future.flatMap(on: worker) {
2022
return try PostgreSQLConnection.connect(hostname: config.hostname, port: config.port, on: worker) { error in
2123
print("[PostgreSQL] \(error)")
2224
}.flatMap(to: PostgreSQLConnection.self) { client in
23-
client.logger = self.logger
2425
return client.authenticate(
2526
username: config.username,
2627
database: config.database,
@@ -31,9 +32,6 @@ public final class PostgreSQLDatabase: Database {
3132
}
3233
}
3334

34-
/// A connection created by a `PostgreSQLDatabase`.
35-
extension PostgreSQLConnection: DatabaseConnection, BasicWorker { }
36-
3735
extension DatabaseIdentifier {
3836
/// Default identifier for `PostgreSQLDatabase`.
3937
public static var psql: DatabaseIdentifier<PostgreSQLDatabase> {

Sources/PostgreSQL/Message+Parse/PostgreSQLMessageDecoder.swift

+1
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ final class PostgreSQLMessageDecoder: ByteToMessageDecoder {
4949
let decoder = _PostgreSQLMessageDecoder(data: messageData)
5050
let message: PostgreSQLMessage
5151
switch messageType {
52+
case .A: message = try .notificationResponse(decoder.decode())
5253
case .E: message = try .error(decoder.decode())
5354
case .N: message = try .notice(decoder.decode())
5455
case .R: message = try .authenticationRequest(decoder.decode())

Sources/PostgreSQL/Message/PostgreSQLMessage.swift

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ enum PostgreSQLMessage {
77
case error(PostgreSQLDiagnosticResponse)
88
/// Identifies the message as a notice.
99
case notice(PostgreSQLDiagnosticResponse)
10+
/// Identifies the message as a notification response.
11+
case notificationResponse(PostgreSQLNotificationResponse)
1012
/// One of the various authentication request message formats.
1113
case authenticationRequest(PostgreSQLAuthenticationRequest)
1214
/// Identifies the message as a password response.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
struct PostgreSQLNotificationResponse: Decodable {
4+
/// The message coming from PSQL
5+
let channel: String
6+
let message: String
7+
init(from decoder: Decoder) throws {
8+
let container = try decoder.singleValueContainer()
9+
_ = try container.decode(Int32.self)
10+
channel = try container.decode(String.self)
11+
message = try container.decode(String.self)
12+
}
13+
}

Sources/PostgreSQL/PostgreSQLProvider.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public final class PostgreSQLProvider: Provider {
1414
try services.register(DatabaseKitProvider())
1515
services.register(PostgreSQLDatabaseConfig.self)
1616
services.register(PostgreSQLDatabase.self)
17-
var databases = DatabaseConfig()
17+
var databases = DatabasesConfig()
1818
databases.add(database: PostgreSQLDatabase.self, as: .psql)
1919
services.register(databases)
2020
}

0 commit comments

Comments
 (0)