Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
f5c8c49
psql beta
tanner0101 Jan 14, 2018
9144e0d
wip serializers
tanner0101 Jan 14, 2018
84f4881
working encoder
tanner0101 Jan 14, 2018
5280f31
psql authentication
tanner0101 Jan 14, 2018
071ad33
circleci config
tanner0101 Jan 14, 2018
1195591
test tmp
tanner0101 Jan 14, 2018
1b8df86
move tests back
tanner0101 Jan 14, 2018
ba95437
fix linux main
tanner0101 Jan 14, 2018
e32cc6c
comments
tanner0101 Jan 15, 2018
9094a43
tmp
tanner0101 Jan 15, 2018
4ee31c6
fix folder case
tanner0101 Jan 15, 2018
df067d4
! -> fatalError
tanner0101 Jan 15, 2018
926a3d4
async updates
tanner0101 Jan 15, 2018
7b8bd2b
macos add psql role
tanner0101 Jan 15, 2018
63db2dc
create w/ empty password
tanner0101 Jan 15, 2018
eccd3e8
temporarily skips macos tests
tanner0101 Jan 15, 2018
039842d
psql client + asymmetric stream
tanner0101 Jan 15, 2018
3506a3a
query + data
tanner0101 Jan 15, 2018
97dbc92
cleanup
tanner0101 Jan 15, 2018
b2d222a
additional type support
tanner0101 Jan 15, 2018
a308b07
format code cleanup
tanner0101 Jan 15, 2018
c0d831d
remove manual decode init
tanner0101 Jan 15, 2018
b76ffd5
keyed decoder
tanner0101 Jan 15, 2018
310e85c
parameterized query progress
tanner0101 Jan 16, 2018
f121eb1
execute request
tanner0101 Jan 16, 2018
7b4595c
parameterized queries
tanner0101 Jan 16, 2018
735b130
text format support for common types
tanner0101 Jan 17, 2018
69bf116
parse cleanup
tanner0101 Jan 17, 2018
58746f9
cleanup string/data helpers
tanner0101 Jan 17, 2018
c6d8ee8
parameterized types passing
tanner0101 Jan 17, 2018
b170158
cleanup + binary/text encoding
tanner0101 Jan 17, 2018
c171e1b
add point support
tanner0101 Jan 17, 2018
54234b0
linux compatiable hex parsing
tanner0101 Jan 17, 2018
7b3b23a
int64
tanner0101 Jan 17, 2018
859ff81
string numeric support
tanner0101 Jan 17, 2018
47e6b77
better diag types
tanner0101 Jan 17, 2018
9214941
keyed encoder
tanner0101 Jan 18, 2018
278630a
add data encoding tests
tanner0101 Jan 18, 2018
fa6cbdd
data decoder
tanner0101 Jan 18, 2018
9537e14
add decoder test
tanner0101 Jan 18, 2018
78ed32f
additional partial data convenience
tanner0101 Jan 18, 2018
875aef2
unkeyed encoder
tanner0101 Jan 18, 2018
856bdda
single value encoding container
tanner0101 Jan 18, 2018
575e5be
unkeyed decoding container
tanner0101 Jan 18, 2018
e1b9bd2
encoder/decoder passing tests
tanner0101 Jan 18, 2018
69dcbee
folder cleanup
tanner0101 Jan 18, 2018
e04a2e0
add parameterized encodable test
tanner0101 Jan 18, 2018
e2d2f86
better query method names
tanner0101 Jan 18, 2018
fdf0ee4
add databasekit
tanner0101 Jan 18, 2018
fb308be
remove some temporary hacks
tanner0101 Jan 18, 2018
bb03730
non-enum data type
tanner0101 Jan 18, 2018
bc9109a
add logger
tanner0101 Jan 18, 2018
01782ad
set logger from db
tanner0101 Jan 18, 2018
9cffb7e
support no params
tanner0101 Jan 18, 2018
74363e5
uuid support
tanner0101 Jan 18, 2018
17724bc
postgresql data custom codable
tanner0101 Jan 18, 2018
689dd74
parse dates as GMT
tanner0101 Jan 18, 2018
c686bd4
move to as! (unsafeBitCast not working well on linux
tanner0101 Jan 18, 2018
6709f03
small close fixes and asymetric stream fix
tanner0101 Jan 19, 2018
6136edd
improve diagnostic responses
tanner0101 Jan 19, 2018
9854689
new translating stream updates
tanner0101 Jan 19, 2018
2d8559e
fix readyForQuery after error
tanner0101 Jan 19, 2018
03b2b98
add init
tanner0101 Jan 19, 2018
1bdeb3f
basic jsonb support
tanner0101 Jan 19, 2018
8ae02ac
data refactor
tanner0101 Jan 19, 2018
a2fd67a
encoder fixes
tanner0101 Jan 19, 2018
171658c
psql data updates
tanner0101 Jan 19, 2018
54fcb8d
custom convertible types default
tanner0101 Jan 19, 2018
5f5feca
remove codable data helpers
tanner0101 Jan 19, 2018
a565178
add bool support
tanner0101 Jan 19, 2018
f2ead73
point string decode
tanner0101 Jan 19, 2018
d9640f9
point type
tanner0101 Jan 19, 2018
05ba3c2
test updates
tanner0101 Jan 19, 2018
d408f87
fix tests
tanner0101 Jan 19, 2018
aa77a21
comment numeric number parser
tanner0101 Jan 20, 2018
e4ba6a5
use extract method in decoder
tanner0101 Jan 20, 2018
086c03f
remove old parse/serialize code
tanner0101 Jan 20, 2018
7933bf8
add optional type
tanner0101 Jan 20, 2018
f744d13
remove static preferred types
tanner0101 Jan 20, 2018
6f2510e
rename JSONType to PostgreSQLJSONCustomConvertible
tanner0101 Jan 20, 2018
2a2eba4
Merge pull request #5 from vapor/data-refactor
tanner0101 Jan 20, 2018
1fdc734
add sockets dep
tanner0101 Jan 22, 2018
6dcd3bf
array support
tanner0101 Jan 23, 2018
a3b40ab
move sql to knownSQLName
tanner0101 Jan 23, 2018
1ac3a84
Merge pull request #6 from vapor/array-convertible
tanner0101 Jan 23, 2018
e0625cf
small array fix
tanner0101 Jan 23, 2018
e21369b
swift 4.1
tanner0101 Jan 31, 2018
0735c21
remove comment
tanner0101 Jan 31, 2018
4d23bd3
Merge pull request #10 from vapor/swift-41
tanner0101 Jan 31, 2018
c53b11f
password support
tanner0101 Feb 1, 2018
42011b3
md5 password hash impl
tanner0101 Feb 1, 2018
6bc472b
Update PostgreSQLConnection.swift
tanner0101 Feb 1, 2018
6c8026f
revert test changes
tanner0101 Feb 1, 2018
2ae8c2d
Merge branch 'password' of github.com:vapor/postgresql into password
tanner0101 Feb 1, 2018
b919eff
Merge pull request #11 from vapor/password
tanner0101 Feb 1, 2018
b065c71
Add possibility to enter password in PostgreSQLDatabaseConfig
MihaelIsaev Feb 1, 2018
53741a7
Update PostgreSQLDatabaseConfig.swift
tanner0101 Feb 1, 2018
650dc1c
Update PostgreSQLDatabase.swift
tanner0101 Feb 1, 2018
220fae2
Merge pull request #12 from MihaelIsaev/beta
tanner0101 Feb 1, 2018
f4e7b9f
3.0 dep updates
tanner0101 Feb 10, 2018
6490c35
Replace fatalErrors() by throwing errors
labradon Feb 12, 2018
47e39a6
Update dependencies
twof Feb 13, 2018
fa0511e
correct error identifier
labradon Feb 13, 2018
80554b2
Merge pull request #15 from labradon/replaceFatalErrors
tanner0101 Feb 14, 2018
8c5f8ed
Merge pull request #16 from twof/patch-1
tanner0101 Feb 14, 2018
82c7e14
dbkit beta 2
tanner0101 Feb 14, 2018
dbf408d
null data encode
tanner0101 Feb 14, 2018
b3ef0f6
fix tests'
tanner0101 Feb 14, 2018
2c9333d
Merge pull request #17 from vapor/null-encode
tanner0101 Feb 14, 2018
29b446c
prerelease version updates
tanner0101 Feb 15, 2018
c8f8823
update to dbkit beta 3 & remove connection config
tanner0101 Feb 16, 2018
53c619b
remove engine dep
tanner0101 Feb 16, 2018
6ce7b99
Merge pull request #20 from vapor/connection-config
tanner0101 Feb 16, 2018
c4739e3
version updates
tanner0101 Feb 19, 2018
7714408
Adds conformance to RawRepresentable to ease enum encoding
bensyverson Feb 21, 2018
e132581
metadata + error source updates
tanner0101 Feb 24, 2018
03f9f59
Merge pull request #21 from bensyverson/beta
tanner0101 Feb 24, 2018
5dacbf9
raw
tanner0101 Feb 24, 2018
635b068
Merge branch 'master' into beta
tanner0101 Feb 24, 2018
c80f392
Update README.md
tanner0101 Feb 24, 2018
bdb1eaf
Update README.md
tanner0101 Feb 24, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
Package.resolved

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
The MIT License (MIT)

Copyright (c) 2018 Qutheory, LLC

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
32 changes: 32 additions & 0 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// swift-tools-version:4.0
import PackageDescription

let package = Package(
name: "PostgreSQL",
products: [
.library(name: "PostgreSQL", targets: ["PostgreSQL"]),
],
dependencies: [
// ⏱ Promises and reactive-streams in Swift built for high-performance and scalability.
.package(url: "https://github.com/vapor/async.git", from: "1.0.0-rc"),

// 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging.
.package(url: "https://github.com/vapor/core.git", from: "3.0.0-rc"),

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

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

// 📦 Dependency injection / inversion of control framework.
.package(url: "https://github.com/vapor/service.git", from: "1.0.0-rc"),

// 🔌 Non-blocking TCP socket layer, with event-driven server and client.
.package(url: "https://github.com/vapor/sockets.git", from: "3.0.0-rc"),
],
targets: [
.target(name: "PostgreSQL", dependencies: ["Async", "Bits", "Crypto", "DatabaseKit", "Service", "TCP"]),
.testTarget(name: "PostgreSQLTests", dependencies: ["PostgreSQL"]),
]
)
25 changes: 22 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,24 @@
(See [vapor-community/postgresql](https://github.com/vapor-community/postgresql/) for libpq-based version)
<p align="center">
<img src="https://user-images.githubusercontent.com/1342803/36623751-7f1f2884-18d5-11e8-9fd8-5a94c23ec7ce.png" height="64" alt="PostgreSQL">
<br>
<br>
<a href="http://docs.vapor.codes/3.0/">
<img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation">
</a>
<a href="http://vapor.team">
<img src="http://vapor.team/badge.svg" alt="Slack Team">
</a>
<a href="LICENSE">
<img src="http://img.shields.io/badge/license-MIT-brightgreen.svg" alt="MIT License">
</a>
<a href="https://circleci.com/gh/vapor/postgresql">
<img src="https://circleci.com/gh/vapor/postgresql.svg?style=shield" alt="Continuous Integration">
</a>
<a href="https://swift.org">
<img src="http://img.shields.io/badge/swift-4.1-brightgreen.svg" alt="Swift 4.1">
</a>
</p>

# Pure-Swift PostgreSQL Library
<hr>

Work in progress
See [vapor-community/postgresql](https://github.com/vapor-community/postgresql/) for `libpq` based version.
77 changes: 77 additions & 0 deletions Sources/PostgreSQL/Connection/PostgreSQLConnection+Query.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import Async

extension PostgreSQLConnection {
/// Sends a parameterized PostgreSQL query command, collecting the parsed results.
public func query(
_ string: String,
_ parameters: [PostgreSQLDataCustomConvertible] = []
) throws -> Future<[[String: PostgreSQLData]]> {
var rows: [[String: PostgreSQLData]] = []
return try query(string, parameters) { row in
rows.append(row)
}.map(to: [[String: PostgreSQLData]].self) {
return rows
}
}

/// Sends a parameterized PostgreSQL query command, returning the parsed results to
/// the supplied closure.
public func query(
_ string: String,
_ parameters: [PostgreSQLDataCustomConvertible] = [],
resultFormat: PostgreSQLResultFormat = .binary(),
onRow: @escaping ([String: PostgreSQLData]) -> ()
) throws -> Future<Void> {
let parameters = try parameters.map { try $0.convertToPostgreSQLData() }
logger?.log(query: string, parameters: parameters)
let parse = PostgreSQLParseRequest(
statementName: "",
query: string,
parameterTypes: parameters.map { $0.type }
)
let describe = PostgreSQLDescribeRequest(type: .statement, name: "")
var currentRow: PostgreSQLRowDescription?

return send([
.parse(parse), .describe(describe), .sync
]) { message in
switch message {
case .parseComplete: break
case .rowDescription(let row): currentRow = row
case .parameterDescription: break
case .noData: break
default: throw PostgreSQLError(identifier: "query", reason: "Unexpected message during PostgreSQLParseRequest: \(message)", source: .capture())
}
}.flatMap(to: Void.self) {
let resultFormats = resultFormat.formatCodeFactory(currentRow?.fields.map { $0.dataType } ?? [])
// cache so we don't compute twice
let bind = PostgreSQLBindRequest(
portalName: "",
statementName: "",
parameterFormatCodes: parameters.map { $0.format },
parameters: parameters.map { .init(data: $0.data) },
resultFormatCodes: resultFormats
)
let execute = PostgreSQLExecuteRequest(
portalName: "",
maxRows: 0
)
return self.send([
.bind(bind), .execute(execute), .sync
]) { message in
switch message {
case .bindComplete: break
case .dataRow(let data):
guard let row = currentRow else {
throw PostgreSQLError(identifier: "query", reason: "Unexpected PostgreSQLDataRow without preceding PostgreSQLRowDescription.", source: .capture())
}
let parsed = try row.parse(data: data, formatCodes: resultFormats)
onRow(parsed)
case .close: break
case .noData: break
default: throw PostgreSQLError(identifier: "query", reason: "Unexpected message during PostgreSQLParseRequest: \(message)", source: .capture())
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Async

extension PostgreSQLConnection {
/// Sends a simple PostgreSQL query command, collecting the parsed results.
public func simpleQuery(_ string: String) -> Future<[[String: PostgreSQLData]]> {
var rows: [[String: PostgreSQLData]] = []
return simpleQuery(string) { row in
rows.append(row)
}.map(to: [[String: PostgreSQLData]].self) {
return rows
}
}

/// Sends a simple PostgreSQL query command, returning the parsed results to
/// the supplied closure.
public func simpleQuery(_ string: String, onRow: @escaping ([String: PostgreSQLData]) -> ()) -> Future<Void> {
logger?.log(query: string, parameters: [])
var currentRow: PostgreSQLRowDescription?
let query = PostgreSQLQuery(query: string)
return send([.query(query)]) { message in
switch message {
case .rowDescription(let row):
currentRow = row
case .dataRow(let data):
guard let row = currentRow else {
throw PostgreSQLError(identifier: "simpleQuery", reason: "Unexpected PostgreSQLDataRow without preceding PostgreSQLRowDescription.", source: .capture())
}
let parsed = try row.parse(data: data, formatCodes: row.fields.map { $0.formatCode })
onRow(parsed)
case .close: break // query over, waiting for `readyForQuery`
default: throw PostgreSQLError(identifier: "simpleQuery", reason: "Unexpected message during PostgreSQLQuery: \(message)", source: .capture())
}
}
}
}
18 changes: 18 additions & 0 deletions Sources/PostgreSQL/Connection/PostgreSQLConnection+TCP.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Async
import TCP

extension PostgreSQLConnection {
/// Connects to a Redis server using a TCP socket.
public static func connect(
hostname: String = "localhost",
port: UInt16 = 5432,
on worker: Worker,
onError: @escaping TCPSocketSink.ErrorHandler
) throws -> PostgreSQLConnection {
let socket = try TCPSocket(isNonBlocking: true)
let client = try TCPClient(socket: socket)
try client.connect(hostname: hostname, port: port)
let stream = socket.stream(on: worker, onError: onError)
return PostgreSQLConnection(stream: stream, on: worker)
}
}
130 changes: 130 additions & 0 deletions Sources/PostgreSQL/Connection/PostgreSQLConnection.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import Async
import Crypto

/// A PostgreSQL frontend client.
public final class PostgreSQLConnection {
/// Handles enqueued redis commands and responses.
private let queueStream: QueueStream<PostgreSQLMessage, PostgreSQLMessage>

/// If non-nil, will log queries.
public var logger: PostgreSQLLogger?

/// Creates a new Redis client on the provided data source and sink.
init<Stream>(stream: Stream, on worker: Worker) where Stream: ByteStream {
let queueStream = QueueStream<PostgreSQLMessage, PostgreSQLMessage>()

let serializerStream = PostgreSQLMessageSerializer().stream(on: worker)
let parserStream = PostgreSQLMessageParser().stream(on: worker)

stream.stream(to: parserStream)
.stream(to: queueStream)
.stream(to: serializerStream)
.output(to: stream)

self.queueStream = queueStream
}

/// Sends `PostgreSQLMessage` to the server.
func send(_ messages: [PostgreSQLMessage], onResponse: @escaping (PostgreSQLMessage) throws -> ()) -> Future<Void> {
var error: Error?
return queueStream.enqueue(messages) { message in
switch message {
case .readyForQuery:
if let e = error { throw e }
return true
case .error(let e): error = e
case .notice(let n): print(n)
default: try onResponse(message)
}
return false // request until ready for query
}
}

/// Sends `PostgreSQLMessage` to the server.
func send(_ message: [PostgreSQLMessage]) -> Future<[PostgreSQLMessage]> {
var responses: [PostgreSQLMessage] = []
return send(message) { response in
responses.append(response)
}.map(to: [PostgreSQLMessage].self) {
return responses
}
}

/// Authenticates the `PostgreSQLClient` using a username with no password.
public func authenticate(username: String, database: String? = nil, password: String? = nil) -> Future<Void> {
let startup = PostgreSQLStartupMessage.versionThree(parameters: [
"user": username,
"database": database ?? username
])
var authRequest: PostgreSQLAuthenticationRequest?
return queueStream.enqueue([.startupMessage(startup)]) { message in
switch message {
case .authenticationRequest(let a):
authRequest = a
return true
default: throw PostgreSQLError(identifier: "auth", reason: "Unsupported message encountered during auth: \(message).", source: .capture())
}
}.flatMap(to: Void.self) {
guard let auth = authRequest else {
throw PostgreSQLError(identifier: "authRequest", reason: "No authorization request / status sent.", source: .capture())
}

let input: [PostgreSQLMessage]
switch auth {
case .ok:
guard password == nil else {
throw PostgreSQLError(identifier: "trust", reason: "No password is required", source: .capture())
}
input = []
case .plaintext:
guard let password = password else {
throw PostgreSQLError(identifier: "password", reason: "Password is required", source: .capture())
}
let passwordMessage = PostgreSQLPasswordMessage(password: password)
input = [.password(passwordMessage)]
case .md5(let salt):
guard let password = password else {
throw PostgreSQLError(identifier: "password", reason: "Password is required", source: .capture())
}
guard let passwordData = password.data(using: .utf8) else {
throw PostgreSQLError(identifier: "passwordUTF8", reason: "Could not convert password to UTF-8 encoded Data.", source: .capture())
}

guard let usernameData = username.data(using: .utf8) else {
throw PostgreSQLError(identifier: "usernameUTF8", reason: "Could not convert username to UTF-8 encoded Data.", source: .capture())
}

let hasher = MD5()
// pwdhash = md5(password + username).hexdigest()
var passwordUsernameData = passwordData + usernameData
hasher.update(sequence: &passwordUsernameData)
hasher.finalize()
guard let pwdhash = hasher.hash.hexString.data(using: .utf8) else {
throw PostgreSQLError(identifier: "hashUTF8", reason: "Could not convert password hash to UTF-8 encoded Data.", source: .capture())
}
hasher.reset()
// hash = ′ md 5′ + md 5(pwdhash + salt ).hexdigest ()
var saltedData = pwdhash + salt
hasher.update(sequence: &saltedData)
hasher.finalize()
let passwordMessage = PostgreSQLPasswordMessage(password: "md5" + hasher.hash.hexString)
input = [.password(passwordMessage)]
}

return self.queueStream.enqueue(input) { message in
switch message {
case .error(let error): throw error
case .readyForQuery: return true
case .authenticationRequest: return false
case .parameterStatus, .backendKeyData: return false
default: throw PostgreSQLError(identifier: "authenticationMessage", reason: "Unexpected authentication message: \(message)", source: .capture())
}
}
}
}

/// Closes this client.
public func close() {
queueStream.close()
}
}
Loading