├── .github ├── CODEOWNERS ├── contributing.md └── workflows │ ├── api-docs.yml │ └── test.yml ├── .gitignore ├── .spi.yml ├── Benchmarks ├── .gitignore ├── Benchmarks │ └── ConnectionPoolBenchmarks │ │ └── ConnectionPoolBenchmarks.swift └── Package.swift ├── LICENSE ├── NOTICE.txt ├── Package.swift ├── README.md ├── Snippets ├── Birthdays.swift └── PostgresClient.swift ├── Sources ├── ConnectionPoolModule │ ├── ConnectionIDGenerator.swift │ ├── ConnectionPool.swift │ ├── ConnectionPoolError.swift │ ├── ConnectionPoolObservabilityDelegate.swift │ ├── ConnectionRequest.swift │ ├── Max2Sequence.swift │ ├── NIOLock.swift │ ├── NIOLockedValueBox.swift │ ├── NoKeepAliveBehavior.swift │ ├── PoolStateMachine+ConnectionGroup.swift │ ├── PoolStateMachine+ConnectionState.swift │ ├── PoolStateMachine+RequestQueue.swift │ ├── PoolStateMachine.swift │ └── TinyFastSequence.swift ├── ConnectionPoolTestUtils │ ├── MockClock.swift │ ├── MockConnection.swift │ ├── MockConnectionFactory.swift │ ├── MockPingPongBehaviour.swift │ └── MockRequest.swift └── PostgresNIO │ ├── Connection │ ├── PostgresConnection+Configuration.swift │ ├── PostgresConnection.swift │ └── PostgresDatabase+PreparedQuery.swift │ ├── Data │ ├── PostgresData+Array.swift │ ├── PostgresData+Bool.swift │ ├── PostgresData+Bytes.swift │ ├── PostgresData+Date.swift │ ├── PostgresData+Decimal.swift │ ├── PostgresData+Double.swift │ ├── PostgresData+Float.swift │ ├── PostgresData+Int.swift │ ├── PostgresData+JSON.swift │ ├── PostgresData+JSONB.swift │ ├── PostgresData+Numeric.swift │ ├── PostgresData+Optional.swift │ ├── PostgresData+RawRepresentable.swift │ ├── PostgresData+Set.swift │ ├── PostgresData+String.swift │ ├── PostgresData+UUID.swift │ ├── PostgresData.swift │ ├── PostgresDataConvertible.swift │ ├── PostgresDataType.swift │ └── PostgresRow.swift │ ├── Deprecated │ ├── PostgresConnection+Configuration+Deprecated.swift │ ├── PostgresData+UInt.swift │ ├── PostgresMessage+Authentication.swift │ ├── PostgresMessage+Bind.swift │ ├── PostgresMessage+Close.swift │ ├── PostgresMessage+CommandComplete.swift │ ├── PostgresMessage+Describe.swift │ ├── PostgresMessage+Execute.swift │ ├── PostgresMessage+ParameterDescription.swift │ ├── PostgresMessage+ParameterStatus.swift │ ├── PostgresMessage+Parse.swift │ ├── PostgresMessage+Password.swift │ ├── PostgresMessage+ReadyForQuery.swift │ ├── PostgresMessage+SASLResponse.swift │ ├── PostgresMessage+SSLRequest.swift │ ├── PostgresMessage+SimpleQuery.swift │ ├── PostgresMessage+Startup.swift │ ├── PostgresMessage+Sync.swift │ ├── PostgresMessage+Terminate.swift │ ├── PostgresMessageDecoder.swift │ └── PostgresMessageEncoder.swift │ ├── Docs.docc │ ├── coding.md │ ├── deprecated.md │ ├── images │ │ └── vapor-postgresnio-logo.svg │ ├── index.md │ ├── listen.md │ ├── migrations.md │ ├── prepared-statement.md │ ├── running-queries.md │ └── theme-settings.json │ ├── Message │ ├── PostgresMessage+0.swift │ ├── PostgresMessage+BackendKeyData.swift │ ├── PostgresMessage+DataRow.swift │ ├── PostgresMessage+Error.swift │ ├── PostgresMessage+Identifier.swift │ ├── PostgresMessage+NotificationResponse.swift │ ├── PostgresMessage+RowDescription.swift │ └── PostgresMessageType.swift │ ├── New │ ├── Connection State Machine │ │ ├── AuthenticationStateMachine.swift │ │ ├── CloseStateMachine.swift │ │ ├── ConnectionStateMachine.swift │ │ ├── ExtendedQueryStateMachine.swift │ │ ├── ListenStateMachine.swift │ │ ├── PreparedStatementStateMachine.swift │ │ └── RowStreamStateMachine.swift │ ├── Data │ │ ├── Array+PostgresCodable.swift │ │ ├── Bool+PostgresCodable.swift │ │ ├── Bytes+PostgresCodable.swift │ │ ├── Date+PostgresCodable.swift │ │ ├── Decimal+PostgresCodable.swift │ │ ├── Float+PostgresCodable.swift │ │ ├── Int+PostgresCodable.swift │ │ ├── JSON+PostgresCodable.swift │ │ ├── Range+PostgresCodable.swift │ │ ├── RawRepresentable+PostgresCodable.swift │ │ ├── String+PostgresCodable.swift │ │ └── UUID+PostgresCodable.swift │ ├── Extensions │ │ ├── ByteBuffer+PSQL.swift │ │ └── Logging+PSQL.swift │ ├── Messages │ │ ├── Authentication.swift │ │ ├── BackendKeyData.swift │ │ ├── DataRow.swift │ │ ├── ErrorResponse.swift │ │ ├── NotificationResponse.swift │ │ ├── ParameterDescription.swift │ │ ├── ParameterStatus.swift │ │ ├── ReadyForQuery.swift │ │ └── RowDescription.swift │ ├── NotificationListener.swift │ ├── PSQLError.swift │ ├── PSQLEventsHandler.swift │ ├── PSQLPreparedStatement.swift │ ├── PSQLRowStream.swift │ ├── PSQLTask.swift │ ├── PostgresBackendMessage.swift │ ├── PostgresBackendMessageDecoder.swift │ ├── PostgresCell.swift │ ├── PostgresChannelHandler.swift │ ├── PostgresCodable.swift │ ├── PostgresFrontendMessageEncoder.swift │ ├── PostgresNotificationSequence.swift │ ├── PostgresQuery.swift │ ├── PostgresRowSequence.swift │ ├── PostgresTransactionError.swift │ ├── PreparedStatement.swift │ └── VariadicGenerics.swift │ ├── Pool │ ├── ConnectionFactory.swift │ ├── PostgresClient.swift │ └── PostgresClientMetrics.swift │ ├── Postgres+PSQLCompat.swift │ ├── PostgresDatabase+Query.swift │ ├── PostgresDatabase+SimpleQuery.swift │ ├── PostgresDatabase.swift │ ├── PostgresRequest.swift │ └── Utilities │ ├── Exports.swift │ ├── NIOUtils.swift │ ├── PostgresError+Code.swift │ ├── PostgresError.swift │ ├── PostgresJSONDecoder.swift │ ├── PostgresJSONEncoder.swift │ ├── SASLAuthentication+SCRAM-SHA256.swift │ └── SASLAuthenticationManager.swift ├── Tests ├── ConnectionPoolModuleTests │ ├── ConnectionIDGeneratorTests.swift │ ├── ConnectionPoolTests.swift │ ├── ConnectionRequestTests.swift │ ├── Max2SequenceTests.swift │ ├── Mocks │ │ └── MockTimerCancellationToken.swift │ ├── NoKeepAliveBehaviorTests.swift │ ├── PoolStateMachine+ConnectionGroupTests.swift │ ├── PoolStateMachine+ConnectionStateTests.swift │ ├── PoolStateMachine+RequestQueueTests.swift │ ├── PoolStateMachineTests.swift │ ├── TinyFastSequenceTests.swift │ └── Utils │ │ └── Future.swift ├── IntegrationTests │ ├── AsyncTests.swift │ ├── PSQLIntegrationTests.swift │ ├── PerformanceTests.swift │ ├── PostgresClientTests.swift │ ├── PostgresNIOTests.swift │ └── Utilities.swift └── PostgresNIOTests │ ├── Data │ └── PostgresData+JSONTests.swift │ ├── Message │ └── PostgresMessageDecoderTests.swift │ ├── New │ ├── Connection State Machine │ │ ├── AuthenticationStateMachineTests.swift │ │ ├── ConnectionStateMachineTests.swift │ │ ├── ExtendedQueryStateMachineTests.swift │ │ ├── PrepareStatementStateMachineTests.swift │ │ └── PreparedStatementStateMachineTests.swift │ ├── Data │ │ ├── Array+PSQLCodableTests.swift │ │ ├── Bool+PSQLCodableTests.swift │ │ ├── Bytes+PSQLCodableTests.swift │ │ ├── Date+PSQLCodableTests.swift │ │ ├── Decimal+PSQLCodableTests.swift │ │ ├── Float+PSQLCodableTests.swift │ │ ├── Int+PSQLCodableTests.swift │ │ ├── JSON+PSQLCodableTests.swift │ │ ├── Range+PSQLCodableTests.swift │ │ ├── RawRepresentable+PSQLCodableTests.swift │ │ ├── String+PSQLCodableTests.swift │ │ └── UUID+PSQLCodableTests.swift │ ├── Extensions │ │ ├── ByteBuffer+Utils.swift │ │ ├── ConnectionAction+TestUtils.swift │ │ ├── PSQLBackendMessageEncoder.swift │ │ ├── PSQLFrontendMessageDecoder.swift │ │ ├── PostgresFrontendMessage.swift │ │ ├── ReverseByteToMessageHandler.swift │ │ └── ReverseMessageToByteHandler.swift │ ├── Messages │ │ ├── AuthenticationTests.swift │ │ ├── BackendKeyDataTests.swift │ │ ├── BindTests.swift │ │ ├── CancelTests.swift │ │ ├── CloseTests.swift │ │ ├── DataRowTests.swift │ │ ├── DescribeTests.swift │ │ ├── ErrorResponseTests.swift │ │ ├── ExecuteTests.swift │ │ ├── NotificationResponseTests.swift │ │ ├── ParameterDescriptionTests.swift │ │ ├── ParameterStatusTests.swift │ │ ├── ParseTests.swift │ │ ├── PasswordTests.swift │ │ ├── ReadyForQueryTests.swift │ │ ├── RowDescriptionTests.swift │ │ ├── SASLInitialResponseTests.swift │ │ ├── SASLResponseTests.swift │ │ ├── SSLRequestTests.swift │ │ └── StartupTests.swift │ ├── PSQLBackendMessageTests.swift │ ├── PSQLFrontendMessageTests.swift │ ├── PSQLMetadataTests.swift │ ├── PSQLRowStreamTests.swift │ ├── PostgresCellTests.swift │ ├── PostgresChannelHandlerTests.swift │ ├── PostgresCodableTests.swift │ ├── PostgresConnectionTests.swift │ ├── PostgresErrorTests.swift │ ├── PostgresQueryTests.swift │ ├── PostgresRowSequenceTests.swift │ └── PostgresRowTests.swift │ ├── Utilities.swift │ └── Utilities │ └── PostgresJSONCodingTests.swift ├── dev ├── generate-postgresrow-multi-decode.sh └── generate-postgresrowsequence-multi-decode.sh └── docker-compose.yml /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @fabianfett @gwynne 2 | -------------------------------------------------------------------------------- /.github/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to PostgresNIO 2 | 3 | 👋 Welcome to the Vapor team! 4 | 5 | ## Testing 6 | 7 | To run this package's tests, you need to start a local Postgres database. The easiest way to do this is using Docker. 8 | 9 | If you have Docker installed and running, you can use the `docker-compose` included with this package. The following command will download the required files and boot a local Postgres server: 10 | 11 | ```fish 12 | docker-compose up psql-12 13 | ``` 14 | 15 | Run this in the project's root folder (where the `docker-compose.yml` file is). Check out that file to see the other versions of Postgres you can test against. 16 | 17 | Once you have a server running, you can run the test suite from Xcode by hitting `CMD+u` or from the command line: 18 | 19 | ```fish 20 | swift test 21 | ``` 22 | 23 | Make sure to add tests for any new code you write. 24 | 25 | ---------- 26 | 27 | Join us on Discord if you have any questions: [http://vapor.team](http://vapor.team). 28 | 29 | — Thanks! 🙌 30 | -------------------------------------------------------------------------------- /.github/workflows/api-docs.yml: -------------------------------------------------------------------------------- 1 | name: deploy-api-docs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | build-and-deploy: 9 | uses: vapor/api-docs/.github/workflows/build-and-deploy-docs-workflow.yml@main 10 | secrets: inherit 11 | with: 12 | package_name: postgres-nio 13 | modules: PostgresNIO 14 | pathsToInvalidate: /postgresnio/* 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | DerivedData 6 | Package.resolved 7 | .swiftpm 8 | Tests/LinuxMain.swift 9 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | external_links: 3 | documentation: "https://api.vapor.codes/postgresnio/documentation/postgresnio/" 4 | 5 | -------------------------------------------------------------------------------- /Benchmarks/.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | -------------------------------------------------------------------------------- /Benchmarks/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 6.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "benchmarks", 7 | platforms: [ 8 | .macOS("14") 9 | ], 10 | dependencies: [ 11 | .package(path: "../"), 12 | .package(url: "https://github.com/ordo-one/package-benchmark.git", from: "1.29.0"), 13 | ], 14 | targets: [ 15 | .executableTarget( 16 | name: "ConnectionPoolBenchmarks", 17 | dependencies: [ 18 | .product(name: "_ConnectionPoolModule", package: "postgres-nio"), 19 | .product(name: "_ConnectionPoolTestUtils", package: "postgres-nio"), 20 | .product(name: "Benchmark", package: "package-benchmark"), 21 | ], 22 | path: "Benchmarks/ConnectionPoolBenchmarks", 23 | plugins: [ 24 | .plugin(name: "BenchmarkPlugin", package: "package-benchmark") 25 | ] 26 | ), 27 | ] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Tanner Nelson 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. 22 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | //===----------------------------------------------------------------------===// 2 | // 3 | // This source file is part of the Vapor open source project 4 | // 5 | // Copyright (c) 2017-2023 Vapor project authors 6 | // Licensed under MIT 7 | // 8 | // See LICENSE for license information 9 | // 10 | // SPDX-License-Identifier: MIT 11 | // 12 | //===----------------------------------------------------------------------===// 13 | 14 | This product contains a derivation of the NIOLock implementation 15 | from Swift NIO. 16 | 17 | * LICENSE (Apache License 2.0): 18 | * https://www.apache.org/licenses/LICENSE-2.0 19 | * HOMEPAGE: 20 | * https://github.com/apple/swift-nio 21 | -------------------------------------------------------------------------------- /Snippets/Birthdays.swift: -------------------------------------------------------------------------------- 1 | import PostgresNIO 2 | import Foundation 3 | 4 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 5 | enum Birthday { 6 | static func main() async throws { 7 | // 1. Create a configuration to match server's parameters 8 | let config = PostgresClient.Configuration( 9 | host: "localhost", 10 | port: 5432, 11 | username: "test_username", 12 | password: "test_password", 13 | database: "test_database", 14 | tls: .disable 15 | ) 16 | 17 | // 2. Create a client 18 | let client = PostgresClient(configuration: config) 19 | 20 | // 3. Run the client 21 | try await withThrowingTaskGroup(of: Void.self) { taskGroup in 22 | taskGroup.addTask { 23 | await client.run() // !important 24 | } 25 | 26 | // 4. Create a friends table to store data into 27 | try await client.query(""" 28 | CREATE TABLE IF NOT EXISTS "friends" ( 29 | id SERIAL PRIMARY KEY, 30 | given_name TEXT, 31 | last_name TEXT, 32 | birthday TIMESTAMP WITH TIME ZONE 33 | ) 34 | """ 35 | ) 36 | 37 | // 5. Create a Swift friend representation 38 | struct Friend { 39 | var firstName: String 40 | var lastName: String 41 | var birthday: Date 42 | } 43 | 44 | // 6. Create John Appleseed with special birthday 45 | let dateFormatter = DateFormatter() 46 | dateFormatter.dateFormat = "yyyy-MM-dd" 47 | let johnsBirthday = dateFormatter.date(from: "1960-09-26")! 48 | let friend = Friend(firstName: "Hans", lastName: "Müller", birthday: johnsBirthday) 49 | 50 | // 7. Store friend into the database 51 | try await client.query(""" 52 | INSERT INTO "friends" (given_name, last_name, birthday) 53 | VALUES 54 | (\(friend.firstName), \(friend.lastName), \(friend.birthday)); 55 | """ 56 | ) 57 | 58 | // 8. Query database for the friend we just inserted 59 | let rows = try await client.query(""" 60 | SELECT id, given_name, last_name, birthday FROM "friends" WHERE given_name = \(friend.firstName) 61 | """ 62 | ) 63 | 64 | // 9. Iterate the returned rows, decoding the rows into Swift primitives 65 | for try await (id, firstName, lastName, birthday) in rows.decode((Int, String, String, Date).self) { 66 | print("\(id) | \(firstName) \(lastName), \(birthday)") 67 | } 68 | 69 | // 10. Shutdown the client, by cancelling its run method, through cancelling the taskGroup. 70 | taskGroup.cancelAll() 71 | } 72 | } 73 | } 74 | 75 | -------------------------------------------------------------------------------- /Snippets/PostgresClient.swift: -------------------------------------------------------------------------------- 1 | import PostgresNIO 2 | import struct Foundation.UUID 3 | 4 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 5 | enum Runner { 6 | static func main() async throws { 7 | 8 | // snippet.configuration 9 | let config = PostgresClient.Configuration( 10 | host: "localhost", 11 | port: 5432, 12 | username: "my_username", 13 | password: "my_password", 14 | database: "my_database", 15 | tls: .disable 16 | ) 17 | // snippet.end 18 | 19 | // snippet.makeClient 20 | let client = PostgresClient(configuration: config) 21 | // snippet.end 22 | 23 | } 24 | 25 | static func runAndCancel(client: PostgresClient) async { 26 | // snippet.run 27 | await withTaskGroup(of: Void.self) { taskGroup in 28 | taskGroup.addTask { 29 | await client.run() // !important 30 | } 31 | 32 | // You can use the client while the `client.run()` method is not cancelled. 33 | 34 | // To shutdown the client, cancel its run method, by cancelling the taskGroup. 35 | taskGroup.cancelAll() 36 | } 37 | // snippet.end 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/ConnectionIDGenerator.swift: -------------------------------------------------------------------------------- 1 | import Atomics 2 | 3 | public struct ConnectionIDGenerator: ConnectionIDGeneratorProtocol { 4 | static let globalGenerator = ConnectionIDGenerator() 5 | 6 | private let atomic: ManagedAtomic 7 | 8 | public init() { 9 | self.atomic = .init(0) 10 | } 11 | 12 | public func next() -> Int { 13 | return self.atomic.loadThenWrappingIncrement(ordering: .relaxed) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/ConnectionPoolError.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct ConnectionPoolError: Error, Hashable { 3 | enum Base: Error, Hashable { 4 | case requestCancelled 5 | case poolShutdown 6 | } 7 | 8 | private let base: Base 9 | 10 | init(_ base: Base) { self.base = base } 11 | 12 | /// The connection requests got cancelled 13 | public static let requestCancelled = ConnectionPoolError(.requestCancelled) 14 | /// The connection requests can't be fulfilled as the pool has already been shutdown 15 | public static let poolShutdown = ConnectionPoolError(.poolShutdown) 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/ConnectionPoolObservabilityDelegate.swift: -------------------------------------------------------------------------------- 1 | 2 | public protocol ConnectionPoolObservabilityDelegate: Sendable { 3 | associatedtype ConnectionID: Hashable & Sendable 4 | 5 | /// The connection with the given ID has started trying to establish a connection. The outcome 6 | /// of the connection will be reported as either ``connectSucceeded(id:streamCapacity:)`` or 7 | /// ``connectFailed(id:error:)``. 8 | func startedConnecting(id: ConnectionID) 9 | 10 | /// A connection attempt failed with the given error. After some period of 11 | /// time ``startedConnecting(id:)`` may be called again. 12 | func connectFailed(id: ConnectionID, error: Error) 13 | 14 | /// A connection was established on the connection with the given ID. `streamCapacity` streams are 15 | /// available to use on the connection. The maximum number of available streams may change over 16 | /// time and is reported via ````. The 17 | func connectSucceeded(id: ConnectionID, streamCapacity: UInt16) 18 | 19 | /// The utlization of the connection changed; a stream may have been used, returned or the 20 | /// maximum number of concurrent streams available on the connection changed. 21 | func connectionUtilizationChanged(id:ConnectionID, streamsUsed: UInt16, streamCapacity: UInt16) 22 | 23 | func keepAliveTriggered(id: ConnectionID) 24 | 25 | func keepAliveSucceeded(id: ConnectionID) 26 | 27 | func keepAliveFailed(id: ConnectionID, error: Error) 28 | 29 | /// The remote peer is quiescing the connection: no new streams will be created on it. The 30 | /// connection will eventually be closed and removed from the pool. 31 | func connectionClosing(id: ConnectionID) 32 | 33 | /// The connection was closed. The connection may be established again in the future (notified 34 | /// via ``startedConnecting(id:)``). 35 | func connectionClosed(id: ConnectionID, error: Error?) 36 | 37 | func requestQueueDepthChanged(_ newDepth: Int) 38 | } 39 | 40 | public struct NoOpConnectionPoolMetrics: ConnectionPoolObservabilityDelegate { 41 | public init(connectionIDType: ConnectionID.Type) {} 42 | 43 | public func startedConnecting(id: ConnectionID) {} 44 | 45 | public func connectFailed(id: ConnectionID, error: Error) {} 46 | 47 | public func connectSucceeded(id: ConnectionID, streamCapacity: UInt16) {} 48 | 49 | public func connectionUtilizationChanged(id: ConnectionID, streamsUsed: UInt16, streamCapacity: UInt16) {} 50 | 51 | public func keepAliveTriggered(id: ConnectionID) {} 52 | 53 | public func keepAliveSucceeded(id: ConnectionID) {} 54 | 55 | public func keepAliveFailed(id: ConnectionID, error: Error) {} 56 | 57 | public func connectionClosing(id: ConnectionID) {} 58 | 59 | public func connectionClosed(id: ConnectionID, error: Error?) {} 60 | 61 | public func requestQueueDepthChanged(_ newDepth: Int) {} 62 | } 63 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/ConnectionRequest.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct ConnectionRequest: ConnectionRequestProtocol { 3 | public typealias ID = Int 4 | 5 | public var id: ID 6 | 7 | @usableFromInline 8 | private(set) var continuation: CheckedContinuation 9 | 10 | @inlinable 11 | init( 12 | id: Int, 13 | continuation: CheckedContinuation 14 | ) { 15 | self.id = id 16 | self.continuation = continuation 17 | } 18 | 19 | public func complete(with result: Result) { 20 | self.continuation.resume(with: result) 21 | } 22 | } 23 | 24 | @usableFromInline 25 | let requestIDGenerator = _ConnectionPoolModule.ConnectionIDGenerator() 26 | 27 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 28 | extension ConnectionPool where Request == ConnectionRequest { 29 | public convenience init( 30 | configuration: ConnectionPoolConfiguration, 31 | idGenerator: ConnectionIDGenerator = _ConnectionPoolModule.ConnectionIDGenerator(), 32 | keepAliveBehavior: KeepAliveBehavior, 33 | observabilityDelegate: ObservabilityDelegate, 34 | clock: Clock = ContinuousClock(), 35 | connectionFactory: @escaping ConnectionFactory 36 | ) { 37 | self.init( 38 | configuration: configuration, 39 | idGenerator: idGenerator, 40 | requestType: ConnectionRequest.self, 41 | keepAliveBehavior: keepAliveBehavior, 42 | observabilityDelegate: observabilityDelegate, 43 | clock: clock, 44 | connectionFactory: connectionFactory 45 | ) 46 | } 47 | 48 | @inlinable 49 | public func leaseConnection() async throws -> Connection { 50 | let requestID = requestIDGenerator.next() 51 | 52 | let connection = try await withTaskCancellationHandler { 53 | if Task.isCancelled { 54 | throw CancellationError() 55 | } 56 | 57 | return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 58 | let request = Request( 59 | id: requestID, 60 | continuation: continuation 61 | ) 62 | 63 | self.leaseConnection(request) 64 | } 65 | } onCancel: { 66 | self.cancelLeaseConnection(requestID) 67 | } 68 | 69 | return connection 70 | } 71 | 72 | @inlinable 73 | public func withConnection(_ closure: (Connection) async throws -> Result) async throws -> Result { 74 | let connection = try await self.leaseConnection() 75 | defer { self.releaseConnection(connection) } 76 | return try await closure(connection) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/Max2Sequence.swift: -------------------------------------------------------------------------------- 1 | // A `Sequence` that can contain at most two elements. However it does not heap allocate. 2 | @usableFromInline 3 | struct Max2Sequence: Sequence { 4 | @usableFromInline 5 | private(set) var first: Element? 6 | @usableFromInline 7 | private(set) var second: Element? 8 | 9 | @inlinable 10 | var count: Int { 11 | if self.first == nil { return 0 } 12 | if self.second == nil { return 1 } 13 | return 2 14 | } 15 | 16 | @inlinable 17 | var isEmpty: Bool { 18 | self.first == nil 19 | } 20 | 21 | @inlinable 22 | init(_ first: Element?, _ second: Element? = nil) { 23 | if let first = first { 24 | self.first = first 25 | self.second = second 26 | } else { 27 | self.first = second 28 | self.second = nil 29 | } 30 | } 31 | 32 | @inlinable 33 | init() { 34 | self.first = nil 35 | self.second = nil 36 | } 37 | 38 | @inlinable 39 | func makeIterator() -> Iterator { 40 | Iterator(first: self.first, second: self.second) 41 | } 42 | 43 | @usableFromInline 44 | struct Iterator: IteratorProtocol { 45 | @usableFromInline 46 | let first: Element? 47 | @usableFromInline 48 | let second: Element? 49 | 50 | @usableFromInline 51 | private(set) var index: UInt8 = 0 52 | 53 | @inlinable 54 | init(first: Element?, second: Element?) { 55 | self.first = first 56 | self.second = second 57 | self.index = 0 58 | } 59 | 60 | @inlinable 61 | mutating func next() -> Element? { 62 | switch self.index { 63 | case 0: 64 | self.index += 1 65 | return self.first 66 | case 1: 67 | self.index += 1 68 | return self.second 69 | default: 70 | return nil 71 | } 72 | } 73 | } 74 | 75 | @inlinable 76 | mutating func append(_ element: Element) { 77 | precondition(self.second == nil) 78 | if self.first == nil { 79 | self.first = element 80 | } else if self.second == nil { 81 | self.second = element 82 | } else { 83 | fatalError("Max2Sequence can only hold two Elements.") 84 | } 85 | } 86 | 87 | @inlinable 88 | func map(_ transform: (Element) throws -> (NewElement)) rethrows -> Max2Sequence { 89 | try Max2Sequence(self.first.flatMap(transform), self.second.flatMap(transform)) 90 | } 91 | } 92 | 93 | extension Max2Sequence: ExpressibleByArrayLiteral { 94 | @inlinable 95 | init(arrayLiteral elements: Element...) { 96 | precondition(elements.count <= 2) 97 | var iterator = elements.makeIterator() 98 | self.init(iterator.next(), iterator.next()) 99 | } 100 | } 101 | 102 | extension Max2Sequence: Equatable where Element: Equatable {} 103 | extension Max2Sequence: Hashable where Element: Hashable {} 104 | extension Max2Sequence: Sendable where Element: Sendable {} 105 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/NIOLockedValueBox.swift: -------------------------------------------------------------------------------- 1 | // Implementation vendored from SwiftNIO: 2 | // https://github.com/apple/swift-nio 3 | 4 | //===----------------------------------------------------------------------===// 5 | // 6 | // This source file is part of the SwiftNIO open source project 7 | // 8 | // Copyright (c) 2022 Apple Inc. and the SwiftNIO project authors 9 | // Licensed under Apache License v2.0 10 | // 11 | // See LICENSE.txt for license information 12 | // See CONTRIBUTORS.txt for the list of SwiftNIO project authors 13 | // 14 | // SPDX-License-Identifier: Apache-2.0 15 | // 16 | //===----------------------------------------------------------------------===// 17 | 18 | /// Provides locked access to `Value`. 19 | /// 20 | /// - Note: ``NIOLockedValueBox`` has reference semantics and holds the `Value` 21 | /// alongside a lock behind a reference. 22 | /// 23 | /// This is no different than creating a ``Lock`` and protecting all 24 | /// accesses to a value using the lock. But it's easy to forget to actually 25 | /// acquire/release the lock in the correct place. ``NIOLockedValueBox`` makes 26 | /// that much easier. 27 | @usableFromInline 28 | struct NIOLockedValueBox { 29 | 30 | @usableFromInline 31 | internal let _storage: LockStorage 32 | 33 | /// Initialize the `Value`. 34 | @inlinable 35 | init(_ value: Value) { 36 | self._storage = .create(value: value) 37 | } 38 | 39 | /// Access the `Value`, allowing mutation of it. 40 | @inlinable 41 | func withLockedValue(_ mutate: (inout Value) throws -> T) rethrows -> T { 42 | try self._storage.withLockedValue(mutate) 43 | } 44 | 45 | /// Provides an unsafe view over the lock and its value. 46 | /// 47 | /// This can be beneficial when you require fine grained control over the lock in some 48 | /// situations but don't want lose the benefits of ``withLockedValue(_:)`` in others by 49 | /// switching to ``NIOLock``. 50 | var unsafe: Unsafe { 51 | Unsafe(_storage: self._storage) 52 | } 53 | 54 | /// Provides an unsafe view over the lock and its value. 55 | struct Unsafe { 56 | @usableFromInline 57 | let _storage: LockStorage 58 | 59 | /// Manually acquire the lock. 60 | @inlinable 61 | func lock() { 62 | self._storage.lock() 63 | } 64 | 65 | /// Manually release the lock. 66 | @inlinable 67 | func unlock() { 68 | self._storage.unlock() 69 | } 70 | 71 | /// Mutate the value, assuming the lock has been acquired manually. 72 | /// 73 | /// - Parameter mutate: A closure with scoped access to the value. 74 | /// - Returns: The result of the `mutate` closure. 75 | @inlinable 76 | func withValueAssumingLockIsAcquired( 77 | _ mutate: (_ value: inout Value) throws -> Result 78 | ) rethrows -> Result { 79 | try self._storage.withUnsafeMutablePointerToHeader { value in 80 | try mutate(&value.pointee) 81 | } 82 | } 83 | } 84 | } 85 | 86 | extension NIOLockedValueBox: @unchecked Sendable where Value: Sendable {} 87 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/NoKeepAliveBehavior.swift: -------------------------------------------------------------------------------- 1 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 2 | public struct NoOpKeepAliveBehavior: ConnectionKeepAliveBehavior { 3 | public var keepAliveFrequency: Duration? { nil } 4 | 5 | public func runKeepAlive(for connection: Connection) async throws {} 6 | 7 | public init(connectionType: Connection.Type) {} 8 | } 9 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolModule/PoolStateMachine+RequestQueue.swift: -------------------------------------------------------------------------------- 1 | import DequeModule 2 | 3 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 4 | extension PoolStateMachine { 5 | 6 | /// A request queue, which can enqueue requests in O(1), dequeue requests in O(1) and even cancel requests in O(1). 7 | /// 8 | /// While enqueueing and dequeueing on O(1) is trivial, cancellation is hard, as it normally requires a removal within the 9 | /// underlying Deque. However thanks to having an additional `requests` dictionary, we can remove the cancelled 10 | /// request from the dictionary and keep it inside the queue. Whenever we pop a request from the deque, we validate 11 | /// that it hasn't been cancelled in the meantime by checking if the popped request is still in the `requests` dictionary. 12 | @usableFromInline 13 | struct RequestQueue: Sendable { 14 | @usableFromInline 15 | private(set) var queue: Deque 16 | 17 | @usableFromInline 18 | private(set) var requests: [RequestID: Request] 19 | 20 | @inlinable 21 | var count: Int { 22 | self.requests.count 23 | } 24 | 25 | @inlinable 26 | var isEmpty: Bool { 27 | self.count == 0 28 | } 29 | 30 | @usableFromInline 31 | init() { 32 | self.queue = .init(minimumCapacity: 256) 33 | self.requests = .init(minimumCapacity: 256) 34 | } 35 | 36 | @inlinable 37 | mutating func queue(_ request: Request) { 38 | self.requests[request.id] = request 39 | self.queue.append(request.id) 40 | } 41 | 42 | @inlinable 43 | mutating func pop(max: UInt16) -> TinyFastSequence { 44 | var result = TinyFastSequence() 45 | result.reserveCapacity(Int(max)) 46 | var popped = 0 47 | while popped < max, let requestID = self.queue.popFirst() { 48 | if let requestIndex = self.requests.index(forKey: requestID) { 49 | popped += 1 50 | result.append(self.requests.remove(at: requestIndex).value) 51 | } 52 | } 53 | 54 | assert(result.count <= max) 55 | return result 56 | } 57 | 58 | @inlinable 59 | mutating func remove(_ requestID: RequestID) -> Request? { 60 | self.requests.removeValue(forKey: requestID) 61 | } 62 | 63 | @inlinable 64 | mutating func removeAll() -> TinyFastSequence { 65 | let result = TinyFastSequence(self.requests.values) 66 | self.requests.removeAll() 67 | self.queue.removeAll() 68 | return result 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolTestUtils/MockPingPongBehaviour.swift: -------------------------------------------------------------------------------- 1 | import _ConnectionPoolModule 2 | import DequeModule 3 | import NIOConcurrencyHelpers 4 | 5 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 6 | public final class MockPingPongBehavior: ConnectionKeepAliveBehavior { 7 | public let keepAliveFrequency: Duration? 8 | 9 | let stateBox = NIOLockedValueBox(State()) 10 | 11 | struct State { 12 | var runs = Deque<(Connection, CheckedContinuation)>() 13 | 14 | var waiter = Deque), Never>>() 15 | } 16 | 17 | public init(keepAliveFrequency: Duration?, connectionType: Connection.Type) { 18 | self.keepAliveFrequency = keepAliveFrequency 19 | } 20 | 21 | public func runKeepAlive(for connection: Connection) async throws { 22 | precondition(self.keepAliveFrequency != nil) 23 | 24 | // we currently don't support cancellation when creating a connection 25 | let success = try await withCheckedThrowingContinuation { (checkedContinuation: CheckedContinuation) -> () in 26 | let waiter = self.stateBox.withLockedValue { state -> (CheckedContinuation<(Connection, CheckedContinuation), Never>)? in 27 | if let waiter = state.waiter.popFirst() { 28 | return waiter 29 | } else { 30 | state.runs.append((connection, checkedContinuation)) 31 | return nil 32 | } 33 | } 34 | 35 | if let waiter { 36 | waiter.resume(returning: (connection, checkedContinuation)) 37 | } 38 | } 39 | 40 | precondition(success) 41 | } 42 | 43 | @discardableResult 44 | public func nextKeepAlive(_ closure: (Connection) async throws -> Bool) async rethrows -> Connection { 45 | let (connection, continuation) = await withCheckedContinuation { (continuation: CheckedContinuation<(Connection, CheckedContinuation), Never>) in 46 | let run = self.stateBox.withLockedValue { state -> (Connection, CheckedContinuation)? in 47 | if let run = state.runs.popFirst() { 48 | return run 49 | } else { 50 | state.waiter.append(continuation) 51 | return nil 52 | } 53 | } 54 | 55 | if let run { 56 | continuation.resume(returning: run) 57 | } 58 | } 59 | 60 | do { 61 | let success = try await closure(connection) 62 | 63 | continuation.resume(returning: success) 64 | return connection 65 | } catch { 66 | continuation.resume(throwing: error) 67 | throw error 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/ConnectionPoolTestUtils/MockRequest.swift: -------------------------------------------------------------------------------- 1 | import _ConnectionPoolModule 2 | 3 | public final class MockRequest: ConnectionRequestProtocol, Hashable, Sendable { 4 | public typealias Connection = MockConnection 5 | 6 | public struct ID: Hashable, Sendable { 7 | var objectID: ObjectIdentifier 8 | 9 | init(_ request: MockRequest) { 10 | self.objectID = ObjectIdentifier(request) 11 | } 12 | } 13 | 14 | public init() {} 15 | 16 | public var id: ID { ID(self) } 17 | 18 | public static func ==(lhs: MockRequest, rhs: MockRequest) -> Bool { 19 | lhs.id == rhs.id 20 | } 21 | 22 | public func hash(into hasher: inout Hasher) { 23 | hasher.combine(self.id) 24 | } 25 | 26 | public func complete(with: Result) { 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Connection/PostgresDatabase+PreparedQuery.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import NIOConcurrencyHelpers 3 | import struct Foundation.UUID 4 | 5 | extension PostgresDatabase { 6 | public func prepare(query: String) -> EventLoopFuture { 7 | let name = "nio-postgres-\(UUID().uuidString)" 8 | let request = PrepareQueryRequest(query, as: name) 9 | return self.send(PostgresCommands.prepareQuery(request: request), logger: self.logger).map { _ in 10 | // we can force unwrap the prepared here, since in a success case it must be set 11 | // in the send method of `PostgresDatabase`. We do this dirty trick to work around 12 | // the fact that the send method only returns an `EventLoopFuture`. 13 | // Eventually we should move away from the `PostgresDatabase.send` API. 14 | request.prepared! 15 | } 16 | } 17 | 18 | @preconcurrency 19 | public func prepare(query: String, handler: @Sendable @escaping (PreparedQuery) -> EventLoopFuture<[[PostgresRow]]>) -> EventLoopFuture<[[PostgresRow]]> { 20 | prepare(query: query) 21 | .flatMap { preparedQuery in 22 | handler(preparedQuery) 23 | .flatMap { results in 24 | preparedQuery.deallocate().map { results } 25 | } 26 | } 27 | } 28 | } 29 | 30 | 31 | public struct PreparedQuery: Sendable { 32 | let underlying: PSQLPreparedStatement 33 | let database: PostgresDatabase 34 | 35 | init(underlying: PSQLPreparedStatement, database: PostgresDatabase) { 36 | self.underlying = underlying 37 | self.database = database 38 | } 39 | 40 | public func execute(_ binds: [PostgresData] = []) -> EventLoopFuture<[PostgresRow]> { 41 | let rowsBoxed = NIOLockedValueBox([PostgresRow]()) 42 | return self.execute(binds) { row in 43 | rowsBoxed.withLockedValue { 44 | $0.append(row) 45 | } 46 | }.map { rowsBoxed.withLockedValue { $0 } } 47 | } 48 | 49 | @preconcurrency 50 | public func execute(_ binds: [PostgresData] = [], _ onRow: @Sendable @escaping (PostgresRow) throws -> ()) -> EventLoopFuture { 51 | let command = PostgresCommands.executePreparedStatement(query: self, binds: binds, onRow: onRow) 52 | return self.database.send(command, logger: self.database.logger) 53 | } 54 | 55 | public func deallocate() -> EventLoopFuture { 56 | self.underlying.connection.close(.preparedStatement(self.underlying.name), logger: self.database.logger) 57 | } 58 | } 59 | 60 | final class PrepareQueryRequest: Sendable { 61 | let query: String 62 | let name: String 63 | var prepared: PreparedQuery? { 64 | get { 65 | self._prepared.withLockedValue { $0 } 66 | } 67 | set { 68 | self._prepared.withLockedValue { 69 | $0 = newValue 70 | } 71 | } 72 | } 73 | let _prepared: NIOLockedValueBox = .init(nil) 74 | 75 | init(_ query: String, as name: String) { 76 | self.query = query 77 | self.name = name 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Bool.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresData { 4 | public init(bool: Bool) { 5 | var buffer = ByteBufferAllocator().buffer(capacity: 1) 6 | buffer.writeInteger(bool ? 1 : 0, as: UInt8.self) 7 | self.init(type: .bool, formatCode: .binary, value: buffer) 8 | } 9 | 10 | public var bool: Bool? { 11 | guard var value = self.value else { 12 | return nil 13 | } 14 | guard value.readableBytes == 1 else { 15 | return nil 16 | } 17 | guard let byte = value.readInteger(as: UInt8.self) else { 18 | return nil 19 | } 20 | 21 | switch self.formatCode { 22 | case .text: 23 | switch byte { 24 | case Character("t").asciiValue!: 25 | return true 26 | case Character("f").asciiValue!: 27 | return false 28 | default: 29 | return nil 30 | } 31 | case .binary: 32 | switch byte { 33 | case 1: 34 | return true 35 | case 0: 36 | return false 37 | default: 38 | return nil 39 | } 40 | } 41 | } 42 | } 43 | 44 | extension PostgresData: ExpressibleByBooleanLiteral { 45 | public init(booleanLiteral value: Bool) { 46 | self.init(bool: value) 47 | } 48 | } 49 | 50 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 51 | extension Bool: PostgresDataConvertible { 52 | public static var postgresDataType: PostgresDataType { 53 | return .bool 54 | } 55 | 56 | public var postgresData: PostgresData? { 57 | return .init(bool: self) 58 | } 59 | 60 | public init?(postgresData: PostgresData) { 61 | guard let bool = postgresData.bool else { 62 | return nil 63 | } 64 | self = bool 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Bytes.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | import NIOCore 3 | 4 | extension PostgresData { 5 | public init(bytes: Bytes) 6 | where Bytes: Sequence, Bytes.Element == UInt8 7 | { 8 | var buffer = ByteBufferAllocator().buffer(capacity: 1) 9 | buffer.writeBytes(bytes) 10 | self.init(type: .bytea, formatCode: .binary, value: buffer) 11 | } 12 | 13 | public var bytes: [UInt8]? { 14 | guard var value = self.value else { 15 | return nil 16 | } 17 | guard let bytes = value.readBytes(length: value.readableBytes) else { 18 | return nil 19 | } 20 | return bytes 21 | } 22 | } 23 | 24 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 25 | extension Data: PostgresDataConvertible { 26 | public static var postgresDataType: PostgresDataType { 27 | return .bytea 28 | } 29 | 30 | public var postgresData: PostgresData? { 31 | return .init(bytes: self) 32 | } 33 | 34 | public init?(postgresData: PostgresData) { 35 | guard let bytes = postgresData.bytes else { 36 | return nil 37 | } 38 | self.init(bytes) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Date.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Date 2 | import NIOCore 3 | 4 | extension PostgresData { 5 | public init(date: Date) { 6 | var buffer = ByteBufferAllocator().buffer(capacity: 0) 7 | let seconds = date.timeIntervalSince(_psqlDateStart) * Double(_microsecondsPerSecond) 8 | buffer.writeInteger(Int64(seconds)) 9 | self.init(type: .timestamptz, value: buffer) 10 | } 11 | 12 | public var date: Date? { 13 | guard var value = self.value else { 14 | return nil 15 | } 16 | 17 | switch self.formatCode { 18 | case .text: 19 | return nil 20 | case .binary: 21 | switch self.type { 22 | case .timestamp, .timestamptz: 23 | let microseconds = value.readInteger(as: Int64.self)! 24 | let seconds = Double(microseconds) / Double(_microsecondsPerSecond) 25 | return Date(timeInterval: seconds, since: _psqlDateStart) 26 | case .time, .timetz: 27 | return nil 28 | case .date: 29 | let days = value.readInteger(as: Int32.self)! 30 | let seconds = Int64(days) * _secondsInDay 31 | return Date(timeInterval: Double(seconds), since: _psqlDateStart) 32 | default: 33 | return nil 34 | } 35 | } 36 | } 37 | } 38 | 39 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 40 | extension Date: PostgresDataConvertible { 41 | public static var postgresDataType: PostgresDataType { 42 | return .timestamptz 43 | } 44 | 45 | public init?(postgresData: PostgresData) { 46 | guard let date = postgresData.date else { 47 | return nil 48 | } 49 | self = date 50 | } 51 | 52 | public var postgresData: PostgresData? { 53 | return .init(date: self) 54 | } 55 | } 56 | 57 | // MARK: Private 58 | private let _microsecondsPerSecond: Int64 = 1_000_000 59 | private let _secondsInDay: Int64 = 24 * 60 * 60 60 | private let _psqlDateStart = Date(timeIntervalSince1970: 946_684_800) // values are stored as seconds before or after midnight 2000-01-01 61 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Decimal.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension PostgresData { 4 | public var decimal: Decimal? { 5 | guard let string = self.string else { 6 | return nil 7 | } 8 | guard let decimal = Decimal(string: string) else { 9 | return nil 10 | } 11 | return decimal 12 | } 13 | 14 | public init(decimal: Decimal) { 15 | self.init(string: decimal.description) 16 | } 17 | } 18 | 19 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 20 | extension Decimal: PostgresDataConvertible { 21 | public static var postgresDataType: PostgresDataType { 22 | return .numeric 23 | } 24 | 25 | public init?(postgresData: PostgresData) { 26 | guard let decimal = postgresData.decimal else { 27 | return nil 28 | } 29 | self = decimal 30 | } 31 | 32 | public var postgresData: PostgresData? { 33 | return .init(numeric: PostgresNumeric(decimal: self)) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Double.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresData { 4 | public init(double: Double) { 5 | var buffer = ByteBufferAllocator().buffer(capacity: 0) 6 | buffer.psqlWriteDouble(double) 7 | self.init(type: .float8, formatCode: .binary, value: buffer) 8 | } 9 | 10 | public var double: Double? { 11 | guard var value = self.value else { 12 | return nil 13 | } 14 | 15 | switch self.formatCode { 16 | case .binary: 17 | switch self.type { 18 | case .float4: 19 | return value.psqlReadFloat() 20 | .flatMap { Double($0) } 21 | case .float8: 22 | return value.psqlReadDouble() 23 | case .numeric: 24 | return self.numeric?.double 25 | default: 26 | return nil 27 | } 28 | case .text: 29 | guard let string = self.string else { 30 | return nil 31 | } 32 | return Double(string) 33 | } 34 | } 35 | } 36 | 37 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 38 | extension Double: PostgresDataConvertible { 39 | public static var postgresDataType: PostgresDataType { 40 | return .float8 41 | } 42 | 43 | public init?(postgresData: PostgresData) { 44 | guard let double = postgresData.double else { 45 | return nil 46 | } 47 | self = double 48 | } 49 | 50 | public var postgresData: PostgresData? { 51 | return .init(double: self) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Float.swift: -------------------------------------------------------------------------------- 1 | extension PostgresData { 2 | public init(float: Float) { 3 | self.init(double: Double(float)) 4 | } 5 | 6 | public var float: Float? { 7 | guard var value = self.value else { 8 | return nil 9 | } 10 | 11 | switch self.formatCode { 12 | case .binary: 13 | switch self.type { 14 | case .float4: 15 | return value.psqlReadFloat() 16 | case .float8: 17 | return value.psqlReadDouble() 18 | .flatMap { Float($0) } 19 | default: 20 | return nil 21 | } 22 | case .text: 23 | guard let string = self.string else { 24 | return nil 25 | } 26 | return Float(string) 27 | } 28 | } 29 | } 30 | 31 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 32 | extension Float: PostgresDataConvertible { 33 | public static var postgresDataType: PostgresDataType { 34 | return .float4 35 | } 36 | 37 | public init?(postgresData: PostgresData) { 38 | guard let float = postgresData.float else { 39 | return nil 40 | } 41 | self = float 42 | } 43 | 44 | public var postgresData: PostgresData? { 45 | return .init(float: self) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+JSON.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | import NIOCore 3 | 4 | extension PostgresData { 5 | public init(json jsonData: Data) { 6 | let jsonData = [UInt8](jsonData) 7 | 8 | var buffer = ByteBufferAllocator() 9 | .buffer(capacity: jsonData.count) 10 | buffer.writeBytes(jsonData) 11 | self.init(type: .json, formatCode: .binary, value: buffer) 12 | } 13 | 14 | public init(json value: T) throws where T: Encodable { 15 | let jsonData = try PostgresNIO._defaultJSONEncoder.encode(value) 16 | self.init(json: jsonData) 17 | } 18 | 19 | public var json: Data? { 20 | guard var value = self.value else { 21 | return nil 22 | } 23 | guard case .json = self.type else { 24 | return nil 25 | } 26 | guard let data = value.readBytes(length: value.readableBytes) else { 27 | return nil 28 | } 29 | return Data(data) 30 | } 31 | 32 | public func json(as type: T.Type) throws -> T? where T: Decodable { 33 | guard let data = self.json else { 34 | return nil 35 | } 36 | return try PostgresNIO._defaultJSONDecoder.decode(T.self, from: data) 37 | } 38 | } 39 | 40 | @available(*, deprecated, message: "This protocol is going to be replaced with ``PostgresEncodable`` and ``PostgresDecodable`` and conforming to ``Codable`` at the same time") 41 | public protocol PostgresJSONCodable: Codable, PostgresDataConvertible { } 42 | 43 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 44 | extension PostgresJSONCodable { 45 | public static var postgresDataType: PostgresDataType { 46 | return .json 47 | } 48 | 49 | public var postgresData: PostgresData? { 50 | return try? .init(json: self) 51 | } 52 | 53 | public init?(postgresData: PostgresData) { 54 | guard let value = try? postgresData.json(as: Self.self) else { 55 | return nil 56 | } 57 | self = value 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+JSONB.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import struct Foundation.Data 3 | 4 | fileprivate let jsonBVersionBytes: [UInt8] = [0x01] 5 | 6 | extension PostgresData { 7 | public init(jsonb jsonData: Data) { 8 | let jsonBData = [UInt8](jsonData) 9 | 10 | var buffer = ByteBufferAllocator() 11 | .buffer(capacity: jsonBVersionBytes.count + jsonBData.count) 12 | buffer.writeBytes(jsonBVersionBytes) 13 | buffer.writeBytes(jsonBData) 14 | 15 | self.init(type: .jsonb, formatCode: .binary, value: buffer) 16 | } 17 | 18 | public init(jsonb value: T) throws where T: Encodable { 19 | let jsonData = try PostgresNIO._defaultJSONEncoder.encode(value) 20 | self.init(jsonb: jsonData) 21 | } 22 | 23 | public var jsonb: Data? { 24 | guard var value = self.value else { 25 | return nil 26 | } 27 | guard case .jsonb = self.type else { 28 | return nil 29 | } 30 | 31 | guard let versionBytes = value.readBytes(length: jsonBVersionBytes.count), [UInt8](versionBytes) == jsonBVersionBytes else { 32 | return nil 33 | } 34 | 35 | guard let data = value.readBytes(length: value.readableBytes) else { 36 | return nil 37 | } 38 | 39 | return Data(data) 40 | } 41 | 42 | public func jsonb(as type: T.Type) throws -> T? where T: Decodable { 43 | guard let data = jsonb else { 44 | return nil 45 | } 46 | 47 | return try PostgresNIO._defaultJSONDecoder.decode(T.self, from: data) 48 | } 49 | } 50 | 51 | @available(*, deprecated, message: "This protocol is going to be replaced with ``PostgresEncodable`` and ``PostgresDecodable`` and conforming to ``Codable`` at the same time") 52 | public protocol PostgresJSONBCodable: Codable, PostgresDataConvertible { } 53 | 54 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 55 | extension PostgresJSONBCodable { 56 | public static var postgresDataType: PostgresDataType { 57 | return .jsonb 58 | } 59 | 60 | public var postgresData: PostgresData? { 61 | return try? .init(jsonb: self) 62 | } 63 | 64 | public init?(postgresData: PostgresData) { 65 | guard let value = try? postgresData.jsonb(as: Self.self) else { 66 | return nil 67 | } 68 | self = value 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Optional.swift: -------------------------------------------------------------------------------- 1 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 2 | extension Optional: PostgresDataConvertible where Wrapped: PostgresDataConvertible { 3 | public static var postgresDataType: PostgresDataType { 4 | return Wrapped.postgresDataType 5 | } 6 | 7 | public init?(postgresData: PostgresData) { 8 | self = Wrapped.init(postgresData: postgresData) 9 | } 10 | 11 | public var postgresData: PostgresData? { 12 | switch self { 13 | case .some(let wrapped): 14 | return wrapped.postgresData 15 | case .none: 16 | return .init(type: Wrapped.postgresDataType, value: nil) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+RawRepresentable.swift: -------------------------------------------------------------------------------- 1 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 2 | extension RawRepresentable where Self.RawValue: PostgresDataConvertible { 3 | public static var postgresDataType: PostgresDataType { 4 | RawValue.postgresDataType 5 | } 6 | 7 | public init?(postgresData: PostgresData) { 8 | guard let rawValue = RawValue.init(postgresData: postgresData) else { 9 | return nil 10 | } 11 | self.init(rawValue: rawValue) 12 | } 13 | 14 | public var postgresData: PostgresData? { 15 | self.rawValue.postgresData 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+Set.swift: -------------------------------------------------------------------------------- 1 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 2 | extension Set: PostgresDataConvertible where Element: PostgresDataConvertible { 3 | public static var postgresDataType: PostgresDataType { 4 | [Element].postgresDataType 5 | } 6 | 7 | public init?(postgresData: PostgresData) { 8 | guard let array = [Element](postgresData: postgresData) else { 9 | return nil 10 | } 11 | self = Set(array) 12 | } 13 | 14 | public var postgresData: PostgresData? { 15 | [Element](self).postgresData 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresData+UUID.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | 4 | extension PostgresData { 5 | public init(uuid: UUID) { 6 | var buffer = ByteBufferAllocator().buffer(capacity: 16) 7 | buffer.writeUUIDBytes(uuid) 8 | self.init(type: .uuid, formatCode: .binary, value: buffer) 9 | } 10 | 11 | public var uuid: UUID? { 12 | guard var value = self.value else { 13 | return nil 14 | } 15 | 16 | switch self.formatCode { 17 | case .binary: 18 | switch self.type { 19 | case .uuid: 20 | return value.readUUIDBytes() 21 | case .varchar, .text: 22 | return self.string.flatMap { UUID(uuidString: $0) } 23 | default: 24 | return nil 25 | } 26 | case .text: 27 | return nil 28 | } 29 | } 30 | } 31 | 32 | @available(*, deprecated, message: "Deprecating conformance to `PostgresDataConvertible`, since it is deprecated.") 33 | extension UUID: PostgresDataConvertible { 34 | public static var postgresDataType: PostgresDataType { 35 | return .uuid 36 | } 37 | 38 | public init?(postgresData: PostgresData) { 39 | guard let uuid = postgresData.uuid else { 40 | return nil 41 | } 42 | self = uuid 43 | } 44 | 45 | public var postgresData: PostgresData? { 46 | return .init(uuid: self) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Data/PostgresDataConvertible.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | @available(*, deprecated, message: "This protocol is going to be replaced with ``PostgresEncodable`` and ``PostgresDecodable``") 4 | public protocol PostgresDataConvertible { 5 | static var postgresDataType: PostgresDataType { get } 6 | init?(postgresData: PostgresData) 7 | var postgresData: PostgresData? { get } 8 | } 9 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Bind.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a Bind command. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct Bind: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .bind 9 | } 10 | 11 | public var description: String { 12 | return "Bind(\(self.parameters.count))" 13 | } 14 | 15 | public struct Parameter { 16 | /// The value of the parameter, in the format indicated by the associated format code. n is the above length. 17 | var value: ByteBuffer? 18 | } 19 | 20 | /// The name of the destination portal (an empty string selects the unnamed portal). 21 | public var portalName: String 22 | 23 | /// The name of the source prepared statement (an empty string selects the unnamed prepared statement). 24 | public var statementName: String 25 | 26 | /// The number of parameter format codes that follow (denoted C below). 27 | /// This can be zero to indicate that there are no parameters or that the parameters all use the default format (text); 28 | /// or one, in which case the specified format code is applied to all parameters; or it can equal the actual number of parameters. 29 | /// The parameter format codes. Each must presently be zero (text) or one (binary). 30 | public var parameterFormatCodes: [PostgresFormat] 31 | 32 | /// The number of parameter values that follow (possibly zero). This must match the number of parameters needed by the query. 33 | public var parameters: [Parameter] 34 | 35 | /// The number of result-column format codes that follow (denoted R below). 36 | /// This can be zero to indicate that there are no result columns or that the result columns should all use the default format (text); 37 | /// or one, in which case the specified format code is applied to all result columns (if any); 38 | /// or it can equal the actual number of result columns of the query. 39 | public var resultFormatCodes: [PostgresFormat] 40 | 41 | /// Serializes this message into a byte buffer. 42 | public func serialize(into buffer: inout ByteBuffer) { 43 | buffer.writeNullTerminatedString(self.portalName) 44 | buffer.writeNullTerminatedString(self.statementName) 45 | 46 | buffer.write(array: self.parameterFormatCodes) 47 | buffer.write(array: self.parameters) { 48 | if var data = $1.value { 49 | // The length of the parameter value, in bytes (this count does not include itself). Can be zero. 50 | $0.writeInteger(numericCast(data.readableBytes), as: Int32.self) 51 | // The value of the parameter, in the format indicated by the associated format code. n is the above length. 52 | $0.writeBuffer(&data) 53 | } else { 54 | // As a special case, -1 indicates a NULL parameter value. No value bytes follow in the NULL case. 55 | $0.writeInteger(-1, as: Int32.self) 56 | } 57 | } 58 | buffer.write(array: self.resultFormatCodes) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Close.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a Close Command 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct Close: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .close 9 | } 10 | 11 | /// Close Target. Determines if the Close command should close a prepared statement 12 | /// or portal. 13 | public enum Target: Int8 { 14 | case preparedStatement = 0x53 // 'S' - prepared statement 15 | case portal = 0x50 // 'P' - portal 16 | } 17 | 18 | /// Determines if the `name` identifes a portal or a prepared statement 19 | public var target: Target 20 | 21 | /// The name of the prepared statement or portal to describe 22 | /// (an empty string selects the unnamed prepared statement or portal). 23 | public var name: String 24 | 25 | 26 | /// See `CustomStringConvertible`. 27 | public var description: String { 28 | switch target { 29 | case .preparedStatement: return "Statement(\(name))" 30 | case .portal: return "Portal(\(name))" 31 | } 32 | } 33 | 34 | /// Serializes this message into a byte buffer. 35 | public func serialize(into buffer: inout ByteBuffer) throws { 36 | buffer.writeInteger(target.rawValue) 37 | buffer.writeNullTerminatedString(name) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+CommandComplete.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a Close command. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct CommandComplete: PostgresMessageType { 7 | /// Parses an instance of this message type from a byte buffer. 8 | public static func parse(from buffer: inout ByteBuffer) throws -> CommandComplete { 9 | guard let string = buffer.readNullTerminatedString() else { 10 | throw PostgresError.protocol("Could not parse close response message") 11 | } 12 | return .init(tag: string) 13 | } 14 | 15 | /// The command tag. This is usually a single word that identifies which SQL command was completed. 16 | public var tag: String 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Describe.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a Describe command. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct Describe: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .describe 9 | } 10 | 11 | /// Command type. 12 | public enum Command: UInt8 { 13 | case statement = 0x53 // S 14 | case portal = 0x50 // P 15 | } 16 | 17 | /// 'S' to describe a prepared statement; or 'P' to describe a portal. 18 | public let command: Command 19 | 20 | /// The name of the prepared statement or portal to describe 21 | /// (an empty string selects the unnamed prepared statement or portal). 22 | public var name: String 23 | 24 | /// See `CustomStringConvertible`. 25 | public var description: String { 26 | switch command { 27 | case .statement: return "Statement(" + name + ")" 28 | case .portal: return "Portal(" + name + ")" 29 | } 30 | } 31 | 32 | /// Serializes this message into a byte buffer. 33 | public func serialize(into buffer: inout ByteBuffer) { 34 | buffer.writeInteger(command.rawValue) 35 | buffer.writeNullTerminatedString(name) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Execute.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as an Execute command. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct Execute: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .execute 9 | } 10 | 11 | public var description: String { 12 | return "Execute()" 13 | } 14 | 15 | /// The name of the destination portal (an empty string selects the unnamed portal). 16 | public var portalName: String 17 | 18 | /// Maximum number of rows to return, if portal contains a query that 19 | /// returns rows (ignored otherwise). Zero denotes “no limit”. 20 | public var maxRows: Int32 21 | 22 | /// Serializes this message into a byte buffer. 23 | public func serialize(into buffer: inout ByteBuffer) { 24 | buffer.writeNullTerminatedString(portalName) 25 | buffer.writeInteger(self.maxRows) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+ParameterDescription.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a parameter description. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct ParameterDescription: PostgresMessageType { 7 | /// Parses an instance of this message type from a byte buffer. 8 | public static func parse(from buffer: inout ByteBuffer) throws -> ParameterDescription { 9 | guard let dataTypes = try buffer.read(array: PostgresDataType.self, { buffer in 10 | guard let dataType = buffer.readInteger(as: PostgresDataType.self) else { 11 | throw PostgresError.protocol("Could not parse data type integer in parameter description message.") 12 | } 13 | return dataType 14 | }) else { 15 | throw PostgresError.protocol("Could not parse data types in parameter description message.") 16 | } 17 | return .init(dataTypes: dataTypes) 18 | } 19 | 20 | /// Specifies the object ID of the parameter data type. 21 | public var dataTypes: [PostgresDataType] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+ParameterStatus.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | @available(*, deprecated, message: "Will be removed from public API") 5 | public struct ParameterStatus: PostgresMessageType, CustomStringConvertible { 6 | /// Parses an instance of this message type from a byte buffer. 7 | public static func parse(from buffer: inout ByteBuffer) throws -> ParameterStatus { 8 | guard let parameter = buffer.readNullTerminatedString() else { 9 | throw PostgresError.protocol("Could not read parameter from parameter status message") 10 | } 11 | guard let value = buffer.readNullTerminatedString() else { 12 | throw PostgresError.protocol("Could not read value from parameter status message") 13 | } 14 | return .init(parameter: parameter, value: value) 15 | } 16 | 17 | /// The name of the run-time parameter being reported. 18 | public var parameter: String 19 | 20 | /// The current value of the parameter. 21 | public var value: String 22 | 23 | /// See `CustomStringConvertible`. 24 | public var description: String { 25 | return "\(parameter): \(value)" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Parse.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a Parse command. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct Parse: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .parse 9 | } 10 | 11 | /// The name of the destination prepared statement (an empty string selects the unnamed prepared statement). 12 | public var statementName: String 13 | 14 | /// The query string to be parsed. 15 | public var query: String 16 | 17 | /// The number of parameter data types specified (can be zero). 18 | /// Note that this is not an indication of the number of parameters that might appear in the 19 | /// query string, only the number that the frontend wants to prespecify types for. 20 | /// Specifies the object ID of the parameter data type. Placing a zero here is equivalent to leaving the type unspecified. 21 | public var parameterTypes: [PostgresDataType] 22 | 23 | /// Serializes this message into a byte buffer. 24 | public func serialize(into buffer: inout ByteBuffer) { 25 | buffer.writeString(statementName + "\0") 26 | buffer.writeString(query + "\0") 27 | buffer.writeInteger(numericCast(self.parameterTypes.count), as: Int16.self) 28 | self.parameterTypes.forEach { buffer.writeInteger($0.rawValue) } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Password.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a password response. Note that this is also used for 5 | /// GSSAPI and SSPI response messages (which is really a design error, since the contained 6 | /// data is not a null-terminated string in that case, but can be arbitrary binary data). 7 | @available(*, deprecated, message: "Will be removed from public API") 8 | public struct Password: PostgresMessageType { 9 | public static var identifier: PostgresMessage.Identifier { 10 | return .passwordMessage 11 | } 12 | 13 | public init(string: String) { 14 | self.string = string 15 | } 16 | 17 | /// The password (encrypted, if requested). 18 | public var string: String 19 | 20 | public var description: String { 21 | return "Password(\(string))" 22 | } 23 | 24 | /// Serializes this message into a byte buffer. 25 | public func serialize(into buffer: inout ByteBuffer) { 26 | buffer.writeString(self.string + "\0") 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+ReadyForQuery.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message type. ReadyForQuery is sent whenever the backend is ready for a new query cycle. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct ReadyForQuery: CustomStringConvertible { 7 | /// Parses an instance of this message type from a byte buffer. 8 | public static func parse(from buffer: inout ByteBuffer) throws -> ReadyForQuery { 9 | guard let status = buffer.readInteger(as: UInt8.self) else { 10 | throw PostgresError.protocol("Could not read transaction status from ready for query message") 11 | } 12 | return .init(transactionStatus: status) 13 | } 14 | 15 | /// Current backend transaction status indicator. 16 | /// Possible values are 'I' if idle (not in a transaction block); 17 | /// 'T' if in a transaction block; or 'E' if in a failed transaction block 18 | /// (queries will be rejected until block is ended). 19 | public var transactionStatus: UInt8 20 | 21 | /// See `CustomStringConvertible`. 22 | public var description: String { 23 | let char = String(bytes: [transactionStatus], encoding: .ascii) ?? "n/a" 24 | return "transactionStatus: \(char)" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+SASLResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// SASL ongoing challenge response message sent by the client. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct SASLResponse: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .saslResponse 9 | } 10 | 11 | public let responseData: [UInt8] 12 | 13 | public static func parse(from buffer: inout ByteBuffer) throws -> SASLResponse { 14 | guard let data = buffer.readBytes(length: buffer.readableBytes) else { 15 | throw PostgresError.protocol("Could not parse SASL response from response message") 16 | } 17 | 18 | return SASLResponse(responseData: data) 19 | } 20 | 21 | public func serialize(into buffer: inout ByteBuffer) throws { 22 | buffer.writeBytes(responseData) 23 | } 24 | 25 | public var description: String { 26 | return "SASLResponse(\(responseData))" 27 | } 28 | } 29 | } 30 | 31 | extension PostgresMessage { 32 | /// SASL initial challenge response message sent by the client. 33 | @available(*, deprecated, message: "Will be removed from public API") 34 | public struct SASLInitialResponse { 35 | public let mechanism: String 36 | public let initialData: [UInt8] 37 | 38 | public func serialize(into buffer: inout ByteBuffer) throws { 39 | buffer.writeNullTerminatedString(self.mechanism) 40 | if initialData.count > 0 { 41 | buffer.writeInteger(Int32(initialData.count), as: Int32.self) // write(array:) writes Int16, which is incorrect here 42 | buffer.writeBytes(initialData) 43 | } else { 44 | buffer.writeInteger(-1, as: Int32.self) 45 | } 46 | } 47 | 48 | public var description: String { 49 | return "SASLInitialResponse(\(mechanism), data: \(initialData))" 50 | } 51 | } 52 | } 53 | 54 | @available(*, deprecated, message: "Deprecating conformance to `PostgresMessageType` since it is deprecated.") 55 | extension PostgresMessage.SASLInitialResponse: PostgresMessageType { 56 | public static var identifier: PostgresMessage.Identifier { 57 | return .saslInitialResponse 58 | } 59 | 60 | public static func parse(from buffer: inout ByteBuffer) throws -> Self { 61 | guard let mechanism = buffer.readNullTerminatedString() else { 62 | throw PostgresError.protocol("Could not parse SASL mechanism from initial response message") 63 | } 64 | guard let dataLength = buffer.readInteger(as: Int32.self) else { 65 | throw PostgresError.protocol("Could not parse SASL initial data length from initial response message") 66 | } 67 | 68 | var actualData: [UInt8] = [] 69 | 70 | if dataLength != -1 { 71 | guard let data = buffer.readBytes(length: Int(dataLength)) else { 72 | throw PostgresError.protocol("Could not parse SASL initial data from initial response message") 73 | } 74 | actualData = data 75 | } 76 | return .init(mechanism: mechanism, initialData: actualData) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+SSLRequest.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// A message asking the PostgreSQL server if SSL is supported 5 | /// For more info, see https://www.postgresql.org/docs/10/static/protocol-flow.html#id-1.10.5.7.11 6 | @available(*, deprecated, message: "Will be removed from public API") 7 | public struct SSLRequest: PostgresMessageType { 8 | /// The SSL request code. The value is chosen to contain 1234 in the most significant 16 bits, 9 | /// and 5679 in the least significant 16 bits. 10 | public let code: Int32 11 | 12 | /// See `CustomStringConvertible`. 13 | public var description: String { 14 | return "SSLRequest" 15 | } 16 | 17 | /// Creates a new `SSLRequest`. 18 | public init() { 19 | self.code = 80877103 20 | } 21 | 22 | /// Serializes this message into a byte buffer. 23 | public func serialize(into buffer: inout ByteBuffer) { 24 | buffer.writeInteger(self.code) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+SimpleQuery.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a simple query. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct SimpleQuery: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .query 9 | } 10 | 11 | /// The query string itself. 12 | public var string: String 13 | 14 | /// Serializes this message into a byte buffer. 15 | public func serialize(into buffer: inout ByteBuffer) { 16 | buffer.writeString(self.string + "\0") 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Startup.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// First message sent from the frontend during startup. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct Startup: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .none 9 | } 10 | 11 | public var description: String { 12 | return "Startup()" 13 | } 14 | 15 | /// Creates a `Startup` with "3.0" as the protocol version. 16 | public static func versionThree(parameters: [String: String]) -> Startup { 17 | return .init(protocolVersion: 0x00_03_00_00, parameters: parameters) 18 | } 19 | 20 | /// The protocol version number. The most significant 16 bits are the major 21 | /// version number (3 for the protocol described here). The least significant 22 | /// 16 bits are the minor version number (0 for the protocol described here). 23 | public var protocolVersion: Int32 24 | 25 | /// The protocol version number is followed by one or more pairs of parameter 26 | /// name and value strings. A zero byte is required as a terminator after 27 | /// the last name/value pair. Parameters can appear in any order. user is required, 28 | /// others are optional. Each parameter is specified as: 29 | public var parameters: [String: String] 30 | 31 | /// Creates a new `PostgreSQLStartupMessage`. 32 | public init(protocolVersion: Int32, parameters: [String: String]) { 33 | self.protocolVersion = protocolVersion 34 | self.parameters = parameters 35 | } 36 | 37 | /// Serializes this message into a byte buffer. 38 | public func serialize(into buffer: inout ByteBuffer) { 39 | buffer.writeInteger(self.protocolVersion) 40 | for (key, val) in parameters { 41 | buffer.writeString(key + "\0") 42 | buffer.writeString(val + "\0") 43 | } 44 | // terminator 45 | buffer.writeString("\0") 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Sync.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a Bind command. 5 | @available(*, deprecated, message: "Will be removed from public API") 6 | public struct Sync: PostgresMessageType { 7 | public static var identifier: PostgresMessage.Identifier { 8 | return .sync 9 | } 10 | 11 | public var description: String { 12 | return "Sync" 13 | } 14 | 15 | public func serialize(into buffer: inout ByteBuffer) { 16 | 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessage+Terminate.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | @available(*, deprecated, message: "Will be removed from public API") 5 | public struct Terminate: PostgresMessageType { 6 | public static var identifier: PostgresMessage.Identifier { 7 | .terminate 8 | } 9 | 10 | public func serialize(into buffer: inout ByteBuffer) { } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessageDecoder.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import Logging 3 | 4 | @available(*, deprecated, message: "Will be removed from public API") 5 | public final class PostgresMessageDecoder: ByteToMessageDecoder { 6 | /// See `ByteToMessageDecoder`. 7 | public typealias InboundOut = PostgresMessage 8 | 9 | /// See `ByteToMessageDecoder`. 10 | public var cumulationBuffer: ByteBuffer? 11 | 12 | /// If `true`, the server has asked for authentication. 13 | public var hasSeenFirstMessage: Bool 14 | 15 | /// Logger to send debug messages to. 16 | let logger: Logger? 17 | 18 | /// Creates a new `PostgresMessageDecoder`. 19 | public init(logger: Logger? = nil) { 20 | self.hasSeenFirstMessage = false 21 | self.logger = logger 22 | } 23 | 24 | /// See `ByteToMessageDecoder`. 25 | public func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState { 26 | var peekBuffer = buffer 27 | 28 | // peek at the message identifier 29 | // the message identifier is always the first byte of a message 30 | guard let identifier = peekBuffer.readInteger(as: UInt8.self).map(PostgresMessage.Identifier.init) else { 31 | return .needMoreData 32 | } 33 | 34 | let message: PostgresMessage 35 | 36 | // special ssl case, no body 37 | if !self.hasSeenFirstMessage && (identifier == .sslSupported || identifier == .sslUnsupported) { 38 | message = PostgresMessage(identifier: identifier, data: context.channel.allocator.buffer(capacity: 0)) 39 | } else { 40 | // peek at the message size 41 | // the message size is always a 4 byte integer appearing immediately after the message identifier 42 | guard let messageSize = peekBuffer.readInteger(as: Int32.self).flatMap(Int.init) else { 43 | return .needMoreData 44 | } 45 | 46 | // ensure message is large enough (skipping message type) or reject 47 | guard let data = peekBuffer.readSlice(length: messageSize - 4) else { 48 | return .needMoreData 49 | } 50 | 51 | message = PostgresMessage(identifier: identifier, data: data) 52 | } 53 | self.hasSeenFirstMessage = true 54 | 55 | // there is sufficient data, use this buffer 56 | buffer = peekBuffer 57 | self.logger?.trace("Decoded: PostgresMessage (\(message.identifier))") 58 | context.fireChannelRead(wrapInboundOut(message)) 59 | return .continue 60 | } 61 | 62 | public func decodeLast(context: ChannelHandlerContext, buffer: inout ByteBuffer, seenEOF: Bool) throws -> DecodingState { 63 | // ignore 64 | return .needMoreData 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Deprecated/PostgresMessageEncoder.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import Logging 3 | 4 | @available(*, deprecated, message: "Will be removed from public API") 5 | public final class PostgresMessageEncoder: MessageToByteEncoder { 6 | /// See `MessageToByteEncoder`. 7 | public typealias OutboundIn = PostgresMessage 8 | 9 | /// Logger to send debug messages to. 10 | let logger: Logger? 11 | 12 | /// Creates a new `PostgresMessageEncoder`. 13 | public init(logger: Logger? = nil) { 14 | self.logger = logger 15 | } 16 | 17 | /// See `MessageToByteEncoder`. 18 | public func encode(data message: PostgresMessage, out: inout ByteBuffer) throws { 19 | // serialize identifier 20 | var message = message 21 | switch message.identifier { 22 | case .none: break 23 | default: 24 | out.write(identifier: message.identifier) 25 | } 26 | 27 | // leave room for identifier and size 28 | let messageSizeIndex = out.writerIndex 29 | out.moveWriterIndex(forwardBy: 4) 30 | 31 | // serialize the message data 32 | out.writeBuffer(&message.data) 33 | 34 | // set message size 35 | out.setInteger(Int32(out.writerIndex - messageSizeIndex), at: messageSizeIndex) 36 | self.logger?.trace("Encoded: PostgresMessage (\(message.identifier))") 37 | } 38 | } 39 | 40 | protocol ByteBufferSerializable { 41 | func serialize(into buffer: inout ByteBuffer) 42 | } 43 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/coding.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL data types 2 | 3 | Translate Swift data types to Postgres data types and vica versa. Learn how to write translations 4 | for your own custom Swift types. 5 | 6 | ## Topics 7 | 8 | ### Essentials 9 | 10 | - ``PostgresCodable`` 11 | - ``PostgresDataType`` 12 | - ``PostgresFormat`` 13 | - ``PostgresNumeric`` 14 | 15 | ### Encoding 16 | 17 | - ``PostgresEncodable`` 18 | - ``PostgresNonThrowingEncodable`` 19 | - ``PostgresDynamicTypeEncodable`` 20 | - ``PostgresThrowingDynamicTypeEncodable`` 21 | - ``PostgresArrayEncodable`` 22 | - ``PostgresRangeEncodable`` 23 | - ``PostgresRangeArrayEncodable`` 24 | - ``PostgresEncodingContext`` 25 | 26 | ### Decoding 27 | 28 | - ``PostgresDecodable`` 29 | - ``PostgresArrayDecodable`` 30 | - ``PostgresRangeDecodable`` 31 | - ``PostgresRangeArrayDecodable`` 32 | - ``PostgresDecodingContext`` 33 | 34 | ### JSON 35 | 36 | - ``PostgresJSONEncoder`` 37 | - ``PostgresJSONDecoder`` 38 | 39 | 40 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/deprecated.md: -------------------------------------------------------------------------------- 1 | # Deprecations 2 | 3 | `PostgresNIO` follows SemVer 2.0.0. Learn which APIs are considered deprecated and how to migrate to 4 | their replacements. 5 | 6 | ``PostgresNIO`` reached 1.0 in April 2020. Since then the maintainers have been hard at work to 7 | guarantee API stability. However as the Swift and Swift on server ecosystem have matured approaches 8 | have changed. The introduction of structured concurrency changed what developers expect from a 9 | modern Swift library. Because of this ``PostgresNIO`` added various APIs that embrace the new Swift 10 | patterns. This means however, that PostgresNIO still offers APIs that have fallen out of favor. 11 | Those are documented here. All those APIs will be removed once the maintainers release the next 12 | major version. The maintainers recommend all adopters to move of those APIs sooner rather than 13 | later. 14 | 15 | ## Topics 16 | 17 | ### Migrate of deprecated APIs 18 | 19 | - 20 | 21 | ### Deprecated APIs 22 | 23 | These types are already deprecated or will be deprecated in the near future. All of them will be 24 | removed from the public API with the next major release. 25 | 26 | - ``PostgresDatabase`` 27 | - ``PostgresData`` 28 | - ``PostgresDataConvertible`` 29 | - ``PostgresQueryResult`` 30 | - ``PostgresJSONCodable`` 31 | - ``PostgresJSONBCodable`` 32 | - ``PostgresMessageEncoder`` 33 | - ``PostgresMessageDecoder`` 34 | - ``PostgresRequest`` 35 | - ``PostgresMessage`` 36 | - ``PostgresMessageType`` 37 | - ``PostgresFormatCode`` 38 | - ``PostgresListenContext`` 39 | - ``PreparedQuery`` 40 | - ``SASLAuthenticationManager`` 41 | - ``SASLAuthenticationMechanism`` 42 | - ``SASLAuthenticationError`` 43 | - ``SASLAuthenticationStepResult`` 44 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/images/vapor-postgresnio-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/index.md: -------------------------------------------------------------------------------- 1 | # ``PostgresNIO`` 2 | 3 | @Metadata { 4 | @TitleHeading(Package) 5 | } 6 | 7 | 🐘 Non-blocking, event-driven Swift client for PostgreSQL built on SwiftNIO. 8 | 9 | ## Overview 10 | 11 | ``PostgresNIO`` allows you to connect to, authorize with, query, and retrieve results from a 12 | PostgreSQL server. PostgreSQL is an open source relational database. 13 | 14 | Use a ``PostgresConnection`` to create a connection to the PostgreSQL server. You can then use it to 15 | run queries and prepared statements against the server. ``PostgresConnection`` also supports 16 | PostgreSQL's Listen & Notify API. 17 | 18 | Developers, who don't want to manage connections themselves, can use the ``PostgresClient``, which 19 | offers the same functionality as ``PostgresConnection``. ``PostgresClient`` 20 | pools connections for rapid connection reuse and hides the complexities of connection 21 | management from the user, allowing developers to focus on their SQL queries. ``PostgresClient`` 22 | implements the `Service` protocol from Service Lifecycle allowing an easy adoption in Swift server 23 | applications. 24 | 25 | ``PostgresNIO`` embraces Swift structured concurrency, offering async/await APIs which handle 26 | task cancellation. The query interface makes use of backpressure to ensure that memory can not grow 27 | unbounded for queries that return thousands of rows. 28 | 29 | ``PostgresNIO`` runs efficiently on Linux and Apple platforms. On Apple platforms developers can 30 | configure ``PostgresConnection`` to use `Network.framework` as the underlying transport framework. 31 | 32 | ## Topics 33 | 34 | ### Essentials 35 | 36 | - ``PostgresClient`` 37 | - ``PostgresClient/Configuration`` 38 | - ``PostgresConnection`` 39 | - 40 | 41 | ### Advanced 42 | 43 | - 44 | - 45 | - 46 | 47 | ### Errors 48 | 49 | - ``PostgresError`` 50 | - ``PostgresDecodingError`` 51 | - ``PSQLError`` 52 | 53 | ### Deprecations 54 | 55 | - 56 | 57 | [SwiftNIO]: https://github.com/apple/swift-nio 58 | [SwiftLog]: https://github.com/apple/swift-log 59 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/listen.md: -------------------------------------------------------------------------------- 1 | # Listen & Notify 2 | 3 | ``PostgresNIO`` supports PostgreSQL's listen and notify API. Learn how to listen for changes and 4 | notify other listeners. 5 | 6 | ## Topics 7 | 8 | - ``PostgresNotification`` 9 | - ``PostgresNotificationSequence`` 10 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/prepared-statement.md: -------------------------------------------------------------------------------- 1 | # Boosting Performance with Prepared Statements 2 | 3 | Improve performance by leveraging PostgreSQL's prepared statements. 4 | 5 | ## Topics 6 | 7 | - ``PostgresPreparedStatement`` 8 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/running-queries.md: -------------------------------------------------------------------------------- 1 | # Running Queries 2 | 3 | Interact with the PostgreSQL database by running Queries. 4 | 5 | ## Overview 6 | 7 | 8 | 9 | You interact with the Postgres database by running SQL [Queries]. 10 | 11 | 12 | 13 | ``PostgresQuery`` conforms to 14 | 15 | 16 | ## Topics 17 | 18 | - ``PostgresQuery`` 19 | - ``PostgresBindings`` 20 | - ``PostgresRow`` 21 | - ``PostgresRowSequence`` 22 | - ``PostgresRandomAccessRow`` 23 | - ``PostgresCell`` 24 | - ``PostgresQueryMetadata`` 25 | 26 | [Queries]: doc:PostgresQuery 27 | [`ExpressibleByStringInterpolation`]: https://developer.apple.com/documentation/swift/expressiblebystringinterpolation 28 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Docs.docc/theme-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "theme": { 3 | "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" }, 4 | "border-radius": "0", 5 | "button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 6 | "code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" }, 7 | "color": { 8 | "fill": { "dark": "#000", "light": "#fff" }, 9 | "psqlnio": "#336791", 10 | "documentation-intro-fill": "radial-gradient(circle at top, var(--color-psqlnio) 30%, #000 100%)", 11 | "documentation-intro-accent": "var(--color-psqlnio)", 12 | "documentation-intro-eyebrow": "white", 13 | "documentation-intro-figure": "white", 14 | "documentation-intro-title": "white", 15 | "logo-base": { "dark": "#fff", "light": "#000" }, 16 | "logo-shape": { "dark": "#000", "light": "#fff" } 17 | }, 18 | "icons": { "technology": "/postgresnio/images/vapor-postgresnio-logo.svg" } 19 | }, 20 | "features": { 21 | "quickNavigation": { "enable": true }, 22 | "i18n": { "enable": true } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Message/PostgresMessage+0.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | /// A frontend or backend Postgres message. 4 | public struct PostgresMessage: Equatable { 5 | @available(*, deprecated, message: "Will be removed from public API.") 6 | public var identifier: Identifier 7 | public var data: ByteBuffer 8 | 9 | @available(*, deprecated, message: "Will be removed from public API.") 10 | public init(identifier: Identifier, bytes: Data) 11 | where Data: Sequence, Data.Element == UInt8 12 | { 13 | var buffer = ByteBufferAllocator().buffer(capacity: 0) 14 | buffer.writeBytes(bytes) 15 | self.init(identifier: identifier, data: buffer) 16 | } 17 | 18 | @available(*, deprecated, message: "Will be removed from public API.") 19 | public init(identifier: Identifier, data: ByteBuffer) { 20 | self.identifier = identifier 21 | self.data = data 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Message/PostgresMessage+BackendKeyData.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as cancellation key data. 5 | /// The frontend must save these values if it wishes to be able to issue CancelRequest messages later. 6 | public struct BackendKeyData { 7 | /// The process ID of this backend. 8 | public var processID: Int32 9 | 10 | /// The secret key of this backend. 11 | public var secretKey: Int32 12 | } 13 | } 14 | 15 | @available(*, deprecated, message: "Deprecating conformance to `PostgresMessageType` since it is deprecated.") 16 | extension PostgresMessage.BackendKeyData: PostgresMessageType { 17 | public static var identifier: PostgresMessage.Identifier { 18 | .backendKeyData 19 | } 20 | 21 | /// Parses an instance of this message type from a byte buffer. 22 | public static func parse(from buffer: inout ByteBuffer) throws -> Self { 23 | guard let processID = buffer.readInteger(as: Int32.self) else { 24 | throw PostgresError.protocol("Could not parse process id from backend key data") 25 | } 26 | guard let secretKey = buffer.readInteger(as: Int32.self) else { 27 | throw PostgresError.protocol("Could not parse secret key from backend key data") 28 | } 29 | return .init(processID: processID, secretKey: secretKey) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Message/PostgresMessage+DataRow.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a data row. 5 | public struct DataRow { 6 | public struct Column: CustomStringConvertible { 7 | /// The length of the column value, in bytes (this count does not include itself). 8 | /// Can be zero. As a special case, -1 indicates a NULL column value. No value bytes follow in the NULL case. 9 | 10 | /// The value of the column, in the format indicated by the associated format code. n is the above length. 11 | public var value: ByteBuffer? 12 | 13 | /// See `CustomStringConvertible`. 14 | public var description: String { 15 | if let value = value { 16 | return "0x" + value.readableBytesView.hexdigest() 17 | } else { 18 | return "" 19 | } 20 | } 21 | } 22 | 23 | /// The data row's columns 24 | public var columns: [Column] 25 | 26 | /// See `CustomStringConvertible`. 27 | public var description: String { 28 | return "Columns(" + columns.map { $0.description }.joined(separator: ", ") + ")" 29 | } 30 | } 31 | } 32 | 33 | @available(*, deprecated, message: "Deprecating conformance to `PostgresMessageType` since it is deprecated.") 34 | extension PostgresMessage.DataRow: PostgresMessageType { 35 | public static var identifier: PostgresMessage.Identifier { 36 | return .dataRow 37 | } 38 | 39 | /// Parses an instance of this message type from a byte buffer. 40 | public static func parse(from buffer: inout ByteBuffer) throws -> Self { 41 | guard let columns = buffer.read(array: Column.self, { buffer in 42 | if var slice = buffer.readNullableBytes() { 43 | var copy = ByteBufferAllocator().buffer(capacity: slice.readableBytes) 44 | copy.writeBuffer(&slice) 45 | return .init(value: copy) 46 | } else { 47 | return .init(value: nil) 48 | } 49 | }) else { 50 | throw PostgresError.protocol("Could not parse data row columns") 51 | } 52 | return .init(columns: columns) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Message/PostgresMessage+NotificationResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresMessage { 4 | /// Identifies the message as a notification response. 5 | public struct NotificationResponse { 6 | public var backendPID: Int32 7 | public var channel: String 8 | public var payload: String 9 | } 10 | } 11 | 12 | @available(*, deprecated, message: "Deprecating conformance to `PostgresMessageType` since it is deprecated.") 13 | extension PostgresMessage.NotificationResponse: PostgresMessageType { 14 | public static let identifier = PostgresMessage.Identifier.notificationResponse 15 | 16 | /// Parses an instance of this message type from a byte buffer. 17 | public static func parse(from buffer: inout ByteBuffer) throws -> Self { 18 | guard let backendPID: Int32 = buffer.readInteger() else { 19 | throw PostgresError.protocol("Invalid NotificationResponse message: unable to read backend PID") 20 | } 21 | guard let channel = buffer.readNullTerminatedString() else { 22 | throw PostgresError.protocol("Invalid NotificationResponse message: unable to read channel") 23 | } 24 | guard let payload = buffer.readNullTerminatedString() else { 25 | throw PostgresError.protocol("Invalid NotificationResponse message: unable to read payload") 26 | } 27 | return .init(backendPID: backendPID, channel: channel, payload: payload) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Message/PostgresMessageType.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | @available(*, deprecated, message: "Will be removed from public API. Internally we now use `PostgresBackendMessage` and `PostgresFrontendMessage`") 4 | public protocol PostgresMessageType { 5 | static var identifier: PostgresMessage.Identifier { get } 6 | static func parse(from buffer: inout ByteBuffer) throws -> Self 7 | func serialize(into buffer: inout ByteBuffer) throws 8 | } 9 | 10 | @available(*, deprecated, message: "`PostgresMessageType` protocol is deprecated.") 11 | extension PostgresMessageType { 12 | @available(*, deprecated, message: "Will be removed from public API.") 13 | func message() throws -> PostgresMessage { 14 | var buffer = ByteBufferAllocator().buffer(capacity: 0) 15 | try self.serialize(into: &buffer) 16 | return .init(identifier: Self.identifier, data: buffer) 17 | } 18 | 19 | public init(message: PostgresMessage) throws { 20 | var message = message 21 | self = try Self.parse(from: &message.data) 22 | } 23 | 24 | @available(*, deprecated, message: "Will be removed from public API.") 25 | public static var identifier: PostgresMessage.Identifier { 26 | return .none 27 | } 28 | 29 | public static func parse(from buffer: inout ByteBuffer) throws -> Self { 30 | fatalError("\(Self.self) does not support parsing.") 31 | } 32 | 33 | public func serialize(into buffer: inout ByteBuffer) throws { 34 | fatalError("\(Self.self) does not support serializing.") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/Bool+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension Bool: PostgresDecodable { 4 | @inlinable 5 | public init( 6 | from buffer: inout ByteBuffer, 7 | type: PostgresDataType, 8 | format: PostgresFormat, 9 | context: PostgresDecodingContext 10 | ) throws { 11 | guard type == .bool else { 12 | throw PostgresDecodingError.Code.typeMismatch 13 | } 14 | 15 | switch format { 16 | case .binary: 17 | guard buffer.readableBytes == 1 else { 18 | throw PostgresDecodingError.Code.failure 19 | } 20 | 21 | switch buffer.readInteger(as: UInt8.self) { 22 | case .some(0): 23 | self = false 24 | case .some(1): 25 | self = true 26 | default: 27 | throw PostgresDecodingError.Code.failure 28 | } 29 | case .text: 30 | guard buffer.readableBytes == 1 else { 31 | throw PostgresDecodingError.Code.failure 32 | } 33 | 34 | switch buffer.readInteger(as: UInt8.self) { 35 | case .some(UInt8(ascii: "f")): 36 | self = false 37 | case .some(UInt8(ascii: "t")): 38 | self = true 39 | default: 40 | throw PostgresDecodingError.Code.failure 41 | } 42 | } 43 | } 44 | } 45 | 46 | extension Bool: PostgresNonThrowingEncodable { 47 | public static var psqlType: PostgresDataType { 48 | .bool 49 | } 50 | 51 | public static var psqlFormat: PostgresFormat { 52 | .binary 53 | } 54 | 55 | @inlinable 56 | public func encode( 57 | into byteBuffer: inout ByteBuffer, 58 | context: PostgresEncodingContext 59 | ) { 60 | byteBuffer.writeInteger(self ? 1 : 0, as: UInt8.self) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/Bytes+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import struct Foundation.Data 2 | import NIOCore 3 | import NIOFoundationCompat 4 | 5 | extension PostgresEncodable where Self: Sequence, Self.Element == UInt8 { 6 | public static var psqlType: PostgresDataType { 7 | .bytea 8 | } 9 | 10 | public static var psqlFormat: PostgresFormat { 11 | .binary 12 | } 13 | 14 | @inlinable 15 | public func encode( 16 | into byteBuffer: inout ByteBuffer, 17 | context: PostgresEncodingContext 18 | ) { 19 | byteBuffer.writeBytes(self) 20 | } 21 | } 22 | 23 | extension PostgresNonThrowingEncodable where Self: Sequence, Self.Element == UInt8 {} 24 | 25 | extension ByteBuffer: PostgresNonThrowingEncodable { 26 | public static var psqlType: PostgresDataType { 27 | .bytea 28 | } 29 | 30 | public static var psqlFormat: PostgresFormat { 31 | .binary 32 | } 33 | 34 | @inlinable 35 | public func encode( 36 | into byteBuffer: inout ByteBuffer, 37 | context: PostgresEncodingContext 38 | ) { 39 | var copyOfSelf = self // dirty hack 40 | byteBuffer.writeBuffer(©OfSelf) 41 | } 42 | } 43 | 44 | extension ByteBuffer: PostgresDecodable { 45 | @inlinable 46 | public init( 47 | from buffer: inout ByteBuffer, 48 | type: PostgresDataType, 49 | format: PostgresFormat, 50 | context: PostgresDecodingContext 51 | ) { 52 | self = buffer 53 | } 54 | } 55 | 56 | extension Data: PostgresEncodable { 57 | public static var psqlType: PostgresDataType { 58 | .bytea 59 | } 60 | 61 | public static var psqlFormat: PostgresFormat { 62 | .binary 63 | } 64 | 65 | @inlinable 66 | public func encode( 67 | into byteBuffer: inout ByteBuffer, 68 | context: PostgresEncodingContext 69 | ) { 70 | byteBuffer.writeBytes(self) 71 | } 72 | } 73 | 74 | extension Data: PostgresDecodable { 75 | @inlinable 76 | public init( 77 | from buffer: inout ByteBuffer, 78 | type: PostgresDataType, 79 | format: PostgresFormat, 80 | context: PostgresDecodingContext 81 | ) { 82 | self = buffer.readData(length: buffer.readableBytes, byteTransferStrategy: .automatic)! 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/Date+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import struct Foundation.Date 3 | 4 | extension Date: PostgresNonThrowingEncodable { 5 | public static var psqlType: PostgresDataType { 6 | .timestamptz 7 | } 8 | 9 | public static var psqlFormat: PostgresFormat { 10 | .binary 11 | } 12 | 13 | @inlinable 14 | public func encode( 15 | into byteBuffer: inout ByteBuffer, 16 | context: PostgresEncodingContext 17 | ) { 18 | let seconds = self.timeIntervalSince(Self._psqlDateStart) * Double(Self._microsecondsPerSecond) 19 | byteBuffer.writeInteger(Int64(seconds)) 20 | } 21 | 22 | // MARK: Private Constants 23 | 24 | @usableFromInline 25 | static let _microsecondsPerSecond: Int64 = 1_000_000 26 | @usableFromInline 27 | static let _secondsInDay: Int64 = 24 * 60 * 60 28 | 29 | /// values are stored as seconds before or after midnight 2000-01-01 30 | @usableFromInline 31 | static let _psqlDateStart = Date(timeIntervalSince1970: 946_684_800) 32 | } 33 | 34 | extension Date: PostgresDecodable { 35 | @inlinable 36 | public init( 37 | from buffer: inout ByteBuffer, 38 | type: PostgresDataType, 39 | format: PostgresFormat, 40 | context: PostgresDecodingContext 41 | ) throws { 42 | switch type { 43 | case .timestamp, .timestamptz: 44 | guard buffer.readableBytes == 8, let microseconds = buffer.readInteger(as: Int64.self) else { 45 | throw PostgresDecodingError.Code.failure 46 | } 47 | let seconds = Double(microseconds) / Double(Self._microsecondsPerSecond) 48 | self = Date(timeInterval: seconds, since: Self._psqlDateStart) 49 | case .date: 50 | guard buffer.readableBytes == 4, let days = buffer.readInteger(as: Int32.self) else { 51 | throw PostgresDecodingError.Code.failure 52 | } 53 | let seconds = Int64(days) * Self._secondsInDay 54 | self = Date(timeInterval: Double(seconds), since: Self._psqlDateStart) 55 | default: 56 | throw PostgresDecodingError.Code.typeMismatch 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/Decimal+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import struct Foundation.Decimal 3 | 4 | extension Decimal: PostgresEncodable { 5 | public static var psqlType: PostgresDataType { 6 | .numeric 7 | } 8 | 9 | public static var psqlFormat: PostgresFormat { 10 | .binary 11 | } 12 | 13 | public func encode( 14 | into byteBuffer: inout ByteBuffer, 15 | context: PostgresEncodingContext 16 | ) { 17 | let numeric = PostgresNumeric(decimal: self) 18 | byteBuffer.writeInteger(numeric.ndigits) 19 | byteBuffer.writeInteger(numeric.weight) 20 | byteBuffer.writeInteger(numeric.sign) 21 | byteBuffer.writeInteger(numeric.dscale) 22 | var value = numeric.value 23 | byteBuffer.writeBuffer(&value) 24 | } 25 | } 26 | 27 | extension Decimal: PostgresDecodable { 28 | public init( 29 | from buffer: inout ByteBuffer, 30 | type: PostgresDataType, 31 | format: PostgresFormat, 32 | context: PostgresDecodingContext 33 | ) throws { 34 | switch (format, type) { 35 | case (.binary, .numeric): 36 | guard let numeric = PostgresNumeric(buffer: &buffer) else { 37 | throw PostgresDecodingError.Code.failure 38 | } 39 | self = numeric.decimal 40 | case (.text, .numeric): 41 | guard let string = buffer.readString(length: buffer.readableBytes), let value = Decimal(string: string) else { 42 | throw PostgresDecodingError.Code.failure 43 | } 44 | self = value 45 | default: 46 | throw PostgresDecodingError.Code.typeMismatch 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/Float+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension Float: PostgresNonThrowingEncodable { 4 | public static var psqlType: PostgresDataType { 5 | .float4 6 | } 7 | 8 | public static var psqlFormat: PostgresFormat { 9 | .binary 10 | } 11 | 12 | @inlinable 13 | public func encode( 14 | into byteBuffer: inout ByteBuffer, 15 | context: PostgresEncodingContext 16 | ) { 17 | byteBuffer.psqlWriteFloat(self) 18 | } 19 | } 20 | 21 | extension Float: PostgresDecodable { 22 | @inlinable 23 | public init( 24 | from buffer: inout ByteBuffer, 25 | type: PostgresDataType, 26 | format: PostgresFormat, 27 | context: PostgresDecodingContext 28 | ) throws { 29 | switch (format, type) { 30 | case (.binary, .float4): 31 | guard buffer.readableBytes == 4, let float = buffer.psqlReadFloat() else { 32 | throw PostgresDecodingError.Code.failure 33 | } 34 | self = float 35 | case (.binary, .float8): 36 | guard buffer.readableBytes == 8, let double = buffer.psqlReadDouble() else { 37 | throw PostgresDecodingError.Code.failure 38 | } 39 | self = Float(double) 40 | case (.text, .float4), (.text, .float8): 41 | guard let string = buffer.readString(length: buffer.readableBytes), let value = Float(string) else { 42 | throw PostgresDecodingError.Code.failure 43 | } 44 | self = value 45 | default: 46 | throw PostgresDecodingError.Code.typeMismatch 47 | } 48 | } 49 | } 50 | 51 | extension Double: PostgresNonThrowingEncodable { 52 | public static var psqlType: PostgresDataType { 53 | .float8 54 | } 55 | 56 | public static var psqlFormat: PostgresFormat { 57 | .binary 58 | } 59 | 60 | @inlinable 61 | public func encode( 62 | into byteBuffer: inout ByteBuffer, 63 | context: PostgresEncodingContext 64 | ) { 65 | byteBuffer.psqlWriteDouble(self) 66 | } 67 | } 68 | 69 | extension Double: PostgresDecodable { 70 | @inlinable 71 | public init( 72 | from buffer: inout ByteBuffer, 73 | type: PostgresDataType, 74 | format: PostgresFormat, 75 | context: PostgresDecodingContext 76 | ) throws { 77 | switch (format, type) { 78 | case (.binary, .float4): 79 | guard buffer.readableBytes == 4, let float = buffer.psqlReadFloat() else { 80 | throw PostgresDecodingError.Code.failure 81 | } 82 | self = Double(float) 83 | case (.binary, .float8): 84 | guard buffer.readableBytes == 8, let double = buffer.psqlReadDouble() else { 85 | throw PostgresDecodingError.Code.failure 86 | } 87 | self = double 88 | case (.text, .float4), (.text, .float8): 89 | guard let string = buffer.readString(length: buffer.readableBytes), let value = Double(string) else { 90 | throw PostgresDecodingError.Code.failure 91 | } 92 | self = value 93 | default: 94 | throw PostgresDecodingError.Code.typeMismatch 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/JSON+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import NIOFoundationCompat 3 | import class Foundation.JSONEncoder 4 | import class Foundation.JSONDecoder 5 | 6 | @usableFromInline 7 | let JSONBVersionByte: UInt8 = 0x01 8 | 9 | extension PostgresEncodable where Self: Encodable { 10 | public static var psqlType: PostgresDataType { 11 | .jsonb 12 | } 13 | 14 | public static var psqlFormat: PostgresFormat { 15 | .binary 16 | } 17 | 18 | @inlinable 19 | public func encode( 20 | into byteBuffer: inout ByteBuffer, 21 | context: PostgresEncodingContext 22 | ) throws { 23 | byteBuffer.writeInteger(JSONBVersionByte) 24 | try context.jsonEncoder.encode(self, into: &byteBuffer) 25 | } 26 | } 27 | 28 | extension PostgresDecodable where Self: Decodable { 29 | public init( 30 | from buffer: inout ByteBuffer, 31 | type: PostgresDataType, 32 | format: PostgresFormat, 33 | context: PostgresDecodingContext 34 | ) throws { 35 | switch (format, type) { 36 | case (.binary, .jsonb): 37 | guard JSONBVersionByte == buffer.readInteger(as: UInt8.self) else { 38 | throw PostgresDecodingError.Code.failure 39 | } 40 | self = try context.jsonDecoder.decode(Self.self, from: buffer) 41 | case (.binary, .json), (.text, .jsonb), (.text, .json): 42 | self = try context.jsonDecoder.decode(Self.self, from: buffer) 43 | default: 44 | throw PostgresDecodingError.Code.typeMismatch 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/RawRepresentable+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresEncodable where Self: RawRepresentable, RawValue: PostgresEncodable { 4 | public static var psqlType: PostgresDataType { 5 | RawValue.psqlType 6 | } 7 | 8 | public static var psqlFormat: PostgresFormat { 9 | RawValue.psqlFormat 10 | } 11 | 12 | @inlinable 13 | public func encode( 14 | into byteBuffer: inout ByteBuffer, 15 | context: PostgresEncodingContext 16 | ) throws { 17 | try rawValue.encode(into: &byteBuffer, context: context) 18 | } 19 | } 20 | 21 | extension PostgresDecodable where Self: RawRepresentable, RawValue: PostgresDecodable, RawValue._DecodableType == RawValue { 22 | public init( 23 | from buffer: inout ByteBuffer, 24 | type: PostgresDataType, 25 | format: PostgresFormat, 26 | context: PostgresDecodingContext 27 | ) throws { 28 | guard let rawValue = try? RawValue(from: &buffer, type: type, format: format, context: context), 29 | let selfValue = Self.init(rawValue: rawValue) else { 30 | throw PostgresDecodingError.Code.failure 31 | } 32 | 33 | self = selfValue 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/String+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import struct Foundation.UUID 3 | 4 | extension String: PostgresNonThrowingEncodable { 5 | public static var psqlType: PostgresDataType { 6 | .text 7 | } 8 | 9 | public static var psqlFormat: PostgresFormat { 10 | .binary 11 | } 12 | 13 | @inlinable 14 | public func encode( 15 | into byteBuffer: inout ByteBuffer, 16 | context: PostgresEncodingContext 17 | ) { 18 | byteBuffer.writeString(self) 19 | } 20 | } 21 | 22 | extension String: PostgresDecodable { 23 | 24 | @inlinable 25 | public init( 26 | from buffer: inout ByteBuffer, 27 | type: PostgresDataType, 28 | format: PostgresFormat, 29 | context: PostgresDecodingContext 30 | ) throws { 31 | switch (format, type) { 32 | case (_, .varchar), 33 | (_, .bpchar), 34 | (_, .text), 35 | (_, .name): 36 | // we can force unwrap here, since this method only fails if there are not enough 37 | // bytes available. 38 | self = buffer.readString(length: buffer.readableBytes)! 39 | 40 | case (_, .uuid): 41 | guard let uuid = try? UUID(from: &buffer, type: .uuid, format: format, context: context) else { 42 | throw PostgresDecodingError.Code.failure 43 | } 44 | self = uuid.uuidString 45 | 46 | default: 47 | // We should eagerly try to convert any datatype into a String. For example the oid 48 | // for ltree isn't static. For this reason we should just try to convert anything. 49 | guard let string = buffer.readString(length: buffer.readableBytes) else { 50 | throw PostgresDecodingError.Code.typeMismatch 51 | } 52 | self = string 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Data/UUID+PostgresCodable.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import NIOFoundationCompat 3 | import struct Foundation.UUID 4 | import typealias Foundation.uuid_t 5 | import NIOFoundationCompat 6 | 7 | extension UUID: PostgresNonThrowingEncodable { 8 | public static var psqlType: PostgresDataType { 9 | .uuid 10 | } 11 | 12 | public static var psqlFormat: PostgresFormat { 13 | .binary 14 | } 15 | 16 | @inlinable 17 | public func encode( 18 | into byteBuffer: inout ByteBuffer, 19 | context: PostgresEncodingContext 20 | ) { 21 | byteBuffer.writeUUIDBytes(self) 22 | } 23 | } 24 | 25 | extension UUID: PostgresDecodable { 26 | @inlinable 27 | public init( 28 | from buffer: inout ByteBuffer, 29 | type: PostgresDataType, 30 | format: PostgresFormat, 31 | context: PostgresDecodingContext 32 | ) throws { 33 | switch (format, type) { 34 | case (.binary, .uuid): 35 | guard let uuid = buffer.readUUIDBytes() else { 36 | throw PostgresDecodingError.Code.failure 37 | } 38 | self = uuid 39 | case (.binary, .varchar), 40 | (.binary, .text), 41 | (.text, .uuid), 42 | (.text, .text), 43 | (.text, .varchar): 44 | guard buffer.readableBytes == 36 else { 45 | throw PostgresDecodingError.Code.failure 46 | } 47 | 48 | guard let uuid = buffer.readString(length: 36).flatMap({ UUID(uuidString: $0) }) else { 49 | throw PostgresDecodingError.Code.failure 50 | } 51 | self = uuid 52 | default: 53 | throw PostgresDecodingError.Code.typeMismatch 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Extensions/ByteBuffer+PSQL.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | internal extension ByteBuffer { 4 | 5 | @usableFromInline 6 | mutating func psqlReadFloat() -> Float? { 7 | return self.readInteger(as: UInt32.self).map { Float(bitPattern: $0) } 8 | } 9 | 10 | @usableFromInline 11 | mutating func psqlReadDouble() -> Double? { 12 | return self.readInteger(as: UInt64.self).map { Double(bitPattern: $0) } 13 | } 14 | 15 | @usableFromInline 16 | mutating func psqlWriteFloat(_ float: Float) { 17 | self.writeInteger(float.bitPattern) 18 | } 19 | 20 | @usableFromInline 21 | mutating func psqlWriteDouble(_ double: Double) { 22 | self.writeInteger(double.bitPattern) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Messages/Authentication.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresBackendMessage { 4 | 5 | enum Authentication: PayloadDecodable, Hashable { 6 | case ok 7 | case kerberosV5 8 | case md5(salt: UInt32) 9 | case plaintext 10 | case scmCredential 11 | case gss 12 | case sspi 13 | case gssContinue(data: ByteBuffer) 14 | case sasl(names: [String]) 15 | case saslContinue(data: ByteBuffer) 16 | case saslFinal(data: ByteBuffer) 17 | 18 | static func decode(from buffer: inout ByteBuffer) throws -> Self { 19 | let authID = try buffer.throwingReadInteger(as: Int32.self) 20 | 21 | switch authID { 22 | case 0: 23 | return .ok 24 | case 2: 25 | return .kerberosV5 26 | case 3: 27 | return .plaintext 28 | case 5: 29 | guard let salt = buffer.readInteger(as: UInt32.self) else { 30 | throw PSQLPartialDecodingError.expectedAtLeastNRemainingBytes(4, actual: buffer.readableBytes) 31 | } 32 | return .md5(salt: salt) 33 | case 6: 34 | return .scmCredential 35 | case 7: 36 | return .gss 37 | case 8: 38 | let data = buffer.readSlice(length: buffer.readableBytes)! 39 | return .gssContinue(data: data) 40 | case 9: 41 | return .sspi 42 | case 10: 43 | var names = [String]() 44 | let endIndex = buffer.readerIndex + buffer.readableBytes 45 | while buffer.readerIndex < endIndex, let next = buffer.readNullTerminatedString() { 46 | names.append(next) 47 | } 48 | 49 | return .sasl(names: names) 50 | case 11: 51 | let data = buffer.readSlice(length: buffer.readableBytes)! 52 | return .saslContinue(data: data) 53 | case 12: 54 | let data = buffer.readSlice(length: buffer.readableBytes)! 55 | return .saslFinal(data: data) 56 | default: 57 | throw PSQLPartialDecodingError.unexpectedValue(value: authID) 58 | } 59 | } 60 | 61 | } 62 | } 63 | 64 | extension PostgresBackendMessage.Authentication: CustomDebugStringConvertible { 65 | var debugDescription: String { 66 | switch self { 67 | case .ok: 68 | return ".ok" 69 | case .kerberosV5: 70 | return ".kerberosV5" 71 | case .md5(salt: let salt): 72 | return ".md5(salt: \(String(reflecting: salt)))" 73 | case .plaintext: 74 | return ".plaintext" 75 | case .scmCredential: 76 | return ".scmCredential" 77 | case .gss: 78 | return ".gss" 79 | case .sspi: 80 | return ".sspi" 81 | case .gssContinue(data: let data): 82 | return ".gssContinue(data: \(String(reflecting: data)))" 83 | case .sasl(names: let names): 84 | return ".sasl(names: \(String(reflecting: names)))" 85 | case .saslContinue(data: let data): 86 | return ".saslContinue(salt: \(String(reflecting: data)))" 87 | case .saslFinal(data: let data): 88 | return ".saslFinal(salt: \(String(reflecting: data)))" 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Messages/BackendKeyData.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresBackendMessage { 4 | 5 | struct BackendKeyData: PayloadDecodable, Hashable { 6 | let processID: Int32 7 | let secretKey: Int32 8 | 9 | static func decode(from buffer: inout ByteBuffer) throws -> Self { 10 | guard let (processID, secretKey) = buffer.readMultipleIntegers(endianness: .big, as: (Int32, Int32).self) else { 11 | throw PSQLPartialDecodingError.expectedAtLeastNRemainingBytes(8, actual: buffer.readableBytes) 12 | } 13 | 14 | return .init(processID: processID, secretKey: secretKey) 15 | } 16 | } 17 | } 18 | 19 | extension PostgresBackendMessage.BackendKeyData: CustomDebugStringConvertible { 20 | var debugDescription: String { 21 | "processID: \(processID), secretKey: \(secretKey)" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Messages/NotificationResponse.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresBackendMessage { 4 | 5 | struct NotificationResponse: PayloadDecodable, Hashable { 6 | let backendPID: Int32 7 | let channel: String 8 | let payload: String 9 | 10 | static func decode(from buffer: inout ByteBuffer) throws -> PostgresBackendMessage.NotificationResponse { 11 | let backendPID = try buffer.throwingReadInteger(as: Int32.self) 12 | 13 | guard let channel = buffer.readNullTerminatedString() else { 14 | throw PSQLPartialDecodingError.fieldNotDecodable(type: String.self) 15 | } 16 | guard let payload = buffer.readNullTerminatedString() else { 17 | throw PSQLPartialDecodingError.fieldNotDecodable(type: String.self) 18 | } 19 | 20 | return NotificationResponse(backendPID: backendPID, channel: channel, payload: payload) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Messages/ParameterDescription.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresBackendMessage { 4 | 5 | struct ParameterDescription: PayloadDecodable, Hashable { 6 | /// Specifies the object ID of the parameter data type. 7 | var dataTypes: [PostgresDataType] 8 | 9 | static func decode(from buffer: inout ByteBuffer) throws -> Self { 10 | let parameterCount = try buffer.throwingReadInteger(as: UInt16.self) 11 | 12 | var result = [PostgresDataType]() 13 | result.reserveCapacity(Int(parameterCount)) 14 | 15 | for _ in 0.. Self { 13 | guard let name = buffer.readNullTerminatedString() else { 14 | throw PSQLPartialDecodingError.fieldNotDecodable(type: String.self) 15 | } 16 | 17 | guard let value = buffer.readNullTerminatedString() else { 18 | throw PSQLPartialDecodingError.fieldNotDecodable(type: String.self) 19 | } 20 | 21 | return ParameterStatus(parameter: name, value: value) 22 | } 23 | } 24 | } 25 | 26 | extension PostgresBackendMessage.ParameterStatus: CustomDebugStringConvertible { 27 | var debugDescription: String { 28 | "parameter: \(String(reflecting: self.parameter)), value: \(String(reflecting: self.value))" 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/Messages/ReadyForQuery.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PostgresBackendMessage { 4 | enum TransactionState: UInt8, PayloadDecodable, Hashable { 5 | case idle = 73 // ascii: I 6 | case inTransaction = 84 // ascii: T 7 | case inFailedTransaction = 69 // ascii: E 8 | 9 | static func decode(from buffer: inout ByteBuffer) throws -> Self { 10 | let value = try buffer.throwingReadInteger(as: UInt8.self) 11 | guard let state = Self.init(rawValue: value) else { 12 | throw PSQLPartialDecodingError.valueNotRawRepresentable(value: value, asType: TransactionState.self) 13 | } 14 | 15 | return state 16 | } 17 | } 18 | } 19 | 20 | extension PostgresBackendMessage.TransactionState: CustomDebugStringConvertible { 21 | var debugDescription: String { 22 | switch self { 23 | case .idle: 24 | return ".idle" 25 | case .inTransaction: 26 | return ".inTransaction" 27 | case .inFailedTransaction: 28 | return ".inFailedTransaction" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/PSQLPreparedStatement.swift: -------------------------------------------------------------------------------- 1 | struct PSQLPreparedStatement { 2 | 3 | /// The name with which the statement was prepared at the backend 4 | let name: String 5 | 6 | /// The query that is executed when using this `PSQLPreparedStatement` 7 | let query: String 8 | 9 | /// The postgres connection the statement was prepared on 10 | let connection: PostgresConnection 11 | 12 | /// The `RowDescription` to apply to all `DataRow`s when executing this `PSQLPreparedStatement` 13 | let rowDescription: RowDescription? 14 | } 15 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/PostgresNotificationSequence.swift: -------------------------------------------------------------------------------- 1 | 2 | public struct PostgresNotification: Sendable { 3 | public let payload: String 4 | } 5 | 6 | public struct PostgresNotificationSequence: AsyncSequence, Sendable { 7 | public typealias Element = PostgresNotification 8 | 9 | let base: AsyncThrowingStream 10 | 11 | public func makeAsyncIterator() -> AsyncIterator { 12 | AsyncIterator(base: self.base.makeAsyncIterator()) 13 | } 14 | 15 | public struct AsyncIterator: AsyncIteratorProtocol { 16 | var base: AsyncThrowingStream.AsyncIterator 17 | 18 | public mutating func next() async throws -> Element? { 19 | try await self.base.next() 20 | } 21 | } 22 | } 23 | 24 | @available(*, unavailable) 25 | extension PostgresNotificationSequence.AsyncIterator: Sendable {} 26 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/PostgresTransactionError.swift: -------------------------------------------------------------------------------- 1 | /// A wrapper around the errors that can occur during a transaction. 2 | public struct PostgresTransactionError: Error { 3 | 4 | /// The file in which the transaction was started 5 | public var file: String 6 | /// The line in which the transaction was started 7 | public var line: Int 8 | 9 | /// The error thrown when running the `BEGIN` query 10 | public var beginError: Error? 11 | /// The error thrown in the transaction closure 12 | public var closureError: Error? 13 | 14 | /// The error thrown while rolling the transaction back. If the ``closureError`` is set, 15 | /// but the ``rollbackError`` is empty, the rollback was successful. If the ``rollbackError`` 16 | /// is set, the rollback failed. 17 | public var rollbackError: Error? 18 | 19 | /// The error thrown while commiting the transaction. 20 | public var commitError: Error? 21 | } 22 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/New/PreparedStatement.swift: -------------------------------------------------------------------------------- 1 | /// A prepared statement. 2 | /// 3 | /// Structs conforming to this protocol will need to provide the SQL statement to 4 | /// send to the server and a way of creating bindings are decoding the result. 5 | /// 6 | /// As an example, consider this struct: 7 | /// ```swift 8 | /// struct Example: PostgresPreparedStatement { 9 | /// static let sql = "SELECT pid, datname FROM pg_stat_activity WHERE state = $1" 10 | /// typealias Row = (Int, String) 11 | /// 12 | /// var state: String 13 | /// 14 | /// func makeBindings() -> PostgresBindings { 15 | /// var bindings = PostgresBindings() 16 | /// bindings.append(self.state) 17 | /// return bindings 18 | /// } 19 | /// 20 | /// func decodeRow(_ row: PostgresNIO.PostgresRow) throws -> Row { 21 | /// try row.decode(Row.self) 22 | /// } 23 | /// } 24 | /// ``` 25 | /// 26 | /// Structs conforming to this protocol can then be used with `PostgresConnection.execute(_ preparedStatement:, logger:)`, 27 | /// which will take care of preparing the statement on the server side and executing it. 28 | public protocol PostgresPreparedStatement: Sendable { 29 | /// The prepared statements name. 30 | /// 31 | /// > Note: There is a default implementation that returns the implementor's name. 32 | static var name: String { get } 33 | 34 | /// The type rows returned by the statement will be decoded into 35 | associatedtype Row 36 | 37 | /// The SQL statement to prepare on the database server. 38 | static var sql: String { get } 39 | 40 | /// The postgres data types of the values that are bind when this statement is executed. 41 | /// 42 | /// If an empty array is returned the datatypes are inferred from the ``PostgresBindings`` returned 43 | /// from ``PostgresPreparedStatement/makeBindings()``. 44 | /// 45 | /// > Note: There is a default implementation that returns an empty array, which will lead to 46 | /// automatic inference. 47 | static var bindingDataTypes: [PostgresDataType] { get } 48 | 49 | /// Make the bindings to provided concrete values to use when executing the prepared SQL statement. 50 | /// The order must match ``PostgresPreparedStatement/bindingDataTypes-4b6tx``. 51 | func makeBindings() throws -> PostgresBindings 52 | 53 | /// Decode a row returned by the database into an instance of `Row` 54 | func decodeRow(_ row: PostgresRow) throws -> Row 55 | } 56 | 57 | extension PostgresPreparedStatement { 58 | public static var name: String { String(reflecting: self) } 59 | 60 | public static var bindingDataTypes: [PostgresDataType] { [] } 61 | } 62 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Pool/PostgresClientMetrics.swift: -------------------------------------------------------------------------------- 1 | import _ConnectionPoolModule 2 | import Logging 3 | 4 | final class PostgresClientMetrics: ConnectionPoolObservabilityDelegate { 5 | typealias ConnectionID = PostgresConnection.ID 6 | 7 | let logger: Logger 8 | 9 | init(logger: Logger) { 10 | self.logger = logger 11 | } 12 | 13 | func startedConnecting(id: ConnectionID) { 14 | self.logger.debug("Creating new connection", metadata: [ 15 | .connectionID: "\(id)", 16 | ]) 17 | } 18 | 19 | /// A connection attempt failed with the given error. After some period of 20 | /// time ``startedConnecting(id:)`` may be called again. 21 | func connectFailed(id: ConnectionID, error: Error) { 22 | self.logger.debug("Connection creation failed", metadata: [ 23 | .connectionID: "\(id)", 24 | .error: "\(String(reflecting: error))" 25 | ]) 26 | } 27 | 28 | func connectSucceeded(id: ConnectionID) { 29 | self.logger.debug("Connection established", metadata: [ 30 | .connectionID: "\(id)" 31 | ]) 32 | } 33 | 34 | /// The utlization of the connection changed; a stream may have been used, returned or the 35 | /// maximum number of concurrent streams available on the connection changed. 36 | func connectionLeased(id: ConnectionID) { 37 | self.logger.debug("Connection leased", metadata: [ 38 | .connectionID: "\(id)" 39 | ]) 40 | } 41 | 42 | func connectionReleased(id: ConnectionID) { 43 | self.logger.debug("Connection released", metadata: [ 44 | .connectionID: "\(id)" 45 | ]) 46 | } 47 | 48 | func keepAliveTriggered(id: ConnectionID) { 49 | self.logger.debug("run ping pong", metadata: [ 50 | .connectionID: "\(id)", 51 | ]) 52 | } 53 | 54 | func keepAliveSucceeded(id: ConnectionID) {} 55 | 56 | func keepAliveFailed(id: PostgresConnection.ID, error: Error) {} 57 | 58 | /// The remote peer is quiescing the connection: no new streams will be created on it. The 59 | /// connection will eventually be closed and removed from the pool. 60 | func connectionClosing(id: ConnectionID) { 61 | self.logger.debug("Close connection", metadata: [ 62 | .connectionID: "\(id)" 63 | ]) 64 | } 65 | 66 | /// The connection was closed. The connection may be established again in the future (notified 67 | /// via ``startedConnecting(id:)``). 68 | func connectionClosed(id: ConnectionID, error: Error?) { 69 | self.logger.debug("Connection closed", metadata: [ 70 | .connectionID: "\(id)" 71 | ]) 72 | } 73 | 74 | func requestQueueDepthChanged(_ newDepth: Int) { 75 | 76 | } 77 | 78 | func connectSucceeded(id: PostgresConnection.ID, streamCapacity: UInt16) { 79 | 80 | } 81 | 82 | func connectionUtilizationChanged(id: PostgresConnection.ID, streamsUsed: UInt16, streamCapacity: UInt16) { 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Postgres+PSQLCompat.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | extension PSQLError { 4 | func toPostgresError() -> Error { 5 | switch self.code.base { 6 | case .queryCancelled: 7 | return self 8 | case .server, .listenFailed: 9 | guard let serverInfo = self.serverInfo else { 10 | return self 11 | } 12 | 13 | var fields = [PostgresMessage.Error.Field: String]() 14 | fields.reserveCapacity(serverInfo.underlying.fields.count) 15 | serverInfo.underlying.fields.forEach { (key, value) in 16 | fields[PostgresMessage.Error.Field(rawValue: key.rawValue)!] = value 17 | } 18 | return PostgresError.server(PostgresMessage.Error(fields: fields)) 19 | case .sslUnsupported: 20 | return PostgresError.protocol("Server does not support TLS") 21 | case .failedToAddSSLHandler: 22 | return self.underlying ?? self 23 | case .messageDecodingFailure: 24 | let message = self.underlying != nil ? String(describing: self.underlying!) : "no message" 25 | return PostgresError.protocol("Error decoding message: \(message)") 26 | case .unexpectedBackendMessage: 27 | let message = self.backendMessage != nil ? String(describing: self.backendMessage!) : "no message" 28 | return PostgresError.protocol("Unexpected message: \(message)") 29 | case .unsupportedAuthMechanism: 30 | let message = self.unsupportedAuthScheme != nil ? String(describing: self.unsupportedAuthScheme!) : "no scheme" 31 | return PostgresError.protocol("Unsupported auth scheme: \(message)") 32 | case .authMechanismRequiresPassword: 33 | return PostgresError.protocol("Unable to authenticate without password") 34 | case .receivedUnencryptedDataAfterSSLRequest: 35 | return PostgresError.protocol("Received unencrypted data after SSL request") 36 | case .saslError: 37 | return self.underlying ?? self 38 | case .tooManyParameters, .invalidCommandTag: 39 | return self 40 | case .clientClosedConnection, 41 | .serverClosedConnection: 42 | return PostgresError.connectionClosed 43 | case .connectionError: 44 | return self.underlying ?? self 45 | case .unlistenFailed: 46 | return self.underlying ?? self 47 | case .uncleanShutdown: 48 | return PostgresError.protocol("Unexpected connection close") 49 | case .poolClosed: 50 | return self 51 | } 52 | } 53 | } 54 | 55 | extension PostgresFormat { 56 | init(psqlFormatCode: PostgresFormat) { 57 | switch psqlFormatCode { 58 | case .binary: 59 | self = .binary 60 | case .text: 61 | self = .text 62 | } 63 | } 64 | } 65 | 66 | extension Error { 67 | internal var asAppropriatePostgresError: Error { 68 | if let psqlError = self as? PSQLError { 69 | return psqlError.toPostgresError() 70 | } else { 71 | return self 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/PostgresDatabase+SimpleQuery.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import NIOConcurrencyHelpers 3 | import Logging 4 | 5 | extension PostgresDatabase { 6 | public func simpleQuery(_ string: String) -> EventLoopFuture<[PostgresRow]> { 7 | let rowsBoxed = NIOLockedValueBox([PostgresRow]()) 8 | return self.simpleQuery(string) { row in 9 | rowsBoxed.withLockedValue { 10 | $0.append(row) 11 | } 12 | }.map { rowsBoxed.withLockedValue { $0 } } 13 | } 14 | 15 | @preconcurrency 16 | public func simpleQuery(_ string: String, _ onRow: @Sendable @escaping (PostgresRow) throws -> ()) -> EventLoopFuture { 17 | self.query(string, onRow: onRow) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/PostgresDatabase.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import Logging 3 | 4 | @preconcurrency 5 | public protocol PostgresDatabase: Sendable { 6 | var logger: Logger { get } 7 | var eventLoop: EventLoop { get } 8 | func send( 9 | _ request: PostgresRequest, 10 | logger: Logger 11 | ) -> EventLoopFuture 12 | 13 | func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture 14 | } 15 | 16 | extension PostgresDatabase { 17 | public func logging(to logger: Logger) -> PostgresDatabase { 18 | _PostgresDatabaseCustomLogger(database: self, logger: logger) 19 | } 20 | } 21 | 22 | private struct _PostgresDatabaseCustomLogger { 23 | let database: PostgresDatabase 24 | let logger: Logger 25 | } 26 | 27 | extension _PostgresDatabaseCustomLogger: PostgresDatabase { 28 | var eventLoop: EventLoop { 29 | self.database.eventLoop 30 | } 31 | 32 | func send(_ request: PostgresRequest, logger: Logger) -> EventLoopFuture { 33 | self.database.send(request, logger: logger) 34 | } 35 | 36 | func withConnection(_ closure: @escaping (PostgresConnection) -> EventLoopFuture) -> EventLoopFuture { 37 | self.database.withConnection(closure) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/PostgresRequest.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | 3 | /// Protocol to encapsulate a function call on the Postgres server 4 | /// 5 | /// This protocol is deprecated going forward. 6 | public protocol PostgresRequest { 7 | // return nil to end request 8 | func respond(to message: PostgresMessage) throws -> [PostgresMessage]? 9 | func start() throws -> [PostgresMessage] 10 | func log(to logger: Logger) 11 | } 12 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Utilities/Exports.swift: -------------------------------------------------------------------------------- 1 | @_documentation(visibility: internal) @_exported import NIO 2 | @_documentation(visibility: internal) @_exported import NIOSSL 3 | @_documentation(visibility: internal) @_exported import struct Logging.Logger 4 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Utilities/NIOUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOCore 3 | 4 | internal extension ByteBuffer { 5 | mutating func readInteger(endianness: Endianness = .big, as rawRepresentable: E.Type) -> E? where E: RawRepresentable, E.RawValue: FixedWidthInteger { 6 | guard let rawValue = readInteger(endianness: endianness, as: E.RawValue.self) else { 7 | return nil 8 | } 9 | return E.init(rawValue: rawValue) 10 | } 11 | 12 | mutating func readNullableBytes() -> ByteBuffer? { 13 | guard let count: Int = readInteger(as: Int32.self).flatMap(numericCast) else { 14 | return nil 15 | } 16 | switch count { 17 | case -1: 18 | // As a special case, -1 indicates a NULL parameter value. No value bytes follow in the NULL case. 19 | return nil 20 | default: return readSlice(length: count) 21 | } 22 | } 23 | 24 | mutating func write(array: [T], closure: (inout ByteBuffer, T) -> ()) { 25 | self.writeInteger(numericCast(array.count), as: Int16.self) 26 | for el in array { 27 | closure(&self, el) 28 | } 29 | } 30 | 31 | mutating func write(array: [T]) where T: FixedWidthInteger { 32 | self.write(array: array) { buffer, el in 33 | buffer.writeInteger(el) 34 | } 35 | } 36 | 37 | mutating func write(array: [T]) where T: RawRepresentable, T.RawValue: FixedWidthInteger { 38 | self.write(array: array) { buffer, el in 39 | buffer.writeInteger(el.rawValue) 40 | } 41 | } 42 | 43 | mutating func read(array type: T.Type, _ closure: (inout ByteBuffer) throws -> (T)) rethrows -> [T]? { 44 | guard let count: Int = readInteger(as: Int16.self).flatMap(numericCast) else { 45 | return nil 46 | } 47 | var array: [T] = [] 48 | array.reserveCapacity(count) 49 | for _ in 0.. String { 58 | return reduce("") { $0 + String(format: "%02x", $1) } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Utilities/PostgresError.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public enum PostgresError: Error, LocalizedError, CustomStringConvertible { 4 | case `protocol`(String) 5 | case server(PostgresMessage.Error) 6 | case connectionClosed 7 | 8 | /// See `LocalizedError`. 9 | public var errorDescription: String? { 10 | return self.description 11 | } 12 | 13 | /// See `CustomStringConvertible`. 14 | public var description: String { 15 | let description: String 16 | switch self { 17 | case .protocol(let message): 18 | description = "protocol error: \(message)" 19 | case .server(let error): 20 | return "server: \(error.description)" 21 | case .connectionClosed: 22 | description = "connection closed" 23 | } 24 | return "NIOPostgres error: \(description)" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Utilities/PostgresJSONDecoder.swift: -------------------------------------------------------------------------------- 1 | import class Foundation.JSONDecoder 2 | import struct Foundation.Data 3 | import NIOFoundationCompat 4 | import NIOCore 5 | import NIOConcurrencyHelpers 6 | 7 | /// A protocol that mimicks the Foundation `JSONDecoder.decode(_:from:)` function. 8 | /// Conform a non-Foundation JSON decoder to this protocol if you want PostgresNIO to be 9 | /// able to use it when decoding JSON & JSONB values (see `PostgresNIO._defaultJSONDecoder`) 10 | @preconcurrency 11 | public protocol PostgresJSONDecoder: Sendable { 12 | func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable 13 | 14 | func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T 15 | } 16 | 17 | extension PostgresJSONDecoder { 18 | public func decode(_ type: T.Type, from buffer: ByteBuffer) throws -> T { 19 | var copy = buffer 20 | let data = copy.readData(length: buffer.readableBytes)! 21 | return try self.decode(type, from: data) 22 | } 23 | } 24 | 25 | //@available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 26 | extension JSONDecoder: PostgresJSONDecoder {} 27 | 28 | private let jsonDecoderLocked: NIOLockedValueBox = NIOLockedValueBox(JSONDecoder()) 29 | 30 | /// The default JSON decoder used by PostgresNIO when decoding JSON & JSONB values. 31 | /// As `_defaultJSONDecoder` will be reused for decoding all JSON & JSONB values 32 | /// from potentially multiple threads at once, you must ensure your custom JSON decoder is 33 | /// thread safe internally like `Foundation.JSONDecoder`. 34 | public var _defaultJSONDecoder: PostgresJSONDecoder { 35 | set { 36 | jsonDecoderLocked.withLockedValue { $0 = newValue } 37 | } 38 | get { 39 | jsonDecoderLocked.withLockedValue { $0 } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Sources/PostgresNIO/Utilities/PostgresJSONEncoder.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import NIOFoundationCompat 3 | import NIOCore 4 | import NIOConcurrencyHelpers 5 | 6 | /// A protocol that mimicks the Foundation `JSONEncoder.encode(_:)` function. 7 | /// Conform a non-Foundation JSON encoder to this protocol if you want PostgresNIO to be 8 | /// able to use it when encoding JSON & JSONB values (see `PostgresNIO._defaultJSONEncoder`) 9 | @preconcurrency 10 | public protocol PostgresJSONEncoder: Sendable { 11 | func encode(_ value: T) throws -> Data where T : Encodable 12 | 13 | func encode(_ value: T, into buffer: inout ByteBuffer) throws 14 | } 15 | 16 | extension PostgresJSONEncoder { 17 | public func encode(_ value: T, into buffer: inout ByteBuffer) throws { 18 | let data = try self.encode(value) 19 | buffer.writeData(data) 20 | } 21 | } 22 | 23 | extension JSONEncoder: PostgresJSONEncoder {} 24 | 25 | private let jsonEncoderLocked: NIOLockedValueBox = NIOLockedValueBox(JSONEncoder()) 26 | 27 | /// The default JSON encoder used by PostgresNIO when encoding JSON & JSONB values. 28 | /// As `_defaultJSONEncoder` will be reused for encoding all JSON & JSONB values 29 | /// from potentially multiple threads at once, you must ensure your custom JSON encoder is 30 | /// thread safe internally like `Foundation.JSONEncoder`. 31 | public var _defaultJSONEncoder: PostgresJSONEncoder { 32 | set { 33 | jsonEncoderLocked.withLockedValue { $0 = newValue } 34 | } 35 | get { 36 | jsonEncoderLocked.withLockedValue { $0 } 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /Tests/ConnectionPoolModuleTests/ConnectionIDGeneratorTests.swift: -------------------------------------------------------------------------------- 1 | import _ConnectionPoolModule 2 | import XCTest 3 | 4 | final class ConnectionIDGeneratorTests: XCTestCase { 5 | func testGenerateConnectionIDs() async { 6 | let idGenerator = ConnectionIDGenerator() 7 | 8 | XCTAssertEqual(idGenerator.next(), 0) 9 | XCTAssertEqual(idGenerator.next(), 1) 10 | XCTAssertEqual(idGenerator.next(), 2) 11 | 12 | await withTaskGroup(of: Void.self) { taskGroup in 13 | for _ in 0..<1000 { 14 | taskGroup.addTask { 15 | _ = idGenerator.next() 16 | } 17 | } 18 | } 19 | 20 | XCTAssertEqual(idGenerator.next(), 1003) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Tests/ConnectionPoolModuleTests/ConnectionRequestTests.swift: -------------------------------------------------------------------------------- 1 | @testable import _ConnectionPoolModule 2 | import _ConnectionPoolTestUtils 3 | import XCTest 4 | 5 | final class ConnectionRequestTests: XCTestCase { 6 | 7 | func testHappyPath() async throws { 8 | let mockConnection = MockConnection(id: 1) 9 | let connection = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 10 | let request = ConnectionRequest(id: 42, continuation: continuation) 11 | XCTAssertEqual(request.id, 42) 12 | continuation.resume(with: .success(mockConnection)) 13 | } 14 | 15 | XCTAssert(connection === mockConnection) 16 | } 17 | 18 | func testSadPath() async throws { 19 | do { 20 | _ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in 21 | continuation.resume(with: .failure(ConnectionPoolError.requestCancelled)) 22 | } 23 | XCTFail("This point should not be reached") 24 | } catch { 25 | XCTAssertEqual(error as? ConnectionPoolError, .requestCancelled) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/ConnectionPoolModuleTests/Max2SequenceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import _ConnectionPoolModule 2 | import XCTest 3 | 4 | final class Max2SequenceTests: XCTestCase { 5 | func testCountAndIsEmpty() async { 6 | var sequence = Max2Sequence() 7 | XCTAssertEqual(sequence.count, 0) 8 | XCTAssertEqual(sequence.isEmpty, true) 9 | sequence.append(1) 10 | XCTAssertEqual(sequence.count, 1) 11 | XCTAssertEqual(sequence.isEmpty, false) 12 | sequence.append(2) 13 | XCTAssertEqual(sequence.count, 2) 14 | XCTAssertEqual(sequence.isEmpty, false) 15 | } 16 | 17 | func testOptionalInitializer() { 18 | let emptySequence = Max2Sequence(nil, nil) 19 | XCTAssertEqual(emptySequence.count, 0) 20 | XCTAssertEqual(emptySequence.isEmpty, true) 21 | var emptySequenceIterator = emptySequence.makeIterator() 22 | XCTAssertNil(emptySequenceIterator.next()) 23 | XCTAssertNil(emptySequenceIterator.next()) 24 | XCTAssertNil(emptySequenceIterator.next()) 25 | 26 | let oneElemSequence1 = Max2Sequence(1, nil) 27 | XCTAssertEqual(oneElemSequence1.count, 1) 28 | XCTAssertEqual(oneElemSequence1.isEmpty, false) 29 | var oneElemSequence1Iterator = oneElemSequence1.makeIterator() 30 | XCTAssertEqual(oneElemSequence1Iterator.next(), 1) 31 | XCTAssertNil(oneElemSequence1Iterator.next()) 32 | XCTAssertNil(oneElemSequence1Iterator.next()) 33 | 34 | let oneElemSequence2 = Max2Sequence(nil, 2) 35 | XCTAssertEqual(oneElemSequence2.count, 1) 36 | XCTAssertEqual(oneElemSequence2.isEmpty, false) 37 | var oneElemSequence2Iterator = oneElemSequence2.makeIterator() 38 | XCTAssertEqual(oneElemSequence2Iterator.next(), 2) 39 | XCTAssertNil(oneElemSequence2Iterator.next()) 40 | XCTAssertNil(oneElemSequence2Iterator.next()) 41 | 42 | let twoElemSequence = Max2Sequence(1, 2) 43 | XCTAssertEqual(twoElemSequence.count, 2) 44 | XCTAssertEqual(twoElemSequence.isEmpty, false) 45 | var twoElemSequenceIterator = twoElemSequence.makeIterator() 46 | XCTAssertEqual(twoElemSequenceIterator.next(), 1) 47 | XCTAssertEqual(twoElemSequenceIterator.next(), 2) 48 | XCTAssertNil(twoElemSequenceIterator.next()) 49 | } 50 | 51 | func testMap() { 52 | let twoElemSequence = Max2Sequence(1, 2).map({ "\($0)" }) 53 | XCTAssertEqual(twoElemSequence.count, 2) 54 | XCTAssertEqual(twoElemSequence.isEmpty, false) 55 | var twoElemSequenceIterator = twoElemSequence.makeIterator() 56 | XCTAssertEqual(twoElemSequenceIterator.next(), "1") 57 | XCTAssertEqual(twoElemSequenceIterator.next(), "2") 58 | XCTAssertNil(twoElemSequenceIterator.next()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Tests/ConnectionPoolModuleTests/Mocks/MockTimerCancellationToken.swift: -------------------------------------------------------------------------------- 1 | @testable import _ConnectionPoolModule 2 | 3 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 4 | struct MockTimerCancellationToken: Hashable, Sendable { 5 | enum Backing: Hashable, Sendable { 6 | case timer(TestPoolStateMachine.Timer) 7 | case connectionTimer(TestPoolStateMachine.ConnectionTimer) 8 | } 9 | var backing: Backing 10 | 11 | init(_ timer: TestPoolStateMachine.Timer) { 12 | self.backing = .timer(timer) 13 | } 14 | 15 | init(_ timer: TestPoolStateMachine.ConnectionTimer) { 16 | self.backing = .connectionTimer(timer) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/ConnectionPoolModuleTests/NoKeepAliveBehaviorTests.swift: -------------------------------------------------------------------------------- 1 | import _ConnectionPoolModule 2 | import _ConnectionPoolTestUtils 3 | import XCTest 4 | 5 | @available(macOS 13.0, iOS 16.0, tvOS 16.0, watchOS 9.0, *) 6 | final class NoKeepAliveBehaviorTests: XCTestCase { 7 | func testNoKeepAlive() { 8 | let keepAliveBehavior = NoOpKeepAliveBehavior(connectionType: MockConnection.self) 9 | XCTAssertNil(keepAliveBehavior.keepAliveFrequency) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Tests/ConnectionPoolModuleTests/TinyFastSequenceTests.swift: -------------------------------------------------------------------------------- 1 | @testable import _ConnectionPoolModule 2 | import XCTest 3 | 4 | final class TinyFastSequenceTests: XCTestCase { 5 | func testCountIsEmptyAndIterator() async { 6 | var sequence = TinyFastSequence() 7 | XCTAssertEqual(sequence.count, 0) 8 | XCTAssertEqual(sequence.isEmpty, true) 9 | XCTAssertEqual(sequence.first, nil) 10 | XCTAssertEqual(Array(sequence), []) 11 | sequence.append(1) 12 | XCTAssertEqual(sequence.count, 1) 13 | XCTAssertEqual(sequence.isEmpty, false) 14 | XCTAssertEqual(sequence.first, 1) 15 | XCTAssertEqual(Array(sequence), [1]) 16 | sequence.append(2) 17 | XCTAssertEqual(sequence.count, 2) 18 | XCTAssertEqual(sequence.isEmpty, false) 19 | XCTAssertEqual(sequence.first, 1) 20 | XCTAssertEqual(Array(sequence), [1, 2]) 21 | sequence.append(3) 22 | XCTAssertEqual(sequence.count, 3) 23 | XCTAssertEqual(sequence.isEmpty, false) 24 | XCTAssertEqual(sequence.first, 1) 25 | XCTAssertEqual(Array(sequence), [1, 2, 3]) 26 | } 27 | 28 | func testReserveCapacityIsForwarded() { 29 | var emptySequence = TinyFastSequence() 30 | emptySequence.reserveCapacity(8) 31 | emptySequence.append(1) 32 | emptySequence.append(2) 33 | emptySequence.append(3) 34 | guard case .n(let array) = emptySequence.base else { 35 | return XCTFail("Expected sequence to be backed by an array") 36 | } 37 | XCTAssertEqual(array.capacity, 8) 38 | 39 | var oneElemSequence = TinyFastSequence(element: 1) 40 | oneElemSequence.reserveCapacity(8) 41 | oneElemSequence.append(2) 42 | oneElemSequence.append(3) 43 | guard case .n(let array) = oneElemSequence.base else { 44 | return XCTFail("Expected sequence to be backed by an array") 45 | } 46 | XCTAssertEqual(array.capacity, 8) 47 | 48 | var twoElemSequence = TinyFastSequence([1, 2]) 49 | twoElemSequence.reserveCapacity(8) 50 | guard case .n(let array) = twoElemSequence.base else { 51 | return XCTFail("Expected sequence to be backed by an array") 52 | } 53 | XCTAssertEqual(array.capacity, 8) 54 | } 55 | 56 | func testNewSequenceSlowPath() { 57 | let sequence = TinyFastSequence("AB".utf8) 58 | XCTAssertEqual(Array(sequence), [UInt8(ascii: "A"), UInt8(ascii: "B")]) 59 | } 60 | 61 | func testSingleItem() { 62 | let sequence = TinyFastSequence("A".utf8) 63 | XCTAssertEqual(Array(sequence), [UInt8(ascii: "A")]) 64 | } 65 | 66 | func testEmptyCollection() { 67 | let sequence = TinyFastSequence("".utf8) 68 | XCTAssertTrue(sequence.isEmpty) 69 | XCTAssertEqual(sequence.count, 0) 70 | XCTAssertEqual(Array(sequence), []) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/Data/PostgresData+JSONTests.swift: -------------------------------------------------------------------------------- 1 | import PostgresNIO 2 | import XCTest 3 | 4 | class PostgresData_JSONTests: XCTestCase { 5 | @available(*, deprecated, message: "Testing deprecated functionality") 6 | func testJSONBConvertible() { 7 | struct Object: PostgresJSONBCodable { 8 | let foo: Int 9 | let bar: Int 10 | } 11 | 12 | XCTAssertEqual(Object.postgresDataType, .jsonb) 13 | 14 | let postgresData = Object(foo: 1, bar: 2).postgresData 15 | XCTAssertEqual(postgresData?.type, .jsonb) 16 | 17 | let object = Object(postgresData: postgresData!) 18 | XCTAssertEqual(object?.foo, 1) 19 | XCTAssertEqual(object?.bar, 2) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/Message/PostgresMessageDecoderTests.swift: -------------------------------------------------------------------------------- 1 | import PostgresNIO 2 | import XCTest 3 | import NIOTestUtils 4 | import NIOCore 5 | 6 | class PostgresMessageDecoderTests: XCTestCase { 7 | @available(*, deprecated, message: "Tests deprecated API") 8 | func testMessageDecoder() { 9 | let sample: [UInt8] = [ 10 | 0x52, // R - authentication 11 | 0x00, 0x00, 0x00, 0x0C, // length = 12 12 | 0x00, 0x00, 0x00, 0x05, // md5 13 | 0x01, 0x02, 0x03, 0x04, // salt 14 | 0x4B, // B - backend key data 15 | 0x00, 0x00, 0x00, 0x0C, // length = 12 16 | 0x05, 0x05, 0x05, 0x05, // process id 17 | 0x01, 0x01, 0x01, 0x01, // secret key 18 | ] 19 | var input = ByteBufferAllocator().buffer(capacity: 0) 20 | input.writeBytes(sample) 21 | 22 | let output: [PostgresMessage] = [ 23 | PostgresMessage(identifier: .authentication, bytes: [ 24 | 0x00, 0x00, 0x00, 0x05, 25 | 0x01, 0x02, 0x03, 0x04, 26 | ]), 27 | PostgresMessage(identifier: .backendKeyData, bytes: [ 28 | 0x05, 0x05, 0x05, 0x05, 29 | 0x01, 0x01, 0x01, 0x01, 30 | ]) 31 | ] 32 | XCTAssertNoThrow(try XCTUnwrap(ByteToMessageDecoderVerifier.verifyDecoder( 33 | inputOutputPairs: [(input, output)], 34 | decoderFactory: { 35 | PostgresMessageDecoder() 36 | } 37 | ))) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Data/Bool+PSQLCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class Bool_PSQLCodableTests: XCTestCase { 6 | 7 | // MARK: - Binary 8 | 9 | func testBinaryTrueRoundTrip() { 10 | let value = true 11 | 12 | var buffer = ByteBuffer() 13 | value.encode(into: &buffer, context: .default) 14 | XCTAssertEqual(Bool.psqlType, .bool) 15 | XCTAssertEqual(Bool.psqlFormat, .binary) 16 | XCTAssertEqual(buffer.readableBytes, 1) 17 | XCTAssertEqual(buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), 1) 18 | 19 | var result: Bool? 20 | XCTAssertNoThrow(result = try Bool(from: &buffer, type: .bool, format: .binary, context: .default)) 21 | XCTAssertEqual(value, result) 22 | } 23 | 24 | func testBinaryFalseRoundTrip() { 25 | let value = false 26 | 27 | var buffer = ByteBuffer() 28 | value.encode(into: &buffer, context: .default) 29 | XCTAssertEqual(Bool.psqlType, .bool) 30 | XCTAssertEqual(Bool.psqlFormat, .binary) 31 | XCTAssertEqual(buffer.readableBytes, 1) 32 | XCTAssertEqual(buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), 0) 33 | 34 | var result: Bool? 35 | XCTAssertNoThrow(result = try Bool(from: &buffer, type: .bool, format: .binary, context: .default)) 36 | XCTAssertEqual(value, result) 37 | } 38 | 39 | func testBinaryDecodeBoolInvalidLength() { 40 | var buffer = ByteBuffer() 41 | buffer.writeInteger(Int64(1)) 42 | 43 | XCTAssertThrowsError(try Bool(from: &buffer, type: .bool, format: .binary, context: .default)) { 44 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .failure) 45 | } 46 | } 47 | 48 | func testBinaryDecodeBoolInvalidValue() { 49 | var buffer = ByteBuffer() 50 | buffer.writeInteger(UInt8(13)) 51 | 52 | XCTAssertThrowsError(try Bool(from: &buffer, type: .bool, format: .binary, context: .default)) { 53 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .failure) 54 | } 55 | } 56 | 57 | // MARK: - Text 58 | 59 | func testTextTrueDecode() { 60 | let value = true 61 | 62 | var buffer = ByteBuffer() 63 | buffer.writeInteger(UInt8(ascii: "t")) 64 | 65 | var result: Bool? 66 | XCTAssertNoThrow(result = try Bool(from: &buffer, type: .bool, format: .text, context: .default)) 67 | XCTAssertEqual(value, result) 68 | } 69 | 70 | func testTextFalseDecode() { 71 | let value = false 72 | 73 | var buffer = ByteBuffer() 74 | buffer.writeInteger(UInt8(ascii: "f")) 75 | 76 | var result: Bool? 77 | XCTAssertNoThrow(result = try Bool(from: &buffer, type: .bool, format: .text, context: .default)) 78 | XCTAssertEqual(value, result) 79 | } 80 | 81 | func testTextDecodeBoolInvalidValue() { 82 | var buffer = ByteBuffer() 83 | buffer.writeInteger(UInt8(13)) 84 | 85 | XCTAssertThrowsError(try Bool(from: &buffer, type: .bool, format: .text, context: .default)) { 86 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .failure) 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Data/Bytes+PSQLCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class Bytes_PSQLCodableTests: XCTestCase { 6 | 7 | func testDataRoundTrip() { 8 | let data = Data((0...UInt8.max)) 9 | 10 | var buffer = ByteBuffer() 11 | data.encode(into: &buffer, context: .default) 12 | XCTAssertEqual(ByteBuffer.psqlType, .bytea) 13 | 14 | var result: Data? 15 | result = Data(from: &buffer, type: .bytea, format: .binary, context: .default) 16 | XCTAssertEqual(data, result) 17 | } 18 | 19 | func testByteBufferRoundTrip() { 20 | let bytes = ByteBuffer(bytes: (0...UInt8.max)) 21 | 22 | var buffer = ByteBuffer() 23 | bytes.encode(into: &buffer, context: .default) 24 | XCTAssertEqual(ByteBuffer.psqlType, .bytea) 25 | 26 | var result: ByteBuffer? 27 | result = ByteBuffer(from: &buffer, type: .bytea, format: .binary, context: .default) 28 | XCTAssertEqual(bytes, result) 29 | } 30 | 31 | func testEncodeSequenceWhereElementUInt8() { 32 | struct ByteSequence: Sequence, PostgresEncodable { 33 | typealias Element = UInt8 34 | typealias Iterator = Array.Iterator 35 | 36 | let bytes: [UInt8] 37 | 38 | init() { 39 | self.bytes = [UInt8]((0...UInt8.max)) 40 | } 41 | 42 | func makeIterator() -> Array.Iterator { 43 | self.bytes.makeIterator() 44 | } 45 | } 46 | 47 | let sequence = ByteSequence() 48 | var buffer = ByteBuffer() 49 | sequence.encode(into: &buffer, context: .default) 50 | XCTAssertEqual(ByteSequence.psqlType, .bytea) 51 | XCTAssertEqual(buffer.readableBytes, 256) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Data/Decimal+PSQLCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class Decimal_PSQLCodableTests: XCTestCase { 6 | 7 | func testRoundTrip() { 8 | let values: [Decimal] = [1.1, .pi, -5e-12] 9 | 10 | for value in values { 11 | var buffer = ByteBuffer() 12 | value.encode(into: &buffer, context: .default) 13 | XCTAssertEqual(Decimal.psqlType, .numeric) 14 | 15 | var result: Decimal? 16 | XCTAssertNoThrow(result = try Decimal(from: &buffer, type: .numeric, format: .binary, context: .default)) 17 | XCTAssertEqual(value, result) 18 | } 19 | } 20 | 21 | func testDecodeFailureInvalidType() { 22 | var buffer = ByteBuffer() 23 | buffer.writeInteger(Int64(0)) 24 | 25 | XCTAssertThrowsError(try Decimal(from: &buffer, type: .int8, format: .binary, context: .default)) { 26 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .typeMismatch) 27 | } 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Data/Int+PSQLCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PostgresNIO 3 | 4 | class Int_PSQLCodableTests: XCTestCase { 5 | 6 | } 7 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Data/JSON+PSQLCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Atomics 3 | import NIOCore 4 | @testable import PostgresNIO 5 | 6 | class JSON_PSQLCodableTests: XCTestCase { 7 | 8 | struct Hello: Equatable, Codable, PostgresCodable { 9 | let hello: String 10 | 11 | init(name: String) { 12 | self.hello = name 13 | } 14 | } 15 | 16 | func testRoundTrip() { 17 | var buffer = ByteBuffer() 18 | let hello = Hello(name: "world") 19 | XCTAssertNoThrow(try hello.encode(into: &buffer, context: .default)) 20 | XCTAssertEqual(Hello.psqlType, .jsonb) 21 | 22 | // verify jsonb prefix byte 23 | XCTAssertEqual(buffer.getInteger(at: buffer.readerIndex, as: UInt8.self), 1) 24 | 25 | var result: Hello? 26 | XCTAssertNoThrow(result = try Hello(from: &buffer, type: .jsonb, format: .binary, context: .default)) 27 | XCTAssertEqual(result, hello) 28 | } 29 | 30 | func testDecodeFromJSON() { 31 | var buffer = ByteBuffer() 32 | buffer.writeString(#"{"hello":"world"}"#) 33 | 34 | var result: Hello? 35 | XCTAssertNoThrow(result = try Hello(from: &buffer, type: .json, format: .binary, context: .default)) 36 | XCTAssertEqual(result, Hello(name: "world")) 37 | } 38 | 39 | func testDecodeFromJSONAsText() { 40 | let combinations : [(PostgresFormat, PostgresDataType)] = [ 41 | (.text, .json), (.text, .jsonb), 42 | ] 43 | var buffer = ByteBuffer() 44 | buffer.writeString(#"{"hello":"world"}"#) 45 | 46 | for (format, dataType) in combinations { 47 | var loopBuffer = buffer 48 | var result: Hello? 49 | XCTAssertNoThrow(result = try Hello(from: &loopBuffer, type: dataType, format: format, context: .default)) 50 | XCTAssertEqual(result, Hello(name: "world")) 51 | } 52 | } 53 | 54 | func testDecodeFromJSONBWithoutVersionPrefixByte() { 55 | var buffer = ByteBuffer() 56 | buffer.writeString(#"{"hello":"world"}"#) 57 | 58 | XCTAssertThrowsError(try Hello(from: &buffer, type: .jsonb, format: .binary, context: .default)) { 59 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .failure) 60 | } 61 | } 62 | 63 | func testDecodeFromJSONBWithWrongDataType() { 64 | var buffer = ByteBuffer() 65 | buffer.writeString(#"{"hello":"world"}"#) 66 | 67 | XCTAssertThrowsError(try Hello(from: &buffer, type: .text, format: .binary, context: .default)) { 68 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .typeMismatch) 69 | } 70 | } 71 | 72 | func testCustomEncoderIsUsed() { 73 | final class TestEncoder: PostgresJSONEncoder { 74 | let encodeHits = ManagedAtomic(0) 75 | 76 | func encode(_ value: T, into buffer: inout ByteBuffer) throws where T : Encodable { 77 | self.encodeHits.wrappingIncrement(ordering: .relaxed) 78 | } 79 | 80 | func encode(_ value: T) throws -> Data where T : Encodable { 81 | preconditionFailure() 82 | } 83 | } 84 | 85 | let hello = Hello(name: "world") 86 | let encoder = TestEncoder() 87 | var buffer = ByteBuffer() 88 | XCTAssertNoThrow(try hello.encode(into: &buffer, context: .init(jsonEncoder: encoder))) 89 | XCTAssertEqual(encoder.encodeHits.load(ordering: .relaxed), 1) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Data/RawRepresentable+PSQLCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class RawRepresentable_PSQLCodableTests: XCTestCase { 6 | 7 | enum MyRawRepresentable: Int16, PostgresCodable { 8 | case testing = 1 9 | case staging = 2 10 | case production = 3 11 | } 12 | 13 | func testRoundTrip() { 14 | let values: [MyRawRepresentable] = [.testing, .staging, .production] 15 | 16 | for value in values { 17 | var buffer = ByteBuffer() 18 | XCTAssertNoThrow(try value.encode(into: &buffer, context: .default)) 19 | XCTAssertEqual(MyRawRepresentable.psqlType, Int16.psqlType) 20 | XCTAssertEqual(buffer.readableBytes, 2) 21 | 22 | var result: MyRawRepresentable? 23 | XCTAssertNoThrow(result = try MyRawRepresentable(from: &buffer, type: Int16.psqlType, format: .binary, context: .default)) 24 | XCTAssertEqual(value, result) 25 | } 26 | } 27 | 28 | func testDecodeInvalidRawTypeValue() { 29 | var buffer = ByteBuffer() 30 | buffer.writeInteger(Int16(4)) // out of bounds 31 | 32 | XCTAssertThrowsError(try MyRawRepresentable(from: &buffer, type: Int16.psqlType, format: .binary, context: .default)) { 33 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .failure) 34 | } 35 | } 36 | 37 | func testDecodeInvalidUnderlyingTypeValue() { 38 | var buffer = ByteBuffer() 39 | buffer.writeInteger(Int32(1)) // out of bounds 40 | 41 | XCTAssertThrowsError(try MyRawRepresentable(from: &buffer, type: Int32.psqlType, format: .binary, context: .default)) { 42 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .failure) 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Data/String+PSQLCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class String_PSQLCodableTests: XCTestCase { 6 | 7 | func testEncode() { 8 | let value = "Hello World" 9 | var buffer = ByteBuffer() 10 | 11 | value.encode(into: &buffer, context: .default) 12 | 13 | XCTAssertEqual(String.psqlType, .text) 14 | XCTAssertEqual(buffer.readString(length: buffer.readableBytes), value) 15 | } 16 | 17 | func testDecodeStringFromTextVarchar() { 18 | let expected = "Hello World" 19 | var buffer = ByteBuffer() 20 | buffer.writeString(expected) 21 | 22 | let dataTypes: [PostgresDataType] = [ 23 | .text, .varchar, .name, .bpchar 24 | ] 25 | 26 | for dataType in dataTypes { 27 | var loopBuffer = buffer 28 | var result: String? 29 | XCTAssertNoThrow(result = try String(from: &loopBuffer, type: dataType, format: .binary, context: .default)) 30 | XCTAssertEqual(result, expected) 31 | } 32 | } 33 | 34 | func testDecodeFromUUID() { 35 | let uuid = UUID() 36 | var buffer = ByteBuffer() 37 | uuid.encode(into: &buffer, context: .default) 38 | 39 | var decoded: String? 40 | XCTAssertNoThrow(decoded = try String(from: &buffer, type: .uuid, format: .binary, context: .default)) 41 | XCTAssertEqual(decoded, uuid.uuidString) 42 | } 43 | 44 | func testDecodeFailureFromInvalidUUID() { 45 | let uuid = UUID() 46 | var buffer = ByteBuffer() 47 | uuid.encode(into: &buffer, context: .default) 48 | // this makes only 15 bytes readable. this should lead to an error 49 | buffer.moveReaderIndex(forwardBy: 1) 50 | 51 | XCTAssertThrowsError(try String(from: &buffer, type: .uuid, format: .binary, context: .default)) { 52 | XCTAssertEqual($0 as? PostgresDecodingError.Code, .failure) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Extensions/ByteBuffer+Utils.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | @testable import PostgresNIO 3 | 4 | extension ByteBuffer { 5 | mutating func psqlWriteBackendMessageID(_ messageID: PostgresBackendMessage.ID) { 6 | self.writeInteger(messageID.rawValue) 7 | } 8 | 9 | static func backendMessage(id: PostgresBackendMessage.ID, _ payload: (inout ByteBuffer) throws -> ()) rethrows -> ByteBuffer { 10 | var byteBuffer = ByteBuffer() 11 | try byteBuffer.writeBackendMessage(id: id, payload) 12 | return byteBuffer 13 | } 14 | 15 | mutating func writeBackendMessage(id: PostgresBackendMessage.ID, _ payload: (inout ByteBuffer) throws -> ()) rethrows { 16 | self.psqlWriteBackendMessageID(id) 17 | let lengthIndex = self.writerIndex 18 | self.writeInteger(Int32(0)) 19 | try payload(&self) 20 | let length = self.writerIndex - lengthIndex 21 | self.setInteger(Int32(length), at: lengthIndex) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Extensions/ReverseByteToMessageHandler.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | /// This is a reverse ``NIOCore/ByteToMessageHandler``. Instead of creating messages from incoming bytes 4 | /// as the normal `ByteToMessageHandler` does, this `ReverseByteToMessageHandler` creates messages 5 | /// from outgoing bytes. This is only important for testing in `EmbeddedChannel`s. 6 | class ReverseByteToMessageHandler: ChannelOutboundHandler { 7 | typealias OutboundIn = ByteBuffer 8 | typealias OutboundOut = Decoder.InboundOut 9 | 10 | let processor: NIOSingleStepByteToMessageProcessor 11 | 12 | init(_ decoder: Decoder) { 13 | self.processor = .init(decoder, maximumBufferSize: nil) 14 | } 15 | 16 | func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise?) { 17 | let buffer = self.unwrapOutboundIn(data) 18 | 19 | do { 20 | var messages = [Decoder.InboundOut]() 21 | try self.processor.process(buffer: buffer) { message in 22 | messages.append(message) 23 | } 24 | 25 | for (index, message) in messages.enumerated() { 26 | if index == messages.index(before: messages.endIndex) { 27 | context.write(self.wrapOutboundOut(message), promise: promise) 28 | } else { 29 | context.write(self.wrapOutboundOut(message), promise: nil) 30 | } 31 | } 32 | } catch { 33 | context.fireErrorCaught(error) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Extensions/ReverseMessageToByteHandler.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | 3 | /// This is a reverse ``NIOCore/ByteToMessageHandler``. Instead of creating messages from incoming bytes 4 | /// as the normal `ByteToMessageHandler` does, this `ReverseByteToMessageHandler` creates messages 5 | /// from outgoing bytes. This is only important for testing in `EmbeddedChannel`s. 6 | class ReverseMessageToByteHandler: ChannelInboundHandler { 7 | typealias InboundIn = Encoder.OutboundIn 8 | typealias InboundOut = ByteBuffer 9 | 10 | var byteBuffer: ByteBuffer! 11 | let encoder: Encoder 12 | 13 | init(_ encoder: Encoder) { 14 | self.encoder = encoder 15 | } 16 | 17 | func handlerAdded(context: ChannelHandlerContext) { 18 | self.byteBuffer = context.channel.allocator.buffer(capacity: 128) 19 | } 20 | 21 | func channelRead(context: ChannelHandlerContext, data: NIOAny) { 22 | let message = self.unwrapInboundIn(data) 23 | 24 | do { 25 | self.byteBuffer.clear() 26 | try self.encoder.encode(data: message, out: &self.byteBuffer) 27 | context.fireChannelRead(self.wrapInboundOut(self.byteBuffer)) 28 | } catch { 29 | context.fireErrorCaught(error) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/AuthenticationTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | import NIOTestUtils 4 | @testable import PostgresNIO 5 | 6 | class AuthenticationTests: XCTestCase { 7 | 8 | func testDecodeAuthentication() { 9 | var expected = [PostgresBackendMessage]() 10 | var buffer = ByteBuffer() 11 | let encoder = PSQLBackendMessageEncoder() 12 | 13 | // add ok 14 | encoder.encode(data: .authentication(.ok), out: &buffer) 15 | expected.append(.authentication(.ok)) 16 | 17 | // add kerberos 18 | encoder.encode(data: .authentication(.kerberosV5), out: &buffer) 19 | expected.append(.authentication(.kerberosV5)) 20 | 21 | // add plaintext 22 | encoder.encode(data: .authentication(.plaintext), out: &buffer) 23 | expected.append(.authentication(.plaintext)) 24 | 25 | // add md5 26 | let salt: UInt32 = 0x01_02_03_04 27 | encoder.encode(data: .authentication(.md5(salt: salt)), out: &buffer) 28 | expected.append(.authentication(.md5(salt: salt))) 29 | 30 | // add scm credential 31 | encoder.encode(data: .authentication(.scmCredential), out: &buffer) 32 | expected.append(.authentication(.scmCredential)) 33 | 34 | // add gss 35 | encoder.encode(data: .authentication(.gss), out: &buffer) 36 | expected.append(.authentication(.gss)) 37 | 38 | // add sspi 39 | encoder.encode(data: .authentication(.sspi), out: &buffer) 40 | expected.append(.authentication(.sspi)) 41 | 42 | XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( 43 | inputOutputPairs: [(buffer, expected)], 44 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: false) } 45 | )) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/BackendKeyDataTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | import NIOTestUtils 4 | @testable import PostgresNIO 5 | 6 | class BackendKeyDataTests: XCTestCase { 7 | func testDecode() { 8 | let buffer = ByteBuffer.backendMessage(id: .backendKeyData) { buffer in 9 | buffer.writeInteger(Int32(1234)) 10 | buffer.writeInteger(Int32(4567)) 11 | } 12 | 13 | let expectedInOuts = [ 14 | (buffer, [PostgresBackendMessage.backendKeyData(.init(processID: 1234, secretKey: 4567))]), 15 | ] 16 | 17 | XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( 18 | inputOutputPairs: expectedInOuts, 19 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: false) })) 20 | } 21 | 22 | func testDecodeInvalidLength() { 23 | var buffer = ByteBuffer() 24 | buffer.psqlWriteBackendMessageID(.backendKeyData) 25 | buffer.writeInteger(Int32(11)) 26 | buffer.writeInteger(Int32(1234)) 27 | buffer.writeInteger(Int32(4567)) 28 | 29 | let expected = [ 30 | (buffer, [PostgresBackendMessage.backendKeyData(.init(processID: 1234, secretKey: 4567))]), 31 | ] 32 | 33 | XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( 34 | inputOutputPairs: expected, 35 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: false) })) { 36 | XCTAssert($0 is PostgresMessageDecodingError) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/BindTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class BindTests: XCTestCase { 6 | 7 | func testEncodeBind() { 8 | var bindings = PostgresBindings() 9 | bindings.append("Hello", context: .default) 10 | bindings.append("World", context: .default) 11 | 12 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 13 | 14 | encoder.bind(portalName: "", preparedStatementName: "", bind: bindings) 15 | var byteBuffer = encoder.flushBuffer() 16 | 17 | XCTAssertEqual(byteBuffer.readableBytes, 37) 18 | XCTAssertEqual(PostgresFrontendMessage.ID.bind.rawValue, byteBuffer.readInteger(as: UInt8.self)) 19 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), 36) 20 | XCTAssertEqual("", byteBuffer.readNullTerminatedString()) 21 | XCTAssertEqual("", byteBuffer.readNullTerminatedString()) 22 | // the number of parameters 23 | XCTAssertEqual(2, byteBuffer.readInteger(as: Int16.self)) 24 | // all (two) parameters have the same format (binary) 25 | XCTAssertEqual(1, byteBuffer.readInteger(as: Int16.self)) 26 | XCTAssertEqual(1, byteBuffer.readInteger(as: Int16.self)) 27 | 28 | // read number of parameters 29 | XCTAssertEqual(2, byteBuffer.readInteger(as: Int16.self)) 30 | 31 | // hello length 32 | XCTAssertEqual(5, byteBuffer.readInteger(as: Int32.self)) 33 | XCTAssertEqual("Hello", byteBuffer.readString(length: 5)) 34 | 35 | // world length 36 | XCTAssertEqual(5, byteBuffer.readInteger(as: Int32.self)) 37 | XCTAssertEqual("World", byteBuffer.readString(length: 5)) 38 | 39 | // all response values have the same format: therefore one format byte is next 40 | XCTAssertEqual(1, byteBuffer.readInteger(as: Int16.self)) 41 | // all response values have the same format (binary) 42 | XCTAssertEqual(1, byteBuffer.readInteger(as: Int16.self)) 43 | 44 | // nothing left to read 45 | XCTAssertEqual(byteBuffer.readableBytes, 0) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/CancelTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class CancelTests: XCTestCase { 6 | 7 | func testEncodeCancel() { 8 | let processID: Int32 = 1234 9 | let secretKey: Int32 = 4567 10 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 11 | encoder.cancel(processID: processID, secretKey: secretKey) 12 | var byteBuffer = encoder.flushBuffer() 13 | 14 | XCTAssertEqual(byteBuffer.readableBytes, 16) 15 | XCTAssertEqual(16, byteBuffer.readInteger(as: Int32.self)) // payload length 16 | XCTAssertEqual(80877102, byteBuffer.readInteger(as: Int32.self)) // cancel request code 17 | XCTAssertEqual(processID, byteBuffer.readInteger(as: Int32.self)) 18 | XCTAssertEqual(secretKey, byteBuffer.readInteger(as: Int32.self)) 19 | XCTAssertEqual(byteBuffer.readableBytes, 0) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/CloseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class CloseTests: XCTestCase { 6 | func testEncodeClosePortal() { 7 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 8 | encoder.closePortal("Hello") 9 | var byteBuffer = encoder.flushBuffer() 10 | 11 | XCTAssertEqual(byteBuffer.readableBytes, 12) 12 | XCTAssertEqual(PostgresFrontendMessage.ID.close.rawValue, byteBuffer.readInteger(as: UInt8.self)) 13 | XCTAssertEqual(11, byteBuffer.readInteger(as: Int32.self)) 14 | XCTAssertEqual(UInt8(ascii: "P"), byteBuffer.readInteger(as: UInt8.self)) 15 | XCTAssertEqual("Hello", byteBuffer.readNullTerminatedString()) 16 | XCTAssertEqual(byteBuffer.readableBytes, 0) 17 | } 18 | 19 | func testEncodeCloseUnnamedStatement() { 20 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 21 | encoder.closePreparedStatement("") 22 | var byteBuffer = encoder.flushBuffer() 23 | 24 | XCTAssertEqual(byteBuffer.readableBytes, 7) 25 | XCTAssertEqual(PostgresFrontendMessage.ID.close.rawValue, byteBuffer.readInteger(as: UInt8.self)) 26 | XCTAssertEqual(6, byteBuffer.readInteger(as: Int32.self)) 27 | XCTAssertEqual(UInt8(ascii: "S"), byteBuffer.readInteger(as: UInt8.self)) 28 | XCTAssertEqual("", byteBuffer.readNullTerminatedString()) 29 | XCTAssertEqual(byteBuffer.readableBytes, 0) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/DescribeTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class DescribeTests: XCTestCase { 6 | 7 | func testEncodeDescribePortal() { 8 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 9 | encoder.describePortal("Hello") 10 | var byteBuffer = encoder.flushBuffer() 11 | 12 | XCTAssertEqual(byteBuffer.readableBytes, 12) 13 | XCTAssertEqual(PostgresFrontendMessage.ID.describe.rawValue, byteBuffer.readInteger(as: UInt8.self)) 14 | XCTAssertEqual(11, byteBuffer.readInteger(as: Int32.self)) 15 | XCTAssertEqual(UInt8(ascii: "P"), byteBuffer.readInteger(as: UInt8.self)) 16 | XCTAssertEqual("Hello", byteBuffer.readNullTerminatedString()) 17 | XCTAssertEqual(byteBuffer.readableBytes, 0) 18 | } 19 | 20 | func testEncodeDescribeUnnamedStatement() { 21 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 22 | encoder.describePreparedStatement("") 23 | var byteBuffer = encoder.flushBuffer() 24 | 25 | XCTAssertEqual(byteBuffer.readableBytes, 7) 26 | XCTAssertEqual(PostgresFrontendMessage.ID.describe.rawValue, byteBuffer.readInteger(as: UInt8.self)) 27 | XCTAssertEqual(6, byteBuffer.readInteger(as: Int32.self)) 28 | XCTAssertEqual(UInt8(ascii: "S"), byteBuffer.readInteger(as: UInt8.self)) 29 | XCTAssertEqual("", byteBuffer.readNullTerminatedString()) 30 | XCTAssertEqual(byteBuffer.readableBytes, 0) 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/ErrorResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | import NIOTestUtils 4 | @testable import PostgresNIO 5 | 6 | class ErrorResponseTests: XCTestCase { 7 | 8 | func testDecode() { 9 | let fields: [PostgresBackendMessage.Field : String] = [ 10 | .file: "auth.c", 11 | .routine: "auth_failed", 12 | .line: "334", 13 | .localizedSeverity: "FATAL", 14 | .sqlState: "28P01", 15 | .severity: "FATAL", 16 | .message: "password authentication failed for user \"postgre3\"", 17 | ] 18 | 19 | let buffer = ByteBuffer.backendMessage(id: .error) { buffer in 20 | fields.forEach { (key, value) in 21 | buffer.writeInteger(key.rawValue, as: UInt8.self) 22 | buffer.writeNullTerminatedString(value) 23 | } 24 | buffer.writeInteger(0, as: UInt8.self) // signal done 25 | } 26 | 27 | let expectedInOuts = [ 28 | (buffer, [PostgresBackendMessage.error(.init(fields: fields))]), 29 | ] 30 | 31 | XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( 32 | inputOutputPairs: expectedInOuts, 33 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: false) })) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/ExecuteTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class ExecuteTests: XCTestCase { 6 | 7 | func testEncodeExecute() { 8 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 9 | encoder.execute(portalName: "", maxNumberOfRows: 0) 10 | var byteBuffer = encoder.flushBuffer() 11 | 12 | XCTAssertEqual(byteBuffer.readableBytes, 10) // 1 (id) + 4 (length) + 1 (empty null terminated string) + 4 (count) 13 | XCTAssertEqual(PostgresFrontendMessage.ID.execute.rawValue, byteBuffer.readInteger(as: UInt8.self)) 14 | XCTAssertEqual(9, byteBuffer.readInteger(as: Int32.self)) // length 15 | XCTAssertEqual("", byteBuffer.readNullTerminatedString()) 16 | XCTAssertEqual(0, byteBuffer.readInteger(as: Int32.self)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/NotificationResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | import NIOTestUtils 4 | @testable import PostgresNIO 5 | 6 | class NotificationResponseTests: XCTestCase { 7 | 8 | func testDecode() { 9 | let expected: [PostgresBackendMessage] = [ 10 | .notification(.init(backendPID: 123, channel: "test", payload: "hello")), 11 | .notification(.init(backendPID: 123, channel: "test", payload: "world")), 12 | .notification(.init(backendPID: 123, channel: "foo", payload: "bar")) 13 | ] 14 | 15 | var buffer = ByteBuffer() 16 | expected.forEach { message in 17 | guard case .notification(let notification) = message else { 18 | return XCTFail("Expected only to get notifications here!") 19 | } 20 | 21 | buffer.writeBackendMessage(id: .notificationResponse) { buffer in 22 | buffer.writeInteger(notification.backendPID) 23 | buffer.writeNullTerminatedString(notification.channel) 24 | buffer.writeNullTerminatedString(notification.payload) 25 | } 26 | } 27 | 28 | XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( 29 | inputOutputPairs: [(buffer, expected)], 30 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) 31 | } 32 | 33 | func testDecodeFailureBecauseOfMissingNullTermination() { 34 | var buffer = ByteBuffer() 35 | buffer.writeBackendMessage(id: .notificationResponse) { buffer in 36 | buffer.writeInteger(Int32(123)) 37 | buffer.writeString("test") 38 | buffer.writeString("hello") 39 | } 40 | 41 | XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( 42 | inputOutputPairs: [(buffer, [])], 43 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) { 44 | XCTAssert($0 is PostgresMessageDecodingError) 45 | } 46 | } 47 | 48 | func testDecodeFailureBecauseOfMissingNullTerminationInValue() { 49 | var buffer = ByteBuffer() 50 | buffer.writeBackendMessage(id: .notificationResponse) { buffer in 51 | buffer.writeInteger(Int32(123)) 52 | buffer.writeNullTerminatedString("hello") 53 | buffer.writeString("world") 54 | } 55 | 56 | XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( 57 | inputOutputPairs: [(buffer, [])], 58 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) { 59 | XCTAssert($0 is PostgresMessageDecodingError) 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/ParameterDescriptionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | import NIOTestUtils 4 | @testable import PostgresNIO 5 | 6 | class ParameterDescriptionTests: XCTestCase { 7 | 8 | func testDecode() { 9 | let expected: [PostgresBackendMessage] = [ 10 | .parameterDescription(.init(dataTypes: [.bool, .varchar, .uuid, .json, .jsonbArray])), 11 | ] 12 | 13 | var buffer = ByteBuffer() 14 | expected.forEach { message in 15 | guard case .parameterDescription(let description) = message else { 16 | return XCTFail("Expected only to get parameter descriptions here!") 17 | } 18 | 19 | buffer.writeBackendMessage(id: .parameterDescription) { buffer in 20 | buffer.writeInteger(Int16(description.dataTypes.count)) 21 | 22 | description.dataTypes.forEach { dataType in 23 | buffer.writeInteger(dataType.rawValue) 24 | } 25 | } 26 | } 27 | 28 | XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( 29 | inputOutputPairs: [(buffer, expected)], 30 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) 31 | } 32 | 33 | func testDecodeWithNegativeCount() { 34 | let dataTypes: [PostgresDataType] = [.bool, .varchar, .uuid, .json, .jsonbArray] 35 | var buffer = ByteBuffer() 36 | buffer.writeBackendMessage(id: .parameterDescription) { buffer in 37 | buffer.writeInteger(Int16(-4)) 38 | 39 | dataTypes.forEach { dataType in 40 | buffer.writeInteger(dataType.rawValue) 41 | } 42 | } 43 | 44 | XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( 45 | inputOutputPairs: [(buffer, [])], 46 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) { 47 | XCTAssert($0 is PostgresMessageDecodingError) 48 | } 49 | } 50 | 51 | func testDecodeColumnCountDoesntMatchMessageLength() { 52 | let dataTypes: [PostgresDataType] = [.bool, .varchar, .uuid, .json, .jsonbArray] 53 | var buffer = ByteBuffer() 54 | buffer.writeBackendMessage(id: .parameterDescription) { buffer in 55 | // means three columns comming, but 5 are in the buffer actually. 56 | buffer.writeInteger(Int16(3)) 57 | 58 | dataTypes.forEach { dataType in 59 | buffer.writeInteger(dataType.rawValue) 60 | } 61 | } 62 | 63 | XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( 64 | inputOutputPairs: [(buffer, [])], 65 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) { 66 | XCTAssert($0 is PostgresMessageDecodingError) 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/ParseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class ParseTests: XCTestCase { 6 | func testEncode() { 7 | let preparedStatementName = "test" 8 | let query = "SELECT version()" 9 | let parameters: [PostgresDataType] = [.bool, .int8, .bytea, .varchar, .text, .uuid, .json, .jsonbArray] 10 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 11 | encoder.parse( 12 | preparedStatementName: preparedStatementName, 13 | query: query, 14 | parameters: parameters 15 | ) 16 | var byteBuffer = encoder.flushBuffer() 17 | 18 | let length: Int = 1 + 4 + (preparedStatementName.count + 1) + (query.count + 1) + 2 + parameters.count * 4 19 | 20 | // 1 id 21 | // + 4 length 22 | // + 4 preparedStatement (3 + 1 null terminator) 23 | // + 1 query () 24 | 25 | XCTAssertEqual(byteBuffer.readableBytes, length) 26 | XCTAssertEqual(byteBuffer.readInteger(as: UInt8.self), PostgresFrontendMessage.ID.parse.rawValue) 27 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(length - 1)) 28 | XCTAssertEqual(byteBuffer.readNullTerminatedString(), preparedStatementName) 29 | XCTAssertEqual(byteBuffer.readNullTerminatedString(), query) 30 | XCTAssertEqual(byteBuffer.readInteger(as: UInt16.self), UInt16(parameters.count)) 31 | for dataType in parameters { 32 | XCTAssertEqual(byteBuffer.readInteger(as: UInt32.self), dataType.rawValue) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/PasswordTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class PasswordTests: XCTestCase { 6 | 7 | func testEncodePassword() { 8 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 9 | // md522d085ed8dc3377968dc1c1a40519a2a = "abc123" with salt 1, 2, 3, 4 10 | let password = "md522d085ed8dc3377968dc1c1a40519a2a" 11 | encoder.password(password.utf8) 12 | var byteBuffer = encoder.flushBuffer() 13 | 14 | let expectedLength = 41 // 1 (id) + 4 (length) + 35 (string) + 1 (null termination) 15 | 16 | XCTAssertEqual(byteBuffer.readableBytes, expectedLength) 17 | XCTAssertEqual(byteBuffer.readInteger(as: UInt8.self), PostgresFrontendMessage.ID.password.rawValue) 18 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(expectedLength - 1)) // length 19 | XCTAssertEqual(byteBuffer.readNullTerminatedString(), "md522d085ed8dc3377968dc1c1a40519a2a") 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/ReadyForQueryTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | import NIOTestUtils 4 | @testable import PostgresNIO 5 | 6 | class ReadyForQueryTests: XCTestCase { 7 | 8 | func testDecode() { 9 | var buffer = ByteBuffer() 10 | 11 | let states: [PostgresBackendMessage.TransactionState] = [ 12 | .idle, 13 | .inFailedTransaction, 14 | .inTransaction, 15 | ] 16 | 17 | states.forEach { state in 18 | buffer.writeBackendMessage(id: .readyForQuery) { buffer in 19 | switch state { 20 | case .idle: 21 | buffer.writeInteger(UInt8(ascii: "I")) 22 | case .inTransaction: 23 | buffer.writeInteger(UInt8(ascii: "T")) 24 | case .inFailedTransaction: 25 | buffer.writeInteger(UInt8(ascii: "E")) 26 | } 27 | } 28 | } 29 | 30 | let expected = states.map { state -> PostgresBackendMessage in 31 | .readyForQuery(state) 32 | } 33 | 34 | XCTAssertNoThrow(try ByteToMessageDecoderVerifier.verifyDecoder( 35 | inputOutputPairs: [(buffer, expected)], 36 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) 37 | 38 | } 39 | 40 | func testDecodeInvalidLength() { 41 | var buffer = ByteBuffer() 42 | 43 | buffer.writeBackendMessage(id: .readyForQuery) { buffer in 44 | buffer.writeInteger(UInt8(ascii: "I")) 45 | buffer.writeInteger(UInt8(ascii: "I")) 46 | } 47 | 48 | XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( 49 | inputOutputPairs: [(buffer, [])], 50 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) { 51 | XCTAssert($0 is PostgresMessageDecodingError) 52 | } 53 | } 54 | 55 | func testDecodeUnexpectedAscii() { 56 | var buffer = ByteBuffer() 57 | 58 | buffer.writeBackendMessage(id: .readyForQuery) { buffer in 59 | buffer.writeInteger(UInt8(ascii: "F")) 60 | } 61 | 62 | XCTAssertThrowsError(try ByteToMessageDecoderVerifier.verifyDecoder( 63 | inputOutputPairs: [(buffer, [])], 64 | decoderFactory: { PostgresBackendMessageDecoder(hasAlreadyReceivedBytes: true) })) { 65 | XCTAssert($0 is PostgresMessageDecodingError) 66 | } 67 | } 68 | 69 | func testDebugDescription() { 70 | XCTAssertEqual(String(reflecting: PostgresBackendMessage.TransactionState.idle), ".idle") 71 | XCTAssertEqual(String(reflecting: PostgresBackendMessage.TransactionState.inTransaction), ".inTransaction") 72 | XCTAssertEqual(String(reflecting: PostgresBackendMessage.TransactionState.inFailedTransaction), ".inFailedTransaction") 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/SASLInitialResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class SASLInitialResponseTests: XCTestCase { 6 | 7 | func testEncode() { 8 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 9 | let saslMechanism = "hello" 10 | let initialData: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7] 11 | encoder.saslInitialResponse(mechanism: saslMechanism, bytes: initialData) 12 | var byteBuffer = encoder.flushBuffer() 13 | 14 | let length: Int = 1 + 4 + (saslMechanism.count + 1) + 4 + initialData.count 15 | 16 | // 1 id 17 | // + 4 length 18 | // + 6 saslMechanism (5 + 1 null terminator) 19 | // + 4 initialData length 20 | // + 8 initialData 21 | 22 | XCTAssertEqual(byteBuffer.readableBytes, length) 23 | XCTAssertEqual(byteBuffer.readInteger(as: UInt8.self), PostgresFrontendMessage.ID.saslInitialResponse.rawValue) 24 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(length - 1)) 25 | XCTAssertEqual(byteBuffer.readNullTerminatedString(), saslMechanism) 26 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(initialData.count)) 27 | XCTAssertEqual(byteBuffer.readBytes(length: initialData.count), initialData) 28 | XCTAssertEqual(byteBuffer.readableBytes, 0) 29 | } 30 | 31 | func testEncodeWithoutData() { 32 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 33 | let saslMechanism = "hello" 34 | let initialData: [UInt8] = [] 35 | encoder.saslInitialResponse(mechanism: saslMechanism, bytes: initialData) 36 | var byteBuffer = encoder.flushBuffer() 37 | 38 | let length: Int = 1 + 4 + (saslMechanism.count + 1) + 4 + initialData.count 39 | 40 | // 1 id 41 | // + 4 length 42 | // + 6 saslMechanism (5 + 1 null terminator) 43 | // + 4 initialData length 44 | // + 0 initialData 45 | 46 | XCTAssertEqual(byteBuffer.readableBytes, length) 47 | XCTAssertEqual(byteBuffer.readInteger(as: UInt8.self), PostgresFrontendMessage.ID.saslInitialResponse.rawValue) 48 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(length - 1)) 49 | XCTAssertEqual(byteBuffer.readNullTerminatedString(), saslMechanism) 50 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(-1)) 51 | XCTAssertEqual(byteBuffer.readBytes(length: initialData.count), initialData) 52 | XCTAssertEqual(byteBuffer.readableBytes, 0) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/SASLResponseTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class SASLResponseTests: XCTestCase { 6 | 7 | func testEncodeWithData() { 8 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 9 | let data: [UInt8] = [0, 1, 2, 3, 4, 5, 6, 7] 10 | encoder.saslResponse(data) 11 | var byteBuffer = encoder.flushBuffer() 12 | 13 | let length: Int = 1 + 4 + (data.count) 14 | 15 | XCTAssertEqual(byteBuffer.readableBytes, length) 16 | XCTAssertEqual(byteBuffer.readInteger(as: UInt8.self), PostgresFrontendMessage.ID.saslResponse.rawValue) 17 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(length - 1)) 18 | XCTAssertEqual(byteBuffer.readBytes(length: data.count), data) 19 | XCTAssertEqual(byteBuffer.readableBytes, 0) 20 | } 21 | 22 | func testEncodeWithoutData() { 23 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 24 | let data: [UInt8] = [] 25 | encoder.saslResponse(data) 26 | var byteBuffer = encoder.flushBuffer() 27 | 28 | let length: Int = 1 + 4 29 | 30 | XCTAssertEqual(byteBuffer.readableBytes, length) 31 | XCTAssertEqual(byteBuffer.readInteger(as: UInt8.self), PostgresFrontendMessage.ID.saslResponse.rawValue) 32 | XCTAssertEqual(byteBuffer.readInteger(as: Int32.self), Int32(length - 1)) 33 | XCTAssertEqual(byteBuffer.readableBytes, 0) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/Messages/SSLRequestTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class SSLRequestTests: XCTestCase { 6 | 7 | func testSSLRequest() { 8 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 9 | encoder.ssl() 10 | var byteBuffer = encoder.flushBuffer() 11 | 12 | let byteBufferLength = Int32(byteBuffer.readableBytes) 13 | XCTAssertEqual(byteBufferLength, byteBuffer.readInteger()) 14 | XCTAssertEqual(PostgresFrontendMessage.SSLRequest.requestCode, byteBuffer.readInteger()) 15 | 16 | XCTAssertEqual(byteBuffer.readableBytes, 0) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/PSQLFrontendMessageTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import NIOCore 3 | @testable import PostgresNIO 4 | 5 | class PSQLFrontendMessageTests: XCTestCase { 6 | 7 | // MARK: ID 8 | 9 | func testMessageIDs() { 10 | XCTAssertEqual(PostgresFrontendMessage.ID.bind.rawValue, UInt8(ascii: "B")) 11 | XCTAssertEqual(PostgresFrontendMessage.ID.close.rawValue, UInt8(ascii: "C")) 12 | XCTAssertEqual(PostgresFrontendMessage.ID.describe.rawValue, UInt8(ascii: "D")) 13 | XCTAssertEqual(PostgresFrontendMessage.ID.execute.rawValue, UInt8(ascii: "E")) 14 | XCTAssertEqual(PostgresFrontendMessage.ID.flush.rawValue, UInt8(ascii: "H")) 15 | XCTAssertEqual(PostgresFrontendMessage.ID.parse.rawValue, UInt8(ascii: "P")) 16 | XCTAssertEqual(PostgresFrontendMessage.ID.password.rawValue, UInt8(ascii: "p")) 17 | XCTAssertEqual(PostgresFrontendMessage.ID.saslInitialResponse.rawValue, UInt8(ascii: "p")) 18 | XCTAssertEqual(PostgresFrontendMessage.ID.saslResponse.rawValue, UInt8(ascii: "p")) 19 | XCTAssertEqual(PostgresFrontendMessage.ID.sync.rawValue, UInt8(ascii: "S")) 20 | XCTAssertEqual(PostgresFrontendMessage.ID.terminate.rawValue, UInt8(ascii: "X")) 21 | } 22 | 23 | // MARK: Encoder 24 | 25 | func testEncodeFlush() { 26 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 27 | encoder.flush() 28 | var byteBuffer = encoder.flushBuffer() 29 | 30 | XCTAssertEqual(byteBuffer.readableBytes, 5) 31 | XCTAssertEqual(PostgresFrontendMessage.ID.flush.rawValue, byteBuffer.readInteger(as: UInt8.self)) 32 | XCTAssertEqual(4, byteBuffer.readInteger(as: Int32.self)) // payload length 33 | } 34 | 35 | func testEncodeSync() { 36 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 37 | encoder.sync() 38 | var byteBuffer = encoder.flushBuffer() 39 | 40 | XCTAssertEqual(byteBuffer.readableBytes, 5) 41 | XCTAssertEqual(PostgresFrontendMessage.ID.sync.rawValue, byteBuffer.readInteger(as: UInt8.self)) 42 | XCTAssertEqual(4, byteBuffer.readInteger(as: Int32.self)) // payload length 43 | } 44 | 45 | func testEncodeTerminate() { 46 | var encoder = PostgresFrontendMessageEncoder(buffer: .init()) 47 | encoder.terminate() 48 | var byteBuffer = encoder.flushBuffer() 49 | 50 | XCTAssertEqual(byteBuffer.readableBytes, 5) 51 | XCTAssertEqual(PostgresFrontendMessage.ID.terminate.rawValue, byteBuffer.readInteger(as: UInt8.self)) 52 | XCTAssertEqual(4, byteBuffer.readInteger(as: Int32.self)) // payload length 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/PSQLMetadataTests.swift: -------------------------------------------------------------------------------- 1 | import NIOCore 2 | import XCTest 3 | @testable import PostgresNIO 4 | 5 | class PSQLMetadataTests: XCTestCase { 6 | func testSelect() { 7 | XCTAssertEqual(100, PostgresQueryMetadata(string: "SELECT 100")?.rows) 8 | XCTAssertNotNil(PostgresQueryMetadata(string: "SELECT")) 9 | XCTAssertNil(PostgresQueryMetadata(string: "SELECT")?.rows) 10 | XCTAssertNil(PostgresQueryMetadata(string: "SELECT 100 100")) 11 | } 12 | 13 | func testUpdate() { 14 | XCTAssertEqual(100, PostgresQueryMetadata(string: "UPDATE 100")?.rows) 15 | XCTAssertNil(PostgresQueryMetadata(string: "UPDATE")) 16 | XCTAssertNil(PostgresQueryMetadata(string: "UPDATE 100 100")) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/PostgresCellTests.swift: -------------------------------------------------------------------------------- 1 | @testable import PostgresNIO 2 | import XCTest 3 | import NIOCore 4 | 5 | final class PostgresCellTests: XCTestCase { 6 | func testDecodingANonOptionalString() { 7 | let cell = PostgresCell( 8 | bytes: ByteBuffer(string: "Hello world"), 9 | dataType: .text, 10 | format: .binary, 11 | columnName: "hello", 12 | columnIndex: 1 13 | ) 14 | 15 | var result: String? 16 | XCTAssertNoThrow(result = try cell.decode(String.self, context: .default)) 17 | XCTAssertEqual(result, "Hello world") 18 | } 19 | 20 | func testDecodingAnOptionalString() { 21 | let cell = PostgresCell( 22 | bytes: nil, 23 | dataType: .text, 24 | format: .binary, 25 | columnName: "hello", 26 | columnIndex: 1 27 | ) 28 | 29 | var result: String? = "test" 30 | XCTAssertNoThrow(result = try cell.decode(String?.self, context: .default)) 31 | XCTAssertNil(result) 32 | } 33 | 34 | func testDecodingFailure() { 35 | let cell = PostgresCell( 36 | bytes: ByteBuffer(string: "Hello world"), 37 | dataType: .text, 38 | format: .binary, 39 | columnName: "hello", 40 | columnIndex: 1 41 | ) 42 | 43 | XCTAssertThrowsError(try cell.decode(Int?.self, context: .default)) { 44 | guard let error = $0 as? PostgresDecodingError else { 45 | return XCTFail("Unexpected error") 46 | } 47 | 48 | XCTAssertEqual(error.file, #fileID) 49 | XCTAssertEqual(error.line, #line - 6) 50 | XCTAssertEqual(error.code, .typeMismatch) 51 | XCTAssertEqual(error.columnName, "hello") 52 | XCTAssertEqual(error.columnIndex, 1) 53 | XCTAssert(error.targetType == Int?.self) 54 | XCTAssertEqual(error.postgresType, .text) 55 | XCTAssertEqual(error.postgresFormat, .binary) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/New/PostgresCodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PostgresNIO 3 | import NIOCore 4 | 5 | final class PostgresCodableTests: XCTestCase { 6 | 7 | func testDecodeAnOptionalFromARow() { 8 | let row = PostgresRow( 9 | data: .makeTestDataRow(nil, ByteBuffer(string: "Hello world!")), 10 | lookupTable: ["id": 0, "name": 1], 11 | columns: [ 12 | RowDescription.Column( 13 | name: "id", 14 | tableOID: 1, 15 | columnAttributeNumber: 1, 16 | dataType: .text, 17 | dataTypeSize: 0, 18 | dataTypeModifier: 0, 19 | format: .binary 20 | ), 21 | RowDescription.Column( 22 | name: "id", 23 | tableOID: 1, 24 | columnAttributeNumber: 1, 25 | dataType: .text, 26 | dataTypeSize: 0, 27 | dataTypeModifier: 0, 28 | format: .binary 29 | ) 30 | ] 31 | ) 32 | 33 | var result: (String?, String?) 34 | XCTAssertNoThrow(result = try row.decode((String?, String?).self, context: .default)) 35 | XCTAssertNil(result.0) 36 | XCTAssertEqual(result.1, "Hello world!") 37 | } 38 | 39 | func testDecodeMissingValueError() { 40 | let row = PostgresRow( 41 | data: .makeTestDataRow(nil), 42 | lookupTable: ["name": 0], 43 | columns: [ 44 | RowDescription.Column( 45 | name: "id", 46 | tableOID: 1, 47 | columnAttributeNumber: 1, 48 | dataType: .text, 49 | dataTypeSize: 0, 50 | dataTypeModifier: 0, 51 | format: .binary 52 | ) 53 | ] 54 | ) 55 | 56 | XCTAssertThrowsError(try row.decode(String.self, context: .default)) { 57 | XCTAssertEqual(($0 as? PostgresDecodingError)?.line, #line - 1) 58 | XCTAssertEqual(($0 as? PostgresDecodingError)?.file, #fileID) 59 | 60 | XCTAssertEqual(($0 as? PostgresDecodingError)?.code, .missingData) 61 | XCTAssert(($0 as? PostgresDecodingError)?.targetType == String.self) 62 | XCTAssertEqual(($0 as? PostgresDecodingError)?.postgresType, .text) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/Utilities.swift: -------------------------------------------------------------------------------- 1 | import Logging 2 | 3 | extension Logger { 4 | static var psqlTest: Logger { 5 | var logger = Logger(label: "psql.test") 6 | logger.logLevel = .info 7 | return logger 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/PostgresNIOTests/Utilities/PostgresJSONCodingTests.swift: -------------------------------------------------------------------------------- 1 | import Atomics 2 | import NIOCore 3 | import XCTest 4 | import PostgresNIO 5 | 6 | class PostgresJSONCodingTests: XCTestCase { 7 | // https://github.com/vapor/postgres-nio/issues/126 8 | func testCustomJSONEncoder() { 9 | let previousDefaultJSONEncoder = PostgresNIO._defaultJSONEncoder 10 | defer { 11 | PostgresNIO._defaultJSONEncoder = previousDefaultJSONEncoder 12 | } 13 | final class CustomJSONEncoder: PostgresJSONEncoder { 14 | let counter = ManagedAtomic(0) 15 | func encode(_ value: T) throws -> Data where T : Encodable { 16 | self.counter.wrappingIncrement(ordering: .relaxed) 17 | return try JSONEncoder().encode(value) 18 | } 19 | } 20 | struct Object: Codable { 21 | var foo: Int 22 | var bar: Int 23 | } 24 | let customJSONEncoder = CustomJSONEncoder() 25 | XCTAssertEqual(customJSONEncoder.counter.load(ordering: .relaxed), 0) 26 | PostgresNIO._defaultJSONEncoder = customJSONEncoder 27 | XCTAssertNoThrow(try PostgresData(json: Object(foo: 1, bar: 2))) 28 | XCTAssertEqual(customJSONEncoder.counter.load(ordering: .relaxed), 1) 29 | 30 | let customJSONBEncoder = CustomJSONEncoder() 31 | XCTAssertEqual(customJSONBEncoder.counter.load(ordering: .relaxed), 0) 32 | PostgresNIO._defaultJSONEncoder = customJSONBEncoder 33 | XCTAssertNoThrow(try PostgresData(json: Object(foo: 1, bar: 2))) 34 | XCTAssertEqual(customJSONBEncoder.counter.load(ordering: .relaxed), 1) 35 | } 36 | 37 | // https://github.com/vapor/postgres-nio/issues/126 38 | func testCustomJSONDecoder() { 39 | let previousDefaultJSONDecoder = PostgresNIO._defaultJSONDecoder 40 | defer { 41 | PostgresNIO._defaultJSONDecoder = previousDefaultJSONDecoder 42 | } 43 | final class CustomJSONDecoder: PostgresJSONDecoder { 44 | let counter = ManagedAtomic(0) 45 | func decode(_ type: T.Type, from data: Data) throws -> T where T : Decodable { 46 | self.counter.wrappingIncrement(ordering: .relaxed) 47 | return try JSONDecoder().decode(type, from: data) 48 | } 49 | } 50 | struct Object: Codable { 51 | var foo: Int 52 | var bar: Int 53 | } 54 | let customJSONDecoder = CustomJSONDecoder() 55 | XCTAssertEqual(customJSONDecoder.counter.load(ordering: .relaxed), 0) 56 | PostgresNIO._defaultJSONDecoder = customJSONDecoder 57 | XCTAssertNoThrow(try PostgresData(json: Object(foo: 1, bar: 2)).json(as: Object.self)) 58 | XCTAssertEqual(customJSONDecoder.counter.load(ordering: .relaxed), 1) 59 | 60 | let customJSONBDecoder = CustomJSONDecoder() 61 | XCTAssertEqual(customJSONBDecoder.counter.load(ordering: .relaxed), 0) 62 | PostgresNIO._defaultJSONDecoder = customJSONBDecoder 63 | XCTAssertNoThrow(try PostgresData(json: Object(foo: 1, bar: 2)).json(as: Object.self)) 64 | XCTAssertEqual(customJSONBDecoder.counter.load(ordering: .relaxed), 1) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /dev/generate-postgresrowsequence-multi-decode.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | here="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | 7 | function genWithContextParameter() { 8 | how_many=$1 9 | 10 | if [[ $how_many -ne 1 ]] ; then 11 | echo "" 12 | fi 13 | 14 | echo " @inlinable" 15 | echo " @_alwaysEmitIntoClient" 16 | echo -n " public func decode(_: (T0" 22 | for ((n = 1; n<$how_many; n +=1)); do 23 | echo -n ", T$(($n))" 24 | done 25 | echo -n ").Type, context: PostgresDecodingContext, file: String = #fileID, line: Int = #line) " 26 | 27 | echo -n "-> AsyncThrowingMapSequence {" 32 | 33 | echo " self.map { row in" 34 | 35 | if [[ $how_many -eq 1 ]] ; then 36 | echo " try row.decode(T0.self, context: context, file: file, line: line)" 37 | else 38 | echo -n " try row.decode((T0" 39 | 40 | for ((n = 1; n<$how_many; n +=1)); do 41 | echo -n ", T$n" 42 | done 43 | echo ").self, context: context, file: file, line: line)" 44 | 45 | fi 46 | 47 | echo " }" 48 | echo " }" 49 | } 50 | 51 | function genWithoutContextParameter() { 52 | how_many=$1 53 | 54 | echo "" 55 | 56 | echo " @inlinable" 57 | echo " @_alwaysEmitIntoClient" 58 | echo -n " public func decode(_: (T0" 64 | for ((n = 1; n<$how_many; n +=1)); do 65 | echo -n ", T$(($n))" 66 | done 67 | echo -n ").Type, file: String = #fileID, line: Int = #line) " 68 | echo -n "-> AsyncThrowingMapSequence {" 73 | 74 | echo -n " self.decode(" 75 | if [[ $how_many -eq 1 ]] ; then 76 | echo -n "T0.self" 77 | else 78 | echo -n "(T0" 79 | for ((n = 1; n<$how_many; n +=1)); do 80 | echo -n ", T$(($n))" 81 | done 82 | echo -n ").self" 83 | fi 84 | echo ", context: .default, file: file, line: line)" 85 | echo " }" 86 | } 87 | 88 | grep -q "ByteBuffer" "${BASH_SOURCE[0]}" || { 89 | echo >&2 "ERROR: ${BASH_SOURCE[0]}: file or directory not found (this should be this script)" 90 | exit 1 91 | } 92 | 93 | { 94 | cat <<"EOF" 95 | /// NOTE: THIS FILE IS AUTO-GENERATED BY dev/generate-postgresrowsequence-multi-decode.sh 96 | EOF 97 | echo 98 | 99 | echo "#if canImport(_Concurrency)" 100 | echo "extension AsyncSequence where Element == PostgresRow {" 101 | 102 | # note: 103 | # - widening the inverval below (eg. going from {1..15} to {1..25}) is Semver minor 104 | # - narrowing the interval below is SemVer _MAJOR_! 105 | for n in {1..15}; do 106 | genWithContextParameter "$n" 107 | genWithoutContextParameter "$n" 108 | done 109 | echo "}" 110 | echo "#endif" 111 | } > "$here/../Sources/PostgresNIO/New/PostgresRowSequence-multi-decode.swift" 112 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | x-shared-config: &shared_config 4 | environment: 5 | POSTGRES_HOST_AUTH_METHOD: "${POSTGRES_HOST_AUTH_METHOD:-scram-sha-256}" 6 | POSTGRES_USER: test_username 7 | POSTGRES_DB: test_database 8 | POSTGRES_PASSWORD: test_password 9 | ports: 10 | - 5432:5432 11 | 12 | services: 13 | psql-16: 14 | image: postgres:16 15 | <<: *shared_config 16 | psql-15: 17 | image: postgres:15 18 | <<: *shared_config 19 | psql-14: 20 | image: postgres:14 21 | <<: *shared_config 22 | psql-13: 23 | image: postgres:13 24 | <<: *shared_config 25 | psql-12: 26 | image: postgres:12 27 | <<: *shared_config 28 | psql-11: 29 | image: postgres:11 30 | <<: *shared_config 31 | --------------------------------------------------------------------------------