├── .swift-version ├── .gitignore ├── Tests ├── LinuxMain.swift └── PostgreSQLTests │ └── PostgreSQLTests.swift ├── Package.swift ├── Sources └── PostgreSQL │ ├── FieldInfo.swift │ ├── Row.swift │ ├── Result.swift │ ├── QueryRenderer.swift │ └── Connection.swift ├── .travis.yml ├── LICENSE └── README.md /.swift-version: -------------------------------------------------------------------------------- 1 | 3.0 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | Build 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PostgreSQLTests 3 | 4 | XCTMain([ 5 | testCase(PostgreSQLTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "PostgreSQL", 5 | dependencies: [ 6 | .Package(url: "https://github.com/Zewo/CLibpq.git", majorVersion: 0, minor: 13), 7 | .Package(url: "https://github.com/Zewo/SQL.git", majorVersion: 0, minor: 14) 8 | ] 9 | ) 10 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/FieldInfo.swift: -------------------------------------------------------------------------------- 1 | @_exported import SQL 2 | 3 | public struct FieldInfo: SQL.FieldInfoProtocol { 4 | public let name: String 5 | public let index: Int 6 | 7 | init(name: String, index: Int) { 8 | self.name = name 9 | self.index = index 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Row.swift: -------------------------------------------------------------------------------- 1 | import CLibpq 2 | @_exported import SQL 3 | 4 | public struct Row: RowProtocol { 5 | public let result: Result 6 | 7 | public let index: Int 8 | 9 | public init(result: Result, index: Int) { 10 | self.result = result 11 | self.index = index 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | notifications: 2 | slack: zewo:VjyVCCQvTOw9yrbzQysZezD1 3 | os: 4 | - linux 5 | - osx 6 | language: generic 7 | sudo: required 8 | dist: trusty 9 | osx_image: xcode8 10 | install: 11 | - eval "$(curl -sL https://raw.githubusercontent.com/Zewo/Zewo/master/Scripts/Travis/install.sh)" 12 | script: 13 | - swift build 14 | - swift build --configuration release 15 | # tests require a live psql database 16 | # TODO: setup psql on travis 17 | # - swift test 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Zewo 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL 2 | 3 | [![Swift][swift-badge]][swift-url] 4 | [![Zewo][zewo-badge]][zewo-url] 5 | [![Platform][platform-badge]][platform-url] 6 | [![License][mit-badge]][mit-url] 7 | [![Slack][slack-badge]][slack-url] 8 | [![Travis][travis-badge]][travis-url] 9 | [![Codebeat][codebeat-badge]][codebeat-url] 10 | 11 | **PostgreSQL** adapter for **Swift 3.0**. 12 | 13 | Conforms to [SQL](https://github.com/Zewo/SQL), which provides a common interface and ORM. Documentation can be found there. 14 | 15 | ## Installation 16 | 17 | - Linux 18 | 19 | ```bash 20 | $ apt-get install libpq-dev 21 | ``` 22 | 23 | - OSX 24 | 25 | ```bash 26 | $ brew install postgresql 27 | ``` 28 | 29 | - Add `PostgreSQL` to your `Package.swift` 30 | 31 | ```swift 32 | import PackageDescription 33 | 34 | let package = Package( 35 | dependencies: [ 36 | .Package(url: "https://github.com/Zewo/PostgreSQL.git", majorVersion: 0, minor: 14) 37 | ] 38 | ) 39 | 40 | ``` 41 | 42 | - Build on OSX 43 | ```bash 44 | $ swift build -Xcc -I/usr/local/include -Xlinker -L/usr/local/lib/ 45 | ``` 46 | 47 | - Generate Xcode project 48 | ```bash 49 | $ swift package generate-xcodeproj -Xcc -I/usr/local/include -Xlinker -L/usr/local/lib/ -Xswiftc -I/usr/local/include 50 | ``` 51 | 52 | - Build on Linux 53 | ```bash 54 | $ swift build -Xcc -I/usr/include/postgresql 55 | ``` 56 | 57 | ## Community 58 | 59 | [![Slack](http://s13.postimg.org/ybwy92ktf/Slack.png)](http://slack.zewo.io) 60 | 61 | Join us on [Slack](http://slack.zewo.io). 62 | 63 | ## License 64 | 65 | **PostgreSQL** is released under the MIT license. See LICENSE for details. 66 | 67 | [swift-badge]: https://img.shields.io/badge/Swift-3.0-orange.svg?style=flat 68 | [swift-url]: https://swift.org 69 | [zewo-badge]: https://img.shields.io/badge/Zewo-0.14-FF7565.svg?style=flat 70 | [zewo-url]: http://zewo.io 71 | [platform-badge]: https://img.shields.io/badge/Platform-Mac%20%26%20Linux-lightgray.svg?style=flat 72 | [platform-url]: https://swift.org 73 | [mit-badge]: https://img.shields.io/badge/License-MIT-blue.svg?style=flat 74 | [mit-url]: https://tldrlegal.com/license/mit-license 75 | [slack-image]: http://s13.postimg.org/ybwy92ktf/Slack.png 76 | [slack-badge]: https://zewo-slackin.herokuapp.com/badge.svg 77 | [slack-url]: http://slack.zewo.io 78 | [travis-badge]: https://travis-ci.org/Zewo/PostgreSQL.svg?branch=master 79 | [travis-url]: https://travis-ci.org/Zewo/PostgreSQL 80 | [codebeat-badge]: https://codebeat.co/badges/2548b359-daf1-404b-b5ae-687b98c02101 81 | [codebeat-url]: https://codebeat.co/projects/github-com-zewo-postgresql 82 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Result.swift: -------------------------------------------------------------------------------- 1 | import CLibpq 2 | import Axis 3 | @_exported import SQL 4 | 5 | public enum ResultError: Error { 6 | case badStatus(Result.Status, String) 7 | } 8 | 9 | public class Result: SQL.ResultProtocol { 10 | 11 | public enum Status: Int, ResultStatus { 12 | case EmptyQuery 13 | case CommandOK 14 | case TuplesOK 15 | case CopyOut 16 | case CopyIn 17 | case BadResponse 18 | case NonFatalError 19 | case FatalError 20 | case CopyBoth 21 | case SingleTuple 22 | case Unknown 23 | 24 | public init(status: ExecStatusType) { 25 | switch status { 26 | case PGRES_EMPTY_QUERY: 27 | self = .EmptyQuery 28 | break 29 | case PGRES_COMMAND_OK: 30 | self = .CommandOK 31 | break 32 | case PGRES_TUPLES_OK: 33 | self = .TuplesOK 34 | break 35 | case PGRES_COPY_OUT: 36 | self = .CopyOut 37 | break 38 | case PGRES_COPY_IN: 39 | self = .CopyIn 40 | break 41 | case PGRES_BAD_RESPONSE: 42 | self = .BadResponse 43 | break 44 | case PGRES_NONFATAL_ERROR: 45 | self = .NonFatalError 46 | break 47 | case PGRES_FATAL_ERROR: 48 | self = .FatalError 49 | break 50 | case PGRES_COPY_BOTH: 51 | self = .CopyBoth 52 | break 53 | case PGRES_SINGLE_TUPLE: 54 | self = .SingleTuple 55 | break 56 | default: 57 | self = .Unknown 58 | break 59 | } 60 | } 61 | 62 | public var successful: Bool { 63 | return self != .BadResponse && self != .FatalError 64 | } 65 | } 66 | 67 | internal init(_ resultPointer: OpaquePointer) throws { 68 | self.resultPointer = resultPointer 69 | 70 | guard status.successful else { 71 | throw ResultError.badStatus(status, String(validatingUTF8: PQresultErrorMessage(resultPointer)) ?? "No error message") 72 | } 73 | } 74 | 75 | deinit { 76 | clear() 77 | } 78 | 79 | public subscript(position: Int) -> Row { 80 | return Row(result: self, index: position) 81 | } 82 | 83 | public func data(atRow rowIndex: Int, forFieldIndex fieldIndex: Int) -> Buffer? { 84 | let rowIndex = Int32(rowIndex) 85 | let fieldIndex = Int32(fieldIndex) 86 | 87 | guard PQgetisnull(resultPointer, rowIndex, fieldIndex) == 0 else { 88 | return nil 89 | } 90 | 91 | let count = PQgetlength(resultPointer, rowIndex, fieldIndex) 92 | guard count > 0, let start = PQgetvalue(resultPointer, rowIndex, fieldIndex) else { 93 | return Buffer() 94 | } 95 | 96 | return start.withMemoryRebound(to: UInt8.self, capacity: Int(count)) { start in 97 | return Buffer(Array(UnsafeBufferPointer(start: start, count: Int(count)))) 98 | } 99 | } 100 | 101 | public var count: Int { 102 | return Int(PQntuples(self.resultPointer)) 103 | } 104 | 105 | lazy public var countAffected: Int = { 106 | guard let str = String(validatingUTF8: PQcmdTuples(self.resultPointer)) else { 107 | return 0 108 | } 109 | 110 | return Int(str) ?? 0 111 | }() 112 | 113 | public var status: Status { 114 | return Status(status: PQresultStatus(resultPointer)) 115 | } 116 | 117 | private let resultPointer: OpaquePointer 118 | 119 | public func clear() { 120 | PQclear(resultPointer) 121 | } 122 | 123 | public lazy var fieldsByName: [String: FieldInfo] = { 124 | var result = [String:FieldInfo]() 125 | 126 | for i in 0.. String { 3 | var components = [String]() 4 | 5 | components.append("SELECT") 6 | 7 | var fieldComponents = [String]() 8 | 9 | for field in statement.fields { 10 | switch field { 11 | case .string(let string): 12 | fieldComponents.append(string) 13 | break 14 | case .subquery(let subquery, alias: let alias): 15 | fieldComponents.append("(\(renderStatement(subquery))) as \(alias)") 16 | break 17 | case .field(let field): 18 | fieldComponents.append(field.qualifiedName) 19 | break 20 | case .function(let function, let alias): 21 | fieldComponents.append("\(function.sqlString) as \(alias)") 22 | break 23 | } 24 | } 25 | 26 | components.append(fieldComponents.joined(separator: ", ")) 27 | components.append("FROM") 28 | 29 | 30 | var sourceComponents = [String]() 31 | 32 | for source in statement.from { 33 | switch source { 34 | case .string(let string): 35 | sourceComponents.append(string) 36 | break 37 | case .subquery(let subquery, alias: let alias): 38 | sourceComponents.append("\(renderStatement(subquery)) as \(alias)") 39 | break 40 | case .field(let field): 41 | sourceComponents.append(field.qualifiedName) 42 | break 43 | case .function(let function, let alias): 44 | fieldComponents.append("\(function.sqlString) as \(alias)") 45 | break 46 | } 47 | } 48 | 49 | components.append(sourceComponents.joined(separator: ", ")) 50 | 51 | if !statement.joins.isEmpty { 52 | components.append(statement.joins.sqlStringJoined(separator: " ")) 53 | } 54 | 55 | if let predicate = statement.predicate { 56 | components.append("WHERE") 57 | components.append(composePredicate(predicate)) 58 | } 59 | 60 | if !statement.order.isEmpty { 61 | components.append(statement.order.sqlStringJoined(separator: ", ")) 62 | } 63 | 64 | if let limit = statement.limit { 65 | components.append("LIMIT \(limit)") 66 | } 67 | 68 | if let offset = statement.offset { 69 | components.append("OFFSET \(offset)") 70 | } 71 | 72 | return components.joined(separator: " ") 73 | } 74 | 75 | public static func renderStatement(_ statement: Update) -> String { 76 | var components = ["UPDATE", statement.tableName, "SET"] 77 | 78 | 79 | components.append( 80 | statement.valuesByField.map { 81 | return "\($0.key.unqualifiedName) = %@" 82 | }.joined(separator: ", ") 83 | ) 84 | 85 | if let predicate = statement.predicate { 86 | components.append("WHERE") 87 | components.append(composePredicate(predicate)) 88 | } 89 | 90 | return components.joined(separator: " ") 91 | } 92 | 93 | public static func renderStatement(_ statement: Insert, forReturningInsertedRows returnInsertedRows: Bool) -> String { 94 | var components = ["INSERT INTO", statement.tableName] 95 | 96 | components.append( 97 | "(\(statement.valuesByField.keys.map { $0.unqualifiedName }.joined(separator: ", ")))" 98 | ) 99 | 100 | components.append("VALUES") 101 | 102 | components.append( 103 | "(\(statement.valuesByField.values.map { _ in "%@" }.joined(separator: ", ")))" 104 | ) 105 | 106 | if returnInsertedRows { 107 | components.append("RETURNING *") 108 | } 109 | 110 | return components.joined(separator: " ") 111 | } 112 | 113 | public static func renderStatement(_ statement: Delete) -> String { 114 | var components = ["DELETE FROM", statement.tableName] 115 | 116 | if let predicate = statement.predicate { 117 | components.append("WHERE") 118 | components.append(composePredicate(predicate)) 119 | } 120 | 121 | return components.joined(separator: " ") 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Tests/PostgreSQLTests/PostgreSQLTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PostgreSQLTests.swift 3 | // PostgreSQL 4 | // 5 | // Created by David Ask on 17/02/16. 6 | // 7 | // 8 | 9 | import XCTest 10 | @testable import PostgreSQL 11 | import Axis 12 | 13 | public final class StandardOutputAppender: Appender { 14 | public var name: String = "Standard Output Appender" 15 | public var closed: Bool = false 16 | public var levels: Logger.Level = .all 17 | 18 | public init () {} 19 | 20 | public func append(event: Logger.Event) { 21 | var logMessage = "\(event.message) \n" 22 | let file = event.locationInfo.file 23 | logMessage += "In File: \(file)" 24 | logMessage += "\n" 25 | let line = event.locationInfo.line 26 | logMessage += "Line: \(line)" 27 | logMessage += "\n" 28 | let function = event.locationInfo.function 29 | logMessage += "Called From: \(function)" 30 | logMessage += "\n" 31 | print(logMessage) 32 | } 33 | } 34 | 35 | // MARK: - Models 36 | 37 | struct Artist { 38 | var name: String 39 | var genre: String 40 | 41 | init(name: String, genre: String) { 42 | self.name = name 43 | self.genre = genre 44 | } 45 | } 46 | 47 | extension Artist: ModelProtocol { 48 | typealias PrimaryKey = Int 49 | 50 | enum Field: String, ModelField { 51 | static let primaryKey = Field.id 52 | static let tableName = "artists" 53 | case id 54 | case name 55 | case genre 56 | } 57 | 58 | func serialize() -> [Field: ValueConvertible?] { 59 | return [.name: name, .genre: genre] 60 | } 61 | 62 | init(row: T) throws { 63 | try self.init( 64 | name: row.value(Field.name.qualifiedField), 65 | genre: row.value(Field.genre.qualifiedField) 66 | ) 67 | } 68 | } 69 | 70 | final class Album { 71 | var name: String 72 | var artistId: Artist.PrimaryKey 73 | 74 | init(name: String, artistId: Artist.PrimaryKey) { 75 | self.name = name 76 | self.artistId = artistId 77 | } 78 | } 79 | 80 | extension Album: ModelProtocol { 81 | typealias PrimaryKey = Int 82 | 83 | enum Field: String, ModelField { 84 | static let primaryKey = Field.id 85 | static let tableName = "albums" 86 | case id = "id" 87 | case name = "name" 88 | case artistId = "artist_id" 89 | } 90 | 91 | func serialize() -> [Field: ValueConvertible?] { 92 | return [ .name: name, .artistId: artistId ] 93 | } 94 | 95 | convenience init(row: T) throws { 96 | try self.init( 97 | name: row.value(Field.name.qualifiedField), 98 | artistId: row.value(Field.artistId.qualifiedField) 99 | ) 100 | } 101 | } 102 | 103 | // MARK: - Tests 104 | 105 | public class PostgreSQLTests: XCTestCase { 106 | let connection = try! PostgreSQL.Connection(info: .init(URL(string: "postgres://localhost:5432/swift_test")!)) 107 | 108 | let logger = Logger(name: "SQL Logger", appenders: [StandardOutputAppender()]) 109 | 110 | override public func setUp() { 111 | super.setUp() 112 | 113 | do { 114 | try connection.open() 115 | try connection.execute("DROP TABLE IF EXISTS albums") 116 | try connection.execute("DROP TABLE IF EXISTS artists") 117 | try connection.execute("CREATE TABLE IF NOT EXISTS artists(id SERIAL PRIMARY KEY, genre VARCHAR(50), name VARCHAR(255))") 118 | try connection.execute("CREATE TABLE IF NOT EXISTS albums(id SERIAL PRIMARY KEY, name VARCHAR(255), artist_id int references artists(id))") 119 | 120 | try connection.execute("INSERT INTO artists (name, genre) VALUES('Josh Rouse', 'Country')") 121 | 122 | connection.logger = logger 123 | } 124 | catch { 125 | XCTFail("Connection error: \(error)") 126 | } 127 | } 128 | 129 | func testSimpleRawQueries() throws { 130 | try connection.execute("SELECT * FROM artists") 131 | let result = try connection.execute("SELECT * FROM artists WHERE name = %@", parameters: "Josh Rouse") 132 | 133 | XCTAssert(try result.first?.value("name") == "Josh Rouse") 134 | } 135 | 136 | func testBulk() throws { 137 | for i in 0..<300 { 138 | let entity = Entity(model: Artist(name: "NAME \(i)", genre: "GENRE \(i)")) 139 | _ = try entity.save(connection: connection) 140 | } 141 | 142 | measure { 143 | do { 144 | let result = try Entity.fetchAll(connection: self.connection) 145 | 146 | for artist in result { 147 | print(artist.model.genre) 148 | } 149 | } 150 | catch { 151 | XCTFail("\(error)") 152 | } 153 | } 154 | } 155 | 156 | func testRockArtists() throws { 157 | 158 | let artists = try Entity.fetchAll(connection: connection) 159 | 160 | _ = try Entity.fetchAll(connection: connection) 161 | 162 | try connection.begin() 163 | 164 | for var artist in artists { 165 | artist.model.genre = "Rock & Roll" 166 | _ = try artist.save(connection: connection) 167 | } 168 | 169 | try connection.commit() 170 | } 171 | 172 | override public func tearDown() { 173 | // Put teardown code here. This method is called after the invocation of each test method in the class. 174 | super.tearDown() 175 | 176 | connection.close() 177 | } 178 | } 179 | 180 | extension PostgreSQLTests { 181 | public static var allTests: [(String, (PostgreSQLTests) -> () throws -> Void)] { 182 | return [ 183 | ("testBulk", testBulk), 184 | ("testSimpleRawQueries", testSimpleRawQueries), 185 | ("testRockArtists", testRockArtists), 186 | ] 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Connection.swift: -------------------------------------------------------------------------------- 1 | @_exported import SQL 2 | import CLibpq 3 | import Axis 4 | 5 | public struct ConnectionError: Error, CustomStringConvertible { 6 | public let description: String 7 | } 8 | 9 | public final class Connection: ConnectionProtocol { 10 | public typealias QueryRenderer = PostgreSQL.QueryRenderer 11 | 12 | public struct ConnectionInfo: ConnectionInfoProtocol { 13 | public var host: String 14 | public var port: Int 15 | public var databaseName: String 16 | public var username: String? 17 | public var password: String? 18 | public var options: String? 19 | public var tty: String? 20 | 21 | public init?(uri: URL) { 22 | do { 23 | try self.init(uri) 24 | } catch { 25 | return nil 26 | } 27 | } 28 | 29 | public init(_ uri: URL) throws { 30 | let databaseName = uri.path.trim(["/"]) 31 | 32 | guard let host = uri.host, let port = uri.port else { 33 | throw ConnectionError(description: "Failed to extract host, port, database name from URI") 34 | } 35 | 36 | self.host = host 37 | self.port = port 38 | self.databaseName = databaseName 39 | self.username = uri.user 40 | self.password = uri.password 41 | } 42 | 43 | public init(host: String, port: Int = 5432, databaseName: String, username: String? = nil, password: String? = nil, options: String? = nil, tty: String? = nil) { 44 | self.host = host 45 | self.port = port 46 | self.databaseName = databaseName 47 | self.username = username 48 | self.password = password 49 | self.options = options 50 | self.tty = tty 51 | } 52 | } 53 | 54 | public enum InternalStatus { 55 | case Bad 56 | case Started 57 | case Made 58 | case AwatingResponse 59 | case AuthOK 60 | case SettingEnvironment 61 | case SSLStartup 62 | case OK 63 | case Unknown 64 | case Needed 65 | 66 | public init(status: ConnStatusType) { 67 | switch status { 68 | case CONNECTION_NEEDED: 69 | self = .Needed 70 | break 71 | case CONNECTION_OK: 72 | self = .OK 73 | break 74 | case CONNECTION_STARTED: 75 | self = .Started 76 | break 77 | case CONNECTION_MADE: 78 | self = .Made 79 | break 80 | case CONNECTION_AWAITING_RESPONSE: 81 | self = .AwatingResponse 82 | break 83 | case CONNECTION_AUTH_OK: 84 | self = .AuthOK 85 | break 86 | case CONNECTION_SSL_STARTUP: 87 | self = .SSLStartup 88 | break 89 | case CONNECTION_SETENV: 90 | self = .SettingEnvironment 91 | break 92 | case CONNECTION_BAD: 93 | self = .Bad 94 | break 95 | default: 96 | self = .Unknown 97 | break 98 | } 99 | } 100 | } 101 | 102 | public var logger: Logger? 103 | 104 | private var connection: OpaquePointer? = nil 105 | 106 | public let connectionInfo: ConnectionInfo 107 | 108 | public required init(info: ConnectionInfo) { 109 | self.connectionInfo = info 110 | } 111 | 112 | deinit { 113 | close() 114 | } 115 | 116 | public var internalStatus: InternalStatus { 117 | return InternalStatus(status: PQstatus(self.connection)) 118 | } 119 | 120 | public func open() throws { 121 | connection = PQsetdbLogin( 122 | connectionInfo.host, 123 | String(connectionInfo.port), 124 | connectionInfo.options ?? "", 125 | connectionInfo.tty ?? "", 126 | connectionInfo.databaseName, 127 | connectionInfo.username ?? "", 128 | connectionInfo.password ?? "" 129 | ) 130 | 131 | if let error = mostRecentError { 132 | throw error 133 | } 134 | } 135 | 136 | public var mostRecentError: ConnectionError? { 137 | guard let errorString = String(validatingUTF8: PQerrorMessage(connection)), !errorString.isEmpty else { 138 | return nil 139 | } 140 | 141 | return ConnectionError(description: errorString) 142 | } 143 | 144 | public func close() { 145 | PQfinish(connection) 146 | connection = nil 147 | } 148 | 149 | public func createSavePointNamed(_ name: String) throws { 150 | try execute("SAVEPOINT \(name)", parameters: nil) 151 | } 152 | 153 | public func rollbackToSavePointNamed(_ name: String) throws { 154 | try execute("ROLLBACK TO SAVEPOINT \(name)", parameters: nil) 155 | } 156 | 157 | public func releaseSavePointNamed(_ name: String) throws { 158 | try execute("RELEASE SAVEPOINT \(name)", parameters: nil) 159 | } 160 | 161 | @discardableResult 162 | public func execute(_ statement: String, parameters: [Value?]?) throws -> Result { 163 | 164 | var statement = statement.sqlStringWithEscapedPlaceholdersUsingPrefix("$") { 165 | return String($0 + 1) 166 | } 167 | 168 | defer { logger?.debug(statement) } 169 | 170 | guard let parameters = parameters else { 171 | guard let resultPointer = PQexec(connection, statement) else { 172 | throw mostRecentError ?? ConnectionError(description: "Empty result") 173 | } 174 | 175 | return try Result(resultPointer) 176 | } 177 | 178 | var parameterData = [UnsafePointer?]() 179 | var deallocators = [() -> ()]() 180 | defer { deallocators.forEach { $0() } } 181 | 182 | for parameter in parameters { 183 | 184 | guard let value = parameter else { 185 | parameterData.append(nil) 186 | continue 187 | } 188 | 189 | let data: AnyCollection 190 | switch value { 191 | case .buffer(let value): 192 | data = AnyCollection(value.map { Int8($0) }) 193 | 194 | case .string(let string): 195 | data = AnyCollection(string.utf8CString) 196 | } 197 | 198 | let pointer = UnsafeMutablePointer.allocate(capacity: Int(data.count)) 199 | deallocators.append { 200 | pointer.deallocate(capacity: Int(data.count)) 201 | } 202 | 203 | for (index, byte) in data.enumerated() { 204 | pointer[index] = byte 205 | } 206 | 207 | parameterData.append(pointer) 208 | } 209 | 210 | let result: OpaquePointer = try parameterData.withUnsafeBufferPointer { buffer in 211 | guard let result = PQexecParams( 212 | self.connection, 213 | statement, 214 | Int32(parameters.count), 215 | nil, 216 | buffer.isEmpty ? nil : buffer.baseAddress, 217 | nil, 218 | nil, 219 | 0 220 | ) else { 221 | throw mostRecentError ?? ConnectionError(description: "Empty result") 222 | } 223 | return result 224 | } 225 | 226 | return try Result(result) 227 | } 228 | } 229 | --------------------------------------------------------------------------------