Skip to content

Commit 25a0435

Browse files
authored
Merge pull request vapor#1 from vapor/beta
psql beta
2 parents faadd6d + bdb1eaf commit 25a0435

File tree

56 files changed

+3168
-3
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+3168
-3
lines changed

.gitignore

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
.DS_Store
2+
/.build
3+
/Packages
4+
/*.xcodeproj
5+
Package.resolved
6+

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2018 Qutheory, LLC
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

Package.swift

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// swift-tools-version:4.0
2+
import PackageDescription
3+
4+
let package = Package(
5+
name: "PostgreSQL",
6+
products: [
7+
.library(name: "PostgreSQL", targets: ["PostgreSQL"]),
8+
],
9+
dependencies: [
10+
// ⏱ Promises and reactive-streams in Swift built for high-performance and scalability.
11+
.package(url: "https://github.com/vapor/async.git", from: "1.0.0-rc"),
12+
13+
// 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging.
14+
.package(url: "https://github.com/vapor/core.git", from: "3.0.0-rc"),
15+
16+
// 🔑 Hashing (BCrypt, SHA, HMAC, etc), encryption, and randomness.
17+
.package(url: "https://github.com/vapor/crypto.git", from: "3.0.0-rc"),
18+
19+
// 🗄 Core services for creating database integrations.
20+
.package(url: "https://github.com/vapor/database-kit.git", from: "1.0.0-rc"),
21+
22+
// 📦 Dependency injection / inversion of control framework.
23+
.package(url: "https://github.com/vapor/service.git", from: "1.0.0-rc"),
24+
25+
// 🔌 Non-blocking TCP socket layer, with event-driven server and client.
26+
.package(url: "https://github.com/vapor/sockets.git", from: "3.0.0-rc"),
27+
],
28+
targets: [
29+
.target(name: "PostgreSQL", dependencies: ["Async", "Bits", "Crypto", "DatabaseKit", "Service", "TCP"]),
30+
.testTarget(name: "PostgreSQLTests", dependencies: ["PostgreSQL"]),
31+
]
32+
)

README.md

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
1-
(See [vapor-community/postgresql](https://github.com/vapor-community/postgresql/) for libpq-based version)
1+
<p align="center">
2+
<img src="https://user-images.githubusercontent.com/1342803/36623751-7f1f2884-18d5-11e8-9fd8-5a94c23ec7ce.png" height="64" alt="PostgreSQL">
3+
<br>
4+
<br>
5+
<a href="http://docs.vapor.codes/3.0/">
6+
<img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation">
7+
</a>
8+
<a href="http://vapor.team">
9+
<img src="http://vapor.team/badge.svg" alt="Slack Team">
10+
</a>
11+
<a href="LICENSE">
12+
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
13+
</a>
14+
<a href="https://circleci.com/gh/vapor/postgresql">
15+
<img src="https://circleci.com/gh/vapor/postgresql.svg?style=shield" alt="Continuous Integration">
16+
</a>
17+
<a href="https://swift.org">
18+
<img src="http://img.shields.io/badge/swift-4.1-brightgreen.svg" alt="Swift 4.1">
19+
</a>
20+
</p>
221

3-
# Pure-Swift PostgreSQL Library
22+
<hr>
423

5-
Work in progress
24+
See [vapor-community/postgresql](https://github.com/vapor-community/postgresql/) for `libpq` based version.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import Async
2+
3+
extension PostgreSQLConnection {
4+
/// Sends a parameterized PostgreSQL query command, collecting the parsed results.
5+
public func query(
6+
_ string: String,
7+
_ parameters: [PostgreSQLDataCustomConvertible] = []
8+
) throws -> Future<[[String: PostgreSQLData]]> {
9+
var rows: [[String: PostgreSQLData]] = []
10+
return try query(string, parameters) { row in
11+
rows.append(row)
12+
}.map(to: [[String: PostgreSQLData]].self) {
13+
return rows
14+
}
15+
}
16+
17+
/// Sends a parameterized PostgreSQL query command, returning the parsed results to
18+
/// the supplied closure.
19+
public func query(
20+
_ string: String,
21+
_ parameters: [PostgreSQLDataCustomConvertible] = [],
22+
resultFormat: PostgreSQLResultFormat = .binary(),
23+
onRow: @escaping ([String: PostgreSQLData]) -> ()
24+
) throws -> Future<Void> {
25+
let parameters = try parameters.map { try $0.convertToPostgreSQLData() }
26+
logger?.log(query: string, parameters: parameters)
27+
let parse = PostgreSQLParseRequest(
28+
statementName: "",
29+
query: string,
30+
parameterTypes: parameters.map { $0.type }
31+
)
32+
let describe = PostgreSQLDescribeRequest(type: .statement, name: "")
33+
var currentRow: PostgreSQLRowDescription?
34+
35+
return send([
36+
.parse(parse), .describe(describe), .sync
37+
]) { message in
38+
switch message {
39+
case .parseComplete: break
40+
case .rowDescription(let row): currentRow = row
41+
case .parameterDescription: break
42+
case .noData: break
43+
default: throw PostgreSQLError(identifier: "query", reason: "Unexpected message during PostgreSQLParseRequest: \(message)", source: .capture())
44+
}
45+
}.flatMap(to: Void.self) {
46+
let resultFormats = resultFormat.formatCodeFactory(currentRow?.fields.map { $0.dataType } ?? [])
47+
// cache so we don't compute twice
48+
let bind = PostgreSQLBindRequest(
49+
portalName: "",
50+
statementName: "",
51+
parameterFormatCodes: parameters.map { $0.format },
52+
parameters: parameters.map { .init(data: $0.data) },
53+
resultFormatCodes: resultFormats
54+
)
55+
let execute = PostgreSQLExecuteRequest(
56+
portalName: "",
57+
maxRows: 0
58+
)
59+
return self.send([
60+
.bind(bind), .execute(execute), .sync
61+
]) { message in
62+
switch message {
63+
case .bindComplete: break
64+
case .dataRow(let data):
65+
guard let row = currentRow else {
66+
throw PostgreSQLError(identifier: "query", reason: "Unexpected PostgreSQLDataRow without preceding PostgreSQLRowDescription.", source: .capture())
67+
}
68+
let parsed = try row.parse(data: data, formatCodes: resultFormats)
69+
onRow(parsed)
70+
case .close: break
71+
case .noData: break
72+
default: throw PostgreSQLError(identifier: "query", reason: "Unexpected message during PostgreSQLParseRequest: \(message)", source: .capture())
73+
}
74+
}
75+
}
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import Async
2+
3+
extension PostgreSQLConnection {
4+
/// Sends a simple PostgreSQL query command, collecting the parsed results.
5+
public func simpleQuery(_ string: String) -> Future<[[String: PostgreSQLData]]> {
6+
var rows: [[String: PostgreSQLData]] = []
7+
return simpleQuery(string) { row in
8+
rows.append(row)
9+
}.map(to: [[String: PostgreSQLData]].self) {
10+
return rows
11+
}
12+
}
13+
14+
/// Sends a simple PostgreSQL query command, returning the parsed results to
15+
/// the supplied closure.
16+
public func simpleQuery(_ string: String, onRow: @escaping ([String: PostgreSQLData]) -> ()) -> Future<Void> {
17+
logger?.log(query: string, parameters: [])
18+
var currentRow: PostgreSQLRowDescription?
19+
let query = PostgreSQLQuery(query: string)
20+
return send([.query(query)]) { message in
21+
switch message {
22+
case .rowDescription(let row):
23+
currentRow = row
24+
case .dataRow(let data):
25+
guard let row = currentRow else {
26+
throw PostgreSQLError(identifier: "simpleQuery", reason: "Unexpected PostgreSQLDataRow without preceding PostgreSQLRowDescription.", source: .capture())
27+
}
28+
let parsed = try row.parse(data: data, formatCodes: row.fields.map { $0.formatCode })
29+
onRow(parsed)
30+
case .close: break // query over, waiting for `readyForQuery`
31+
default: throw PostgreSQLError(identifier: "simpleQuery", reason: "Unexpected message during PostgreSQLQuery: \(message)", source: .capture())
32+
}
33+
}
34+
}
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import Async
2+
import TCP
3+
4+
extension PostgreSQLConnection {
5+
/// Connects to a Redis server using a TCP socket.
6+
public static func connect(
7+
hostname: String = "localhost",
8+
port: UInt16 = 5432,
9+
on worker: Worker,
10+
onError: @escaping TCPSocketSink.ErrorHandler
11+
) throws -> PostgreSQLConnection {
12+
let socket = try TCPSocket(isNonBlocking: true)
13+
let client = try TCPClient(socket: socket)
14+
try client.connect(hostname: hostname, port: port)
15+
let stream = socket.stream(on: worker, onError: onError)
16+
return PostgreSQLConnection(stream: stream, on: worker)
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import Async
2+
import Crypto
3+
4+
/// A PostgreSQL frontend client.
5+
public final class PostgreSQLConnection {
6+
/// Handles enqueued redis commands and responses.
7+
private let queueStream: QueueStream<PostgreSQLMessage, PostgreSQLMessage>
8+
9+
/// If non-nil, will log queries.
10+
public var logger: PostgreSQLLogger?
11+
12+
/// Creates a new Redis client on the provided data source and sink.
13+
init<Stream>(stream: Stream, on worker: Worker) where Stream: ByteStream {
14+
let queueStream = QueueStream<PostgreSQLMessage, PostgreSQLMessage>()
15+
16+
let serializerStream = PostgreSQLMessageSerializer().stream(on: worker)
17+
let parserStream = PostgreSQLMessageParser().stream(on: worker)
18+
19+
stream.stream(to: parserStream)
20+
.stream(to: queueStream)
21+
.stream(to: serializerStream)
22+
.output(to: stream)
23+
24+
self.queueStream = queueStream
25+
}
26+
27+
/// Sends `PostgreSQLMessage` to the server.
28+
func send(_ messages: [PostgreSQLMessage], onResponse: @escaping (PostgreSQLMessage) throws -> ()) -> Future<Void> {
29+
var error: Error?
30+
return queueStream.enqueue(messages) { message in
31+
switch message {
32+
case .readyForQuery:
33+
if let e = error { throw e }
34+
return true
35+
case .error(let e): error = e
36+
case .notice(let n): print(n)
37+
default: try onResponse(message)
38+
}
39+
return false // request until ready for query
40+
}
41+
}
42+
43+
/// Sends `PostgreSQLMessage` to the server.
44+
func send(_ message: [PostgreSQLMessage]) -> Future<[PostgreSQLMessage]> {
45+
var responses: [PostgreSQLMessage] = []
46+
return send(message) { response in
47+
responses.append(response)
48+
}.map(to: [PostgreSQLMessage].self) {
49+
return responses
50+
}
51+
}
52+
53+
/// Authenticates the `PostgreSQLClient` using a username with no password.
54+
public func authenticate(username: String, database: String? = nil, password: String? = nil) -> Future<Void> {
55+
let startup = PostgreSQLStartupMessage.versionThree(parameters: [
56+
"user": username,
57+
"database": database ?? username
58+
])
59+
var authRequest: PostgreSQLAuthenticationRequest?
60+
return queueStream.enqueue([.startupMessage(startup)]) { message in
61+
switch message {
62+
case .authenticationRequest(let a):
63+
authRequest = a
64+
return true
65+
default: throw PostgreSQLError(identifier: "auth", reason: "Unsupported message encountered during auth: \(message).", source: .capture())
66+
}
67+
}.flatMap(to: Void.self) {
68+
guard let auth = authRequest else {
69+
throw PostgreSQLError(identifier: "authRequest", reason: "No authorization request / status sent.", source: .capture())
70+
}
71+
72+
let input: [PostgreSQLMessage]
73+
switch auth {
74+
case .ok:
75+
guard password == nil else {
76+
throw PostgreSQLError(identifier: "trust", reason: "No password is required", source: .capture())
77+
}
78+
input = []
79+
case .plaintext:
80+
guard let password = password else {
81+
throw PostgreSQLError(identifier: "password", reason: "Password is required", source: .capture())
82+
}
83+
let passwordMessage = PostgreSQLPasswordMessage(password: password)
84+
input = [.password(passwordMessage)]
85+
case .md5(let salt):
86+
guard let password = password else {
87+
throw PostgreSQLError(identifier: "password", reason: "Password is required", source: .capture())
88+
}
89+
guard let passwordData = password.data(using: .utf8) else {
90+
throw PostgreSQLError(identifier: "passwordUTF8", reason: "Could not convert password to UTF-8 encoded Data.", source: .capture())
91+
}
92+
93+
guard let usernameData = username.data(using: .utf8) else {
94+
throw PostgreSQLError(identifier: "usernameUTF8", reason: "Could not convert username to UTF-8 encoded Data.", source: .capture())
95+
}
96+
97+
let hasher = MD5()
98+
// pwdhash = md5(password + username).hexdigest()
99+
var passwordUsernameData = passwordData + usernameData
100+
hasher.update(sequence: &passwordUsernameData)
101+
hasher.finalize()
102+
guard let pwdhash = hasher.hash.hexString.data(using: .utf8) else {
103+
throw PostgreSQLError(identifier: "hashUTF8", reason: "Could not convert password hash to UTF-8 encoded Data.", source: .capture())
104+
}
105+
hasher.reset()
106+
// hash = ′ md 5′ + md 5(pwdhash + salt ).hexdigest ()
107+
var saltedData = pwdhash + salt
108+
hasher.update(sequence: &saltedData)
109+
hasher.finalize()
110+
let passwordMessage = PostgreSQLPasswordMessage(password: "md5" + hasher.hash.hexString)
111+
input = [.password(passwordMessage)]
112+
}
113+
114+
return self.queueStream.enqueue(input) { message in
115+
switch message {
116+
case .error(let error): throw error
117+
case .readyForQuery: return true
118+
case .authenticationRequest: return false
119+
case .parameterStatus, .backendKeyData: return false
120+
default: throw PostgreSQLError(identifier: "authenticationMessage", reason: "Unexpected authentication message: \(message)", source: .capture())
121+
}
122+
}
123+
}
124+
}
125+
126+
/// Closes this client.
127+
public func close() {
128+
queueStream.close()
129+
}
130+
}

0 commit comments

Comments
 (0)