Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 59 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Code of Conduct

To be a truly great community, Swift.org needs to welcome developers from all walks of life,
with different backgrounds, and with a wide range of experience. A diverse and friendly
community will have more great ideas, more unique perspectives, and produce more great
code. We will work diligently to make the Swift community welcoming to everyone.

To give clarity of what is expected of our members, Swift.org has adopted the code of conduct
defined by [contributor-covenant.org](https://www.contributor-covenant.org). This document is used across many open source
communities, and we think it articulates our values well. The full text is copied below:

### Contributor Code of Conduct v1.3

As contributors and maintainers of this project, and in the interest of fostering an open and
welcoming community, we pledge to respect all people who contribute through reporting
issues, posting feature requests, updating documentation, submitting pull requests or patches,
and other activities.

We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression, sexual
orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or
nationality.

Examples of unacceptable behavior by participants include:

- The use of sexualized language or imagery
- Personal attacks
- Trolling or insulting/derogatory comments
- Public or private harassment
- Publishing other’s private information, such as physical or electronic addresses, without explicit permission
- Other unethical or unprofessional conduct

Project maintainers have the right and responsibility to remove, edit, or reject comments,
commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of
Conduct, or to ban temporarily or permanently any contributor for other behaviors that they
deem inappropriate, threatening, offensive, or harmful.

By adopting this Code of Conduct, project maintainers commit themselves to fairly and
consistently applying these principles to every aspect of managing this project. Project
maintainers who do not follow or enforce the Code of Conduct may be permanently removed
from the project team.

This code of conduct applies both within project spaces and in public spaces when an
individual is representing the project or its community.

Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
contacting a project maintainer at [conduct@swift.org](mailto:conduct@swift.org). All complaints will be reviewed and
investigated and will result in a response that is deemed necessary and appropriate to the
circumstances. Maintainers are obligated to maintain confidentiality with regard to the reporter
of an incident.

*This policy is adapted from the Contributor Code of Conduct [version 1.3.0](http://contributor-covenant.org/version/1/3/0/).*

### Reporting

A working group of community members is committed to promptly addressing any [reported
issues](mailto:conduct@swift.org). Working group members are volunteers appointed by the project lead, with a
preference for individuals with varied backgrounds and perspectives. Membership is expected
to change regularly, and may grow or shrink.
188 changes: 187 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,189 @@
# NIOPostgres

🐘 Non-blocking, event-driven Swift client for PostgreSQL.
🐘 Non-blocking, event-driven Swift client for PostgreSQL built on [SwiftNIO](https://github.com/apple/swift-nio).

### Major Releases

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

Version | NIO | Swift | SPM
--- | --- | --- | ---
1.0 (alpha) | 2.0+ | 5.0+ | `from: "1.0.0-alpha"`

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

```swift
.package(url: "https://github.com/vapor/nio-postgres.git", from: ...)
```

### Supported Platforms

NIOPostgres supports the following platforms:

- Ubuntu 14.04+
- macOS 10.12+

## Overview

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.

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.

### Motiviation

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.

### Goals

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.

Because of this, NIOPostgres excludes some important concepts for the sake of simplicity, such as:

- Connection pooling
- Swift `Codable` integration
- Query building

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:

- [`vapor/postgres-kit`](https://github.com/vapor/postgresql)

### Dependencies

This package has four dependencies:

- [`apple/swift-nio`](https://github.com/apple/swift-nio) for IO
- [`apple/swift-nio-ssl`](https://github.com/apple/swift-nio-ssl) for TLS
- [`apple/swift-log`](https://github.com/apple/swift-logs) for logging
- [`apple/swift-metrics`](https://github.com/apple/swift-metrics) for metrics

This package has no additional system dependencies.

## API Docs

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.

## Getting Started

This section will provide a quick look at using NIOPostgres.

### Creating a Connection

The first step to making a query is creating a new `PostgresConnection`. The minimum requirements to create one are a `SocketAddress` and `EventLoop`.

```swift
import NIOPostgres

let eventLoop: EventLoop = ...
let conn = try PostgresConnection.connect(
to: .makeAddressResolvingHost("my.psql.server", port: 5432),
on: eventLoop
).wait()
```

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.

There are a few ways to create a `SocketAddress`:

- `init(ipAddress: String, port: Int)`
- `init(unixDomainSocketPath: String)`
- `makeAddressResolvingHost(_ host: String, port: Int)`

There are also some additional arguments you can supply to `connect`.

- `tlsConfiguration` An optional `TLSConfiguration` struct. If supplied, the PostgreSQL connection will be upgraded to use SSL.
- `serverHostname` An optional `String` to use in conjunction with `tlsConfiguration` to specify the server's hostname.

`connect` will return a future `PostgresConnection`, or an error if it could not connect.

### Client Protocol

Interaction with a server revolves around the `PostgresClient` protocol. This protocol includes methods like `query(_:)` for executing SQL queries and reading the resulting rows.

`PostgresConnection` is the default implementation of `PostgresClient` provided by this package. Assume the client here is the connection from the previous example.

```swift
import NIOPostgres

let client: PostgresClient = ...
// now we can use client to do queries
```

### Simple Query

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.

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.

`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.

```swift
let rows = try client.simpleQuery("SELECT version()").wait()
print(rows) // [["version": "11.0.0"]]

try client.simpleQuery("SELECT version()") { row in
print(row) // ["version": "11.0.0"]
}.wait()
```

### Parameterized Query

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`.

These queries are most useful for selecting, inserting, and updating data. Data for these queries is transferred using the highly efficient binary format.

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.

```swift
let rows = try client.query("SELECT * FROM planets WHERE name = $1", ["Earth"]).wait()
print(rows) // [["id": 42, "name": "Earth"]]

try client.query("SELECT * FROM planets WHERE name = $1", ["Earth"]) { row in
print(row) // ["id": 42, "name": "Earth"]
}.wait()
```

### Rows and Data

Both `simpleQuery` and `query` return the same `PostgresRow` type. Columns can be fetched from the row using the `column(_: String)` method.

```swift
let row: PostgresRow = ...
let version = row.column("version")
print(version) // PostgresData?
```

`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:

- Postgres column type
- Wire format: binary or text
- Value as array of bytes

`PostgresData` has a variety of convenience methods for converting column data to usable Swift types.

```swift
let data: PostgresData= ...

print(data.string) // String?

print(data.int) // Int?
print(data.int8) // Int8?
print(data.int16) // Int16?
print(data.int32) // Int32?
print(data.int64) // Int64?

print(data.uint) // UInt?
print(data.uint8) // UInt8?
print(data.uint16) // UInt16?
print(data.uint32) // UInt32?
print(data.uint64) // UInt64?

print(data.float) // Float?
print(data.double) // Double?

print(data.date) // Date?
print(data.uuid) // UUID?

print(data.numeric) // PostgresNumeric?
```

`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.
22 changes: 17 additions & 5 deletions Sources/NIOPostgres/Connection/PostgresConnection.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,34 @@ public final class PostgresConnection {
}

public var logger: Logger

private var didClose: Bool

init(channel: Channel, logger: Logger) {
self.channel = channel
self.logger = logger
self.didClose = false
}

public func close() -> EventLoopFuture<Void> {
guard self.channel.isActive else {
guard !self.didClose else {
return self.eventLoop.makeSucceededFuture(())
}
return self.channel.close()
self.didClose = true

let promise = self.eventLoop.makePromise(of: Void.self)
self.eventLoop.submit {
switch self.channel.isActive {
case true:
promise.succeed(())
case false:
self.channel.close(mode: .all, promise: promise)
}
}.cascadeFailure(to: promise)
return promise.futureResult
}

deinit {
if self.channel.isActive {
assertionFailure("PostgresConnection deinitialized before being closed.")
}
assert(self.didClose, "PostgresConnection deinitialized before being closed.")
}
}
104 changes: 104 additions & 0 deletions Sources/NIOPostgres/Data/PostgresData+Array.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
extension PostgresData {
public init<T>(array: [T])
where T: PostgresDataConvertible
{
let elementType = T.postgresDataType
guard let arrayType = elementType.arrayType else {
fatalError("No array type for \(elementType)")
}
var buffer = ByteBufferAllocator().buffer(capacity: 0)
// 0 if empty, 1 if not
buffer.writeInteger(array.isEmpty ? 0 : 1, as: UInt32.self)
// b
buffer.writeInteger(0, as: UInt32.self)
// array element type
buffer.writeInteger(elementType.rawValue)

// continue if the array is not empty
if !array.isEmpty {
// length of array
buffer.writeInteger(numericCast(array.count), as: UInt32.self)
// dimensions
buffer.writeInteger(1, as: UInt32.self)

for item in array {
if var value = item.postgresData?.value {
buffer.writeInteger(numericCast(value.readableBytes), as: UInt32.self)
buffer.writeBuffer(&value)
} else {
buffer.writeInteger(0, as: UInt32.self)
}
}
}

self.init(type: arrayType, typeModifier: nil, formatCode: .binary, value: buffer)
}

public func array<T>(of type: T.Type = T.self) -> [T]?
where T: PostgresDataConvertible
{
guard var value = self.value else {
return nil
}
// ensures the data type is actually an array
guard self.type.elementType != nil else {
return nil
}
guard let isNotEmpty = value.readInteger(as: UInt32.self) else {
return nil
}
guard let b = value.readInteger(as: UInt32.self) else {
return nil
}
assert(b == 0, "Array b field did not equal zero")
guard let type = value.readInteger(as: PostgresDataType.self) else {
return nil
}
guard isNotEmpty == 1 else {
return []
}
guard let length = value.readInteger(as: UInt32.self) else {
return nil
}
assert(length >= 0, "Invalid length")

guard let dimensions = value.readInteger(as: UInt32.self) else {
return nil
}
assert(dimensions == 1, "Multi-dimensional arrays not yet supported")

var array: [T] = []
while
let itemLength = value.readInteger(as: UInt32.self),
let itemValue = value.readSlice(length: numericCast(itemLength))
{
let data = PostgresData(type: type, typeModifier: nil, formatCode: self.formatCode, value: itemValue)
guard let t = T(postgresData: data) else {
// if we fail to convert any data, fail the entire array
return nil
}
array.append(t)
}
return array
}
}

extension Array: PostgresDataConvertible where Element: PostgresDataConvertible {
public static var postgresDataType: PostgresDataType {
guard let arrayType = Element.postgresDataType.arrayType else {
fatalError("No array type for \(Element.postgresDataType)")
}
return arrayType
}

public init?(postgresData: PostgresData) {
guard let array = postgresData.array(of: Element.self) else {
return nil
}
self = array
}

public var postgresData: PostgresData? {
return PostgresData(array: self)
}
}
Loading