|
1 | 1 | <img src="https://user-images.githubusercontent.com/1342803/59061804-5548e280-8872-11e9-819f-14f19f16fcb6.png" height="64" alt="PostgresNIO"> |
2 | | -<br> |
3 | | -<a href="https://docs.vapor.codes/4.0/"> |
| 2 | + |
| 3 | +<a href="https://github.com/swift-server/sswg/blob/main/process/incubation.md#graduated-level"> |
| 4 | + <img src="https://img.shields.io/badge/sswg-incubating-green.svg" alt="SSWG Incubating"> |
| 5 | +</a> |
| 6 | +<a href="https://api.vapor.codes/postgres-nio/main/PostgresNIO/"> |
4 | 7 | <img src="http://img.shields.io/badge/read_the-docs-2196f3.svg" alt="Documentation"> |
5 | 8 | </a> |
6 | 9 | <a href="https://discord.gg/vapor"> |
|
18 | 21 | <br> |
19 | 22 | <br> |
20 | 23 |
|
21 | | -🐘 Non-blocking, event-driven Swift client for PostgreSQL built on [SwiftNIO](https://github.com/apple/swift-nio). |
22 | | - |
23 | | -### Major Releases |
24 | | - |
25 | | -The table below shows a list of PostgresNIO major releases alongside their compatible NIO and Swift versions. |
26 | | - |
27 | | -|Version|NIO|Swift|SPM| |
28 | | -|-|-|-|-| |
29 | | -|1.0|2.0+|5.2+|`from: "1.0.0"`| |
30 | | - |
31 | | -Use the SPM string to easily include the dependendency in your `Package.swift` file. |
32 | | - |
33 | | -```swift |
34 | | -.package(url: "https://github.com/vapor/postgres-nio.git", from: ...) |
35 | | -``` |
36 | | - |
37 | | -### Supported Platforms |
38 | | - |
39 | | -PostgresNIO supports the following platforms: |
40 | | - |
41 | | -- Ubuntu 16.04+ |
42 | | -- macOS 10.15+ |
43 | | - |
44 | | -### Security |
45 | | - |
46 | | -Please see [SECURITY.md](https://github.com/vapor/.github/blob/main/SECURITY.md) for details on the security process. |
47 | | - |
48 | | -## Overview |
49 | | - |
50 | | -PostgresNIO 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. |
51 | | - |
52 | | -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. |
53 | | - |
54 | | -### Motivation |
55 | | - |
56 | | -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. |
57 | | - |
58 | | -### Goals |
59 | | - |
60 | | -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 PostgresNIO as a foundation for interacting with PostgreSQL servers without needing to duplicate complex logic. |
61 | | - |
62 | | -Because of this, PostgresNIO excludes some important concepts for the sake of simplicity, such as: |
63 | | - |
64 | | -- Connection pooling |
65 | | -- Swift `Codable` integration |
66 | | -- Query building |
67 | | - |
68 | | -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 PostgresNIO: |
69 | | - |
70 | | -- [`vapor/postgres-kit`](https://github.com/vapor/postgresql) |
71 | | - |
72 | | -### Dependencies |
| 24 | +🐘 Non-blocking, event-driven Swift client for PostgreSQL built on [SwiftNIO]. |
73 | 25 |
|
74 | | -This package has four dependencies: |
| 26 | +Features: |
75 | 27 |
|
76 | | -- [`apple/swift-nio`](https://github.com/apple/swift-nio) for IO |
77 | | -- [`apple/swift-nio-ssl`](https://github.com/apple/swift-nio-ssl) for TLS |
78 | | -- [`apple/swift-log`](https://github.com/apple/swift-log) for logging |
79 | | -- [`apple/swift-metrics`](https://github.com/apple/swift-metrics) for metrics |
| 28 | +- A `PostgresConnection` which allows you to connect to, authorize with, query, and retrieve results from a PostgreSQL server |
| 29 | +- An async/await interface that supports backpressure |
| 30 | +- Automatic conversions between Swift primitive types and the Postgres wire format |
| 31 | +- Integrated with the Swift server ecosystem, including use of [SwiftLog]. |
| 32 | +- Designed to run efficiently on all supported platforms (tested extensively on Linux and Darwin systems) |
| 33 | +- Support for `Network.framework` when available (e.g. on Apple platforms) |
80 | 34 |
|
81 | | -This package has no additional system dependencies. |
| 35 | +PostgresNIO does not have a `ConnectionPool` as of today, but this is a feature high on our list. If |
| 36 | +you need a `ConnectionPool` today, please have a look at Vapor's [PostgresKit]. |
82 | 37 |
|
83 | 38 | ## API Docs |
84 | 39 |
|
85 | | -Check out the [PostgresNIO API docs](https://api.vapor.codes/postgres-nio/main/PostgresNIO/) for a detailed look at all of the classes, structs, protocols, and more. |
| 40 | +Check out the [PostgresNIO API docs](https://api.vapor.codes/postgres-nio/main/PostgresNIO/) for a |
| 41 | +detailed look at all of the classes, structs, protocols, and more. |
86 | 42 |
|
87 | | -## Getting Started |
| 43 | +## Getting started |
88 | 44 |
|
89 | | -This section will provide a quick look at using PostgresNIO. |
| 45 | +#### Adding the dependency |
90 | 46 |
|
91 | | -### Creating a Connection |
92 | | - |
93 | | -The first step to making a query is creating a new `PostgresConnection`. The minimum requirements to create one are a `SocketAddress` and `EventLoop`. |
| 47 | +Add `PostgresNIO` as dependency to your `Package.swift`: |
94 | 48 |
|
95 | 49 | ```swift |
96 | | -import PostgresNIO |
97 | | - |
98 | | -let eventLoop: EventLoop = ... |
99 | | -let conn = try PostgresConnection.connect( |
100 | | - to: .makeAddressResolvingHost("my.psql.server", port: 5432), |
101 | | - on: eventLoop |
102 | | -).wait() |
103 | | -defer { try! conn.close().wait() } |
| 50 | + dependencies: [ |
| 51 | + .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.8.0"), |
| 52 | + ... |
| 53 | + ] |
104 | 54 | ``` |
105 | 55 |
|
106 | | -Note: These examples will make use of `wait()` for simplicity. This is appropriate if you are using PostgresNIO on the main thread, like for a CLI tool or in tests. However, you should never use `wait()` on an event loop. |
107 | | - |
108 | | -There are a few ways to create a `SocketAddress`: |
109 | | - |
110 | | -- `init(ipAddress: String, port: Int)` |
111 | | -- `init(unixDomainSocketPath: String)` |
112 | | -- `makeAddressResolvingHost(_ host: String, port: Int)` |
113 | | - |
114 | | -There are also some additional arguments you can supply to `connect`. |
115 | | - |
116 | | -- `tlsConfiguration` An optional `TLSConfiguration` struct. If supplied, the PostgreSQL connection will be upgraded to use SSL. |
117 | | -- `serverHostname` An optional `String` to use in conjunction with `tlsConfiguration` to specify the server's hostname. |
118 | | - |
119 | | -`connect` will return a future `PostgresConnection`, or an error if it could not connect. Make sure you close the connection before it deinitializes. |
120 | | - |
121 | | -### Authentication |
122 | | - |
123 | | -Once you have a connection, you will need to authenticate with the server using the `authenticate` method. |
124 | | - |
| 56 | +Add `PostgresNIO` to the target you want to use it in: |
125 | 57 | ```swift |
126 | | -try conn.authenticate( |
127 | | - username: "your_username", |
128 | | - database: "your_database", |
129 | | - password: "your_password" |
130 | | -).wait() |
| 58 | + targets: [ |
| 59 | + .target(name: "MyFancyTarget", dependencies: [ |
| 60 | + .product(name: "PostgresNIO", package: "postgres-nio"), |
| 61 | + ]) |
| 62 | + ] |
131 | 63 | ``` |
132 | 64 |
|
133 | | -This requires a username. You may supply a database name and password if needed. |
| 65 | +#### Creating a connection |
134 | 66 |
|
135 | | -### Database Protocol |
136 | | - |
137 | | -Interaction with a server revolves around the `PostgresDatabase` protocol. This protocol includes methods like `query(_:)` for executing SQL queries and reading the resulting rows. |
138 | | - |
139 | | -`PostgresConnection` is the default implementation of `PostgresDatabase` provided by this package. Assume `db` here is the connection from the previous example. |
| 67 | +To create a connection, first create a connection configuration object: |
140 | 68 |
|
141 | 69 | ```swift |
142 | 70 | import PostgresNIO |
143 | 71 |
|
144 | | -let db: PostgresDatabase = ... |
145 | | -// now we can use client to do queries |
| 72 | +let config = PostgresConnection.Configuration( |
| 73 | + connection: .init( |
| 74 | + host: "localhost", |
| 75 | + port: 5432 |
| 76 | + ), |
| 77 | + authentication: .init( |
| 78 | + username: "my_username", |
| 79 | + database: "my_database", |
| 80 | + password: "my_password" |
| 81 | + ), |
| 82 | + tls: .disable |
| 83 | +) |
146 | 84 | ``` |
147 | 85 |
|
148 | | -### Simple Query |
| 86 | +A connection must be created on a SwiftNIO `EventLoop`. In most server use cases, an |
| 87 | +`EventLoopGroup` is created at app startup and closed during app shutdown. |
| 88 | + |
| 89 | +```swift |
| 90 | +import NIOPosix |
149 | 91 |
|
150 | | -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. |
| 92 | +let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) |
151 | 93 |
|
152 | | -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. |
| 94 | +// Much later |
| 95 | +try eventLoopGroup.syncShutdown() |
| 96 | +``` |
153 | 97 |
|
154 | | -`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. |
| 98 | +A `Logger` is also required. |
155 | 99 |
|
156 | 100 | ```swift |
157 | | -let rows = try db.simpleQuery("SELECT version()").wait() |
158 | | -print(rows) // [["version": "12.x.x"]] |
| 101 | +import Logging |
159 | 102 |
|
160 | | -try db.simpleQuery("SELECT version()") { row in |
161 | | - print(row) // ["version": "12.x.x"] |
162 | | -}.wait() |
| 103 | +let logger = Logger(label: "postgres-logger") |
163 | 104 | ``` |
164 | 105 |
|
165 | | -### Parameterized Query |
| 106 | +Now we can put it together: |
166 | 107 |
|
167 | | -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`. |
| 108 | +```swift |
| 109 | +import PostgresNIO |
| 110 | +import NIOPosix |
| 111 | +import Logging |
| 112 | + |
| 113 | +let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) |
| 114 | +let logger = Logger(label: "postgres-logger") |
| 115 | + |
| 116 | +let config = PostgresConnection.Configuration( |
| 117 | + connection: .init( |
| 118 | + host: "localhost", |
| 119 | + port: 5432 |
| 120 | + ), |
| 121 | + authentication: .init( |
| 122 | + username: "my_username", |
| 123 | + database: "my_database", |
| 124 | + password: "my_password" |
| 125 | + ), |
| 126 | + tls: .disable |
| 127 | +) |
| 128 | + |
| 129 | +let connection = try await PostgresConnection.connect( |
| 130 | + on eventLoop: eventLoopGroup.next(), |
| 131 | + configuration: config, |
| 132 | + id connectionID: 1, |
| 133 | + logger: logger |
| 134 | +) |
| 135 | + |
| 136 | +// Close your connection once done |
| 137 | +try await connection.close() |
| 138 | + |
| 139 | +// Shutdown the EventLoopGroup, once all connections are closed. |
| 140 | +try eventLoopGroup.syncShutdown() |
| 141 | +``` |
168 | 142 |
|
169 | | -These queries are most useful for selecting, inserting, and updating data. Data for these queries is transferred using the highly efficient binary format. |
| 143 | +#### Querying |
170 | 144 |
|
171 | | -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. |
| 145 | +Once a connection is established, queries can be sent to the server. This is very straightforward: |
172 | 146 |
|
173 | 147 | ```swift |
174 | | -let rows = try db.query("SELECT * FROM planets WHERE name = $1", ["Earth"]).wait() |
175 | | -print(rows) // [["id": 42, "name": "Earth"]] |
176 | | - |
177 | | -try db.query("SELECT * FROM planets WHERE name = $1", ["Earth"]) { row in |
178 | | - print(row) // ["id": 42, "name": "Earth"] |
179 | | -}.wait() |
| 148 | +let rows = try await connection.query("SELECT id, username, birthday FROM users", logger: logger) |
180 | 149 | ``` |
181 | 150 |
|
182 | | -### Rows and Data |
183 | | - |
184 | | -Both `simpleQuery` and `query` return the same `PostgresRow` type. Columns can be fetched from the row using the `column(_: String)` method. |
| 151 | +The query will return a [`PostgresRowSequence`], which is an AsyncSequence of [`PostgresRow`]s. The rows can be iterated one-by-one: |
185 | 152 |
|
186 | 153 | ```swift |
187 | | -let row: PostgresRow = ... |
188 | | -let version = row.column("version") |
189 | | -print(version) // PostgresData? |
| 154 | +for try await row in rows { |
| 155 | + // do something with the row |
| 156 | +} |
190 | 157 | ``` |
191 | 158 |
|
192 | | -`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: |
| 159 | +#### Decoding from PostgresRow |
193 | 160 |
|
194 | | -- Postgres column type |
195 | | -- Wire format: binary or text |
196 | | -- Value as array of bytes |
197 | | - |
198 | | -`PostgresData` has a variety of convenience methods for converting column data to usable Swift types. |
| 161 | +However, in most cases it is much easier to request a row's fields as a set of Swift types: |
199 | 162 |
|
200 | 163 | ```swift |
201 | | -let data: PostgresData= ... |
| 164 | +for try await (id, username, birthday) in rows.decode((Int, String, Date).self, context: .default) { |
| 165 | + // do something with the datatypes. |
| 166 | +} |
| 167 | +``` |
202 | 168 |
|
203 | | -print(data.string) // String? |
| 169 | +A type must implement the `PostgresDecodable` protocol in order to be decoded from a row. PostgresNIO provides default implementations for most of Swift's builtin types, as well as some types provided by Foundation: |
204 | 170 |
|
205 | | -// Postgres only supports signed Ints. |
206 | | -print(data.int) // Int? |
207 | | -print(data.int16) // Int16? |
208 | | -print(data.int32) // Int32? |
209 | | -print(data.int64) // Int64? |
| 171 | +- `Bool` |
| 172 | +- `Bytes`, `Data`, `ByteBuffer` |
| 173 | +- `Date` |
| 174 | +- `UInt8`, `Int16`, `Int32`, `Int64`, `Int` |
| 175 | +- `Float`, `Double` |
| 176 | +- `String` |
| 177 | +- `UUID` |
210 | 178 |
|
211 | | -// 'char' can be interpreted as a UInt8. |
212 | | -// It will show in db as a character though. |
213 | | -print(data.uint8) // UInt8? |
| 179 | +#### Querying with parameters |
214 | 180 |
|
215 | | -print(data.bool) // Bool? |
| 181 | +Sending parameterized queries to the database is also supported (in the coolest way possible): |
216 | 182 |
|
217 | | -print(try data.jsonb(as: Foo.self)) // Foo? |
| 183 | +```swift |
| 184 | +let id = 1 |
| 185 | +let username = "fancyuser" |
| 186 | +let birthday = Date() |
| 187 | +try await connection.query(""" |
| 188 | + INSERT INTO users (id, username, birthday) VALUES (\(id), \(username), \(birthday)) |
| 189 | + """, |
| 190 | + logger: logger |
| 191 | +) |
| 192 | +``` |
218 | 193 |
|
219 | | -print(data.float) // Float? |
220 | | -print(data.double) // Double? |
| 194 | +While this looks at first glance like a classic case of [SQL injection](https://en.wikipedia.org/wiki/SQL_injection) 😱, PostgresNIO's API ensures that this usage is safe. The first parameter of the `query(_:logger:)` method is not a plain `String`, but a `PostgresQuery`, which implements Swift's `ExpressibleByStringInterpolation` protocol. PostgresNIO uses the literal parts of the provided string as the SQL query and replaces each interpolated value with a parameter binding. Only values which implement the `PostgresEncodable` protocol may be interpolated in this way. As with `PostgresDecodable`, PostgresNIO provides default implementations for most common types. |
221 | 195 |
|
222 | | -print(data.date) // Date? |
223 | | -print(data.uuid) // UUID? |
| 196 | +Some queries do not receive any rows from the server (most often `INSERT`, `UPDATE`, and `DELETE` queries with no `RETURNING` clause, not to mention most DDL queries). To support this, the `query(_:logger:)` method is marked `@discardableResult`, so that the compiler does not issue a warning if the return value is not used. |
224 | 197 |
|
225 | | -print(data.numeric) // PostgresNumeric? |
226 | | -``` |
| 198 | +## Security |
| 199 | + |
| 200 | +Please see [SECURITY.md](https://github.com/vapor/.github/blob/main/SECURITY.md) for details on the security process. |
227 | 201 |
|
228 | | -`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. |
| 202 | +[EventLoopGroupConnectionPool]: https://github.com/vapor/async-kit/blob/main/Sources/AsyncKit/ConnectionPool/EventLoopGroupConnectionPool.swift |
| 203 | +[AsyncKit]: https://github.com/vapor/async-kit/ |
| 204 | +[SwiftNIO]: https://github.com/apple/swift-nio |
| 205 | +[SwiftLog]: https://github.com/apple/swift-log |
0 commit comments