Skip to content

Commit d82adad

Browse files
authored
fix already closed error (#29)
* fix already closed error * remove fatal errors * add CoC + readme * add array support * add array support
1 parent 650884b commit d82adad

28 files changed

+652
-183
lines changed

CODE_OF_CONDUCT.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Code of Conduct
2+
3+
To be a truly great community, Swift.org needs to welcome developers from all walks of life,
4+
with different backgrounds, and with a wide range of experience. A diverse and friendly
5+
community will have more great ideas, more unique perspectives, and produce more great
6+
code. We will work diligently to make the Swift community welcoming to everyone.
7+
8+
To give clarity of what is expected of our members, Swift.org has adopted the code of conduct
9+
defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source
10+
communities, and we think it articulates our values well. The full text is copied below:
11+
12+
### Contributor Code of Conduct v1.3
13+
14+
As contributors and maintainers of this project, and in the interest of fostering an open and
15+
welcoming community, we pledge to respect all people who contribute through reporting
16+
issues, posting feature requests, updating documentation, submitting pull requests or patches,
17+
and other activities.
18+
19+
We are committed to making participation in this project a harassment-free experience for
20+
everyone, regardless of level of experience, gender, gender identity and expression, sexual
21+
orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or
22+
nationality.
23+
24+
Examples of unacceptable behavior by participants include:
25+
26+
- The use of sexualized language or imagery
27+
- Personal attacks
28+
- Trolling or insulting/derogatory comments
29+
- Public or private harassment
30+
- Publishing other’s private information, such as physical or electronic addresses, without explicit permission
31+
- Other unethical or unprofessional conduct
32+
33+
Project maintainers have the right and responsibility to remove, edit, or reject comments,
34+
commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of
35+
Conduct, or to ban temporarily or permanently any contributor for other behaviors that they
36+
deem inappropriate, threatening, offensive, or harmful.
37+
38+
By adopting this Code of Conduct, project maintainers commit themselves to fairly and
39+
consistently applying these principles to every aspect of managing this project. Project
40+
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
41+
from the project team.
42+
43+
This code of conduct applies both within project spaces and in public spaces when an
44+
individual is representing the project or its community.
45+
46+
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
47+
contacting a project maintainer at [[email protected]](mailto:[email protected]). All complaints will be reviewed and
48+
investigated and will result in a response that is deemed necessary and appropriate to the
49+
circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter
50+
of an incident.
51+
52+
*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](http://contributor-covenant.org/version/1/3/0/).*
53+
54+
### Reporting
55+
56+
A working group of community members is committed to promptly addressing any [reported
57+
issues](mailto:[email protected]). Working group members are volunteers appointed by the project lead, with a
58+
preference for individuals with varied backgrounds and perspectives. Membership is expected
59+
to change regularly, and may grow or shrink.

README.md

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,189 @@
11
# NIOPostgres
22

3-
🐘 Non-blocking, event-driven Swift client for PostgreSQL.
3+
🐘 Non-blocking, event-driven Swift client for PostgreSQL built on [SwiftNIO](https://github.com/apple/swift-nio).
4+
5+
### Major Releases
6+
7+
The table below shows a list of NIOPostgres major releases alongside their compatible NIO and Swift versions.
8+
9+
Version | NIO | Swift | SPM
10+
--- | --- | --- | ---
11+
1.0 (alpha) | 2.0+ | 5.0+ | `from: "1.0.0-alpha"`
12+
13+
Use the SPM string to easily include the dependendency in your `Package.swift` file.
14+
15+
```swift
16+
.package(url: "https://github.com/vapor/nio-postgres.git", from: ...)
17+
```
18+
19+
### Supported Platforms
20+
21+
NIOPostgres supports the following platforms:
22+
23+
- Ubuntu 14.04+
24+
- macOS 10.12+
25+
26+
## Overview
27+
28+
NIOPostgres is a client package for connecting to, authorizing, and querying a PostgreSQL server. At the heart of this module are NIO channel handlers for parsing and serializing messages in PostgreSQL's proprietary wire protocol. These channel handlers are combined in a request / response style connection type that provides a convenient, client-like interface for performing queries.
29+
30+
Support for both simple (text) and parameterized (binary) querying is provided out of the box alongside a `PostgresData` type that handles conversion between PostgreSQL's wire format and native Swift types.
31+
32+
### Motiviation
33+
34+
Most Swift implementations of Postgres clients are based on the [libpq](https://www.postgresql.org/docs/11/libpq.html) C library which handles transport internally. Building a library directly on top of Postgres' wire protocol using SwiftNIO should yield a more reliable, maintainable, and performant interface for PostgreSQL databases.
35+
36+
### Goals
37+
38+
This package is meant to be a low-level, unopinionated PostgreSQL wire-protocol implementation for Swift. The hope is that higher level packages can share NIOPostgres as a foundation for interacting with PostgreSQL servers without needing to duplicate complex logic.
39+
40+
Because of this, NIOPostgres excludes some important concepts for the sake of simplicity, such as:
41+
42+
- Connection pooling
43+
- Swift `Codable` integration
44+
- Query building
45+
46+
If you are looking for a PostgreSQL client package to use in your project, take a look at these higher-level packages built on top of NIOPostgres:
47+
48+
- [`vapor/postgres-kit`](https://github.com/vapor/postgresql)
49+
50+
### Dependencies
51+
52+
This package has four dependencies:
53+
54+
- [`apple/swift-nio`](https://github.com/apple/swift-nio) for IO
55+
- [`apple/swift-nio-ssl`](https://github.com/apple/swift-nio-ssl) for TLS
56+
- [`apple/swift-log`](https://github.com/apple/swift-logs) for logging
57+
- [`apple/swift-metrics`](https://github.com/apple/swift-metrics) for metrics
58+
59+
This package has no additional system dependencies.
60+
61+
## API Docs
62+
63+
Check out the [NIOPostgres API docs]((https://api.vapor.codes/nio-postgres/master/NIOPostgres/index.html)) for a detailed look at all of the classes, structs, protocols, and more.
64+
65+
## Getting Started
66+
67+
This section will provide a quick look at using NIOPostgres.
68+
69+
### Creating a Connection
70+
71+
The first step to making a query is creating a new `PostgresConnection`. The minimum requirements to create one are a `SocketAddress` and `EventLoop`.
72+
73+
```swift
74+
import NIOPostgres
75+
76+
let eventLoop: EventLoop = ...
77+
let conn = try PostgresConnection.connect(
78+
to: .makeAddressResolvingHost("my.psql.server", port: 5432),
79+
on: eventLoop
80+
).wait()
81+
```
82+
83+
Note: These examples will make use of `wait()` for simplicity. This is appropriate if you are using NIOPostgres on the main thread, like for a CLI tool or in tests. However, you should never use `wait()` on an event loop.
84+
85+
There are a few ways to create a `SocketAddress`:
86+
87+
- `init(ipAddress: String, port: Int)`
88+
- `init(unixDomainSocketPath: String)`
89+
- `makeAddressResolvingHost(_ host: String, port: Int)`
90+
91+
There are also some additional arguments you can supply to `connect`.
92+
93+
- `tlsConfiguration` An optional `TLSConfiguration` struct. If supplied, the PostgreSQL connection will be upgraded to use SSL.
94+
- `serverHostname` An optional `String` to use in conjunction with `tlsConfiguration` to specify the server's hostname.
95+
96+
`connect` will return a future `PostgresConnection`, or an error if it could not connect.
97+
98+
### Client Protocol
99+
100+
Interaction with a server revolves around the `PostgresClient` protocol. This protocol includes methods like `query(_:)` for executing SQL queries and reading the resulting rows.
101+
102+
`PostgresConnection` is the default implementation of `PostgresClient` provided by this package. Assume the client here is the connection from the previous example.
103+
104+
```swift
105+
import NIOPostgres
106+
107+
let client: PostgresClient = ...
108+
// now we can use client to do queries
109+
```
110+
111+
### Simple Query
112+
113+
Simple (or text) queries allow you to execute a SQL string on the connected PostgreSQL server. These queries do not support binding parameters, so any values sent must be escaped manually.
114+
115+
These queries are most useful for schema or transactional queries, or simple selects. Note that values returned by simple queries will be transferred in the less efficient text format.
116+
117+
`simpleQuery` has two overloads, one that returns an array of rows, and one that accepts a closure for handling each row as it is returned.
118+
119+
```swift
120+
let rows = try client.simpleQuery("SELECT version()").wait()
121+
print(rows) // [["version": "11.0.0"]]
122+
123+
try client.simpleQuery("SELECT version()") { row in
124+
print(row) // ["version": "11.0.0"]
125+
}.wait()
126+
```
127+
128+
### Parameterized Query
129+
130+
Parameterized (or binary) queries allow you to execute a SQL string on the connected PostgreSQL server. These queries support passing bound parameters as a separate argument. Each parameter is represented in the SQL string using incrementing placeholders, starting at `$1`.
131+
132+
These queries are most useful for selecting, inserting, and updating data. Data for these queries is transferred using the highly efficient binary format.
133+
134+
Just like `simpleQuery`, `query` also offers two overloads. One that returns an array of rows, and one that accepts a closure for handling each row as it is returned.
135+
136+
```swift
137+
let rows = try client.query("SELECT * FROM planets WHERE name = $1", ["Earth"]).wait()
138+
print(rows) // [["id": 42, "name": "Earth"]]
139+
140+
try client.query("SELECT * FROM planets WHERE name = $1", ["Earth"]) { row in
141+
print(row) // ["id": 42, "name": "Earth"]
142+
}.wait()
143+
```
144+
145+
### Rows and Data
146+
147+
Both `simpleQuery` and `query` return the same `PostgresRow` type. Columns can be fetched from the row using the `column(_: String)` method.
148+
149+
```swift
150+
let row: PostgresRow = ...
151+
let version = row.column("version")
152+
print(version) // PostgresData?
153+
```
154+
155+
`PostgresRow` columns are stored as `PostgresData`. This struct contains the raw bytes returned by PostgreSQL as well as some information for parsing them, such as:
156+
157+
- Postgres column type
158+
- Wire format: binary or text
159+
- Value as array of bytes
160+
161+
`PostgresData` has a variety of convenience methods for converting column data to usable Swift types.
162+
163+
```swift
164+
let data: PostgresData= ...
165+
166+
print(data.string) // String?
167+
168+
print(data.int) // Int?
169+
print(data.int8) // Int8?
170+
print(data.int16) // Int16?
171+
print(data.int32) // Int32?
172+
print(data.int64) // Int64?
173+
174+
print(data.uint) // UInt?
175+
print(data.uint8) // UInt8?
176+
print(data.uint16) // UInt16?
177+
print(data.uint32) // UInt32?
178+
print(data.uint64) // UInt64?
179+
180+
print(data.float) // Float?
181+
print(data.double) // Double?
182+
183+
print(data.date) // Date?
184+
print(data.uuid) // UUID?
185+
186+
print(data.numeric) // PostgresNumeric?
187+
```
188+
189+
`PostgresData` is also used for sending data _to_ the server via parameterized values. To create `PostgresData` from a Swift type, use the available intializer methods.

Sources/NIOPostgres/Connection/PostgresConnection.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,22 +13,34 @@ public final class PostgresConnection {
1313
}
1414

1515
public var logger: Logger
16+
17+
private var didClose: Bool
1618

1719
init(channel: Channel, logger: Logger) {
1820
self.channel = channel
1921
self.logger = logger
22+
self.didClose = false
2023
}
2124

2225
public func close() -> EventLoopFuture<Void> {
23-
guard self.channel.isActive else {
26+
guard !self.didClose else {
2427
return self.eventLoop.makeSucceededFuture(())
2528
}
26-
return self.channel.close()
29+
self.didClose = true
30+
31+
let promise = self.eventLoop.makePromise(of: Void.self)
32+
self.eventLoop.submit {
33+
switch self.channel.isActive {
34+
case true:
35+
promise.succeed(())
36+
case false:
37+
self.channel.close(mode: .all, promise: promise)
38+
}
39+
}.cascadeFailure(to: promise)
40+
return promise.futureResult
2741
}
2842

2943
deinit {
30-
if self.channel.isActive {
31-
assertionFailure("PostgresConnection deinitialized before being closed.")
32-
}
44+
assert(self.didClose, "PostgresConnection deinitialized before being closed.")
3345
}
3446
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
extension PostgresData {
2+
public init<T>(array: [T])
3+
where T: PostgresDataConvertible
4+
{
5+
let elementType = T.postgresDataType
6+
guard let arrayType = elementType.arrayType else {
7+
fatalError("No array type for \(elementType)")
8+
}
9+
var buffer = ByteBufferAllocator().buffer(capacity: 0)
10+
// 0 if empty, 1 if not
11+
buffer.writeInteger(array.isEmpty ? 0 : 1, as: UInt32.self)
12+
// b
13+
buffer.writeInteger(0, as: UInt32.self)
14+
// array element type
15+
buffer.writeInteger(elementType.rawValue)
16+
17+
// continue if the array is not empty
18+
if !array.isEmpty {
19+
// length of array
20+
buffer.writeInteger(numericCast(array.count), as: UInt32.self)
21+
// dimensions
22+
buffer.writeInteger(1, as: UInt32.self)
23+
24+
for item in array {
25+
if var value = item.postgresData?.value {
26+
buffer.writeInteger(numericCast(value.readableBytes), as: UInt32.self)
27+
buffer.writeBuffer(&value)
28+
} else {
29+
buffer.writeInteger(0, as: UInt32.self)
30+
}
31+
}
32+
}
33+
34+
self.init(type: arrayType, typeModifier: nil, formatCode: .binary, value: buffer)
35+
}
36+
37+
public func array<T>(of type: T.Type = T.self) -> [T]?
38+
where T: PostgresDataConvertible
39+
{
40+
guard var value = self.value else {
41+
return nil
42+
}
43+
// ensures the data type is actually an array
44+
guard self.type.elementType != nil else {
45+
return nil
46+
}
47+
guard let isNotEmpty = value.readInteger(as: UInt32.self) else {
48+
return nil
49+
}
50+
guard let b = value.readInteger(as: UInt32.self) else {
51+
return nil
52+
}
53+
assert(b == 0, "Array b field did not equal zero")
54+
guard let type = value.readInteger(as: PostgresDataType.self) else {
55+
return nil
56+
}
57+
guard isNotEmpty == 1 else {
58+
return []
59+
}
60+
guard let length = value.readInteger(as: UInt32.self) else {
61+
return nil
62+
}
63+
assert(length >= 0, "Invalid length")
64+
65+
guard let dimensions = value.readInteger(as: UInt32.self) else {
66+
return nil
67+
}
68+
assert(dimensions == 1, "Multi-dimensional arrays not yet supported")
69+
70+
var array: [T] = []
71+
while
72+
let itemLength = value.readInteger(as: UInt32.self),
73+
let itemValue = value.readSlice(length: numericCast(itemLength))
74+
{
75+
let data = PostgresData(type: type, typeModifier: nil, formatCode: self.formatCode, value: itemValue)
76+
guard let t = T(postgresData: data) else {
77+
// if we fail to convert any data, fail the entire array
78+
return nil
79+
}
80+
array.append(t)
81+
}
82+
return array
83+
}
84+
}
85+
86+
extension Array: PostgresDataConvertible where Element: PostgresDataConvertible {
87+
public static var postgresDataType: PostgresDataType {
88+
guard let arrayType = Element.postgresDataType.arrayType else {
89+
fatalError("No array type for \(Element.postgresDataType)")
90+
}
91+
return arrayType
92+
}
93+
94+
public init?(postgresData: PostgresData) {
95+
guard let array = postgresData.array(of: Element.self) else {
96+
return nil
97+
}
98+
self = array
99+
}
100+
101+
public var postgresData: PostgresData? {
102+
return PostgresData(array: self)
103+
}
104+
}

0 commit comments

Comments
 (0)