├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── Package.swift ├── Package@swift-4.swift ├── Package@swift-5.2.swift ├── README.md ├── Sources └── PostgreSQL │ ├── Bind │ ├── BinaryUtils.swift │ ├── Bind+Node.swift │ ├── Bind.swift │ └── FieldType.swift │ ├── Configuration.swift │ ├── Connection.swift │ ├── ConnectionInfo.swift │ ├── Context.swift │ ├── Database.swift │ ├── Error.swift │ ├── Exports.swift │ └── Result.swift ├── Tests ├── LinuxMain.swift └── PostgreSQLTests │ ├── ArrayTests.swift │ ├── BinaryUtilsTests.swift │ ├── ConnectionTests.swift │ ├── MiscTests.swift │ ├── PostgreSQLTests.swift │ └── Utilities.swift └── codecov.yml /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | jobs: 4 | linux-3: 5 | docker: 6 | - image: swift:3.1.1 7 | - image: circleci/postgres:latest 8 | environment: 9 | POSTGRES_USER: postgres 10 | POSTGRES_DB: test 11 | POSTGRES_PASSWORD: "" 12 | steps: 13 | - run: apt-get update -yq && apt-get install -yq libpq-dev 14 | - checkout 15 | - run: swift build 16 | - run: swift build -c release 17 | - run: swift test 18 | 19 | linux: 20 | docker: 21 | - image: swift:4.0.3 22 | - image: circleci/postgres:latest 23 | environment: 24 | POSTGRES_USER: postgres 25 | POSTGRES_DB: test 26 | POSTGRES_PASSWORD: "" 27 | steps: 28 | - run: apt-get update -yq && apt-get install -yq libpq-dev 29 | - checkout 30 | - run: swift build 31 | - run: swift build -c release 32 | - run: swift test 33 | 34 | workflows: 35 | version: 2 36 | tests: 37 | jobs: 38 | - linux-3 39 | - linux 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | Sources/main.swift 3 | .build 4 | Packages 5 | Database 6 | *.xcodeproj 7 | Package.pins 8 | Package.resolved 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Qutheory 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 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:3.1 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "PostgreSQL", 6 | dependencies: [ 7 | // Module map for `libpq` 8 | .Package(url: "https://github.com/vapor-community/cpostgresql.git", majorVersion: 2), 9 | 10 | // Data structure for converting between multiple representations 11 | .Package(url: "https://github.com/vapor/node.git", majorVersion: 2), 12 | 13 | // Core extensions, type-aliases, and functions that facilitate common tasks 14 | .Package(url: "https://github.com/vapor/core.git", majorVersion: 2) 15 | ] 16 | ) 17 | -------------------------------------------------------------------------------- /Package@swift-4.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "PostgreSQL", 6 | products: [ 7 | .library(name: "PostgreSQL", targets: ["PostgreSQL"]), 8 | ], 9 | dependencies: [ 10 | // Module map for `libpq` 11 | .package(url: "https://github.com/vapor-community/cpostgresql.git", .upToNextMajor(from: "2.1.0")), 12 | 13 | // Data structure for converting between multiple representations 14 | .package(url: "https://github.com/vapor/node.git", .upToNextMajor(from: "2.1.0")), 15 | 16 | // Core extensions, type-aliases, and functions that facilitate common tasks 17 | .package(url: "https://github.com/vapor/core.git", .upToNextMajor(from: "2.1.2")) 18 | ], 19 | targets: [ 20 | .target(name: "PostgreSQL", dependencies: ["CPostgreSQL", "Node", "Core"]), 21 | .testTarget(name: "PostgreSQLTests", dependencies: ["PostgreSQL"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /Package@swift-5.2.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | import PackageDescription 3 | 4 | let package = Package( 5 | name: "PostgreSQL", 6 | products: [ 7 | .library(name: "PostgreSQL", targets: ["PostgreSQL"]), 8 | ], 9 | dependencies: [ 10 | // Module map for `libpq` 11 | .package(name: "CPostgreSQL", url: "https://github.com/vapor-community/cpostgresql.git", from: "2.1.0"), 12 | 13 | // Data structure for converting between multiple representations 14 | .package(name: "Node", url: "https://github.com/vapor/node.git", from: "2.1.0"), 15 | 16 | // Core extensions, type-aliases, and functions that facilitate common tasks 17 | .package(name: "Core", url: "https://github.com/vapor/core.git", from: "2.1.2"), 18 | ], 19 | targets: [ 20 | .target(name: "PostgreSQL", dependencies: ["CPostgreSQL", "Node", "Core"]), 21 | .testTarget(name: "PostgreSQLTests", dependencies: ["PostgreSQL"]), 22 | ] 23 | ) 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Swift](https://img.shields.io/badge/swift-3.1_--_4.0-brightgreen.svg)](https://swift.org) 2 | [![Linux Build Status](https://img.shields.io/circleci/project/github/vapor-community/postgresql.svg?label=Linux)](https://circleci.com/gh/vapor-community/postgresql) 3 | [![macOS Build Status](https://img.shields.io/travis/vapor-community/postgresql.svg?label=macOS)](https://travis-ci.org/vapor-community/postgresql) 4 | [![codecov](https://codecov.io/gh/vapor-community/postgresql/branch/master/graph/badge.svg)](https://codecov.io/gh/vapor-community/postgresql) 5 | [![GitHub license](https://img.shields.io/badge/license-MIT-brightgreen.svg)](LICENSE) 6 | 7 | # PostgreSQL for Swift 8 | 9 | ## Prerequisites 10 | 11 | The PostgreSQL C driver must be installed in order to use this package. 12 | Follow the [README of the cpostgresql repo](https://github.com/vapor-community/cpostgresql/blob/master/README.md) to get started. 13 | 14 | ## Using PostgreSQL 15 | 16 | This section outlines how to import the PostgreSQL package both with or without a Vapor project. 17 | 18 | ### With Vapor 19 | 20 | The easiest way to use PostgreSQL with Vapor is to include the PostgreSQL provider. 21 | 22 | ```swift 23 | import PackageDescription 24 | 25 | let package = Package( 26 | name: "Project", 27 | dependencies: [ 28 | .Package(url: "https://github.com/vapor/vapor.git", majorVersion: 2), 29 | .Package(url: "https://github.com/vapor-community/postgresql-provider.git", majorVersion: 2) 30 | ], 31 | exclude: [ ... ] 32 | ) 33 | ``` 34 | 35 | The PostgreSQL provider package adds PostgreSQL to your project and adds some additional, Vapor-specific conveniences like `drop.postgresql()`. 36 | 37 | Using `import PostgreSQLProvider` will import both Fluent and Fluent's Vapor-specific APIs. 38 | 39 | ### With Fluent 40 | 41 | Fluent is a powerful, pure-Swift ORM that can be used with any Server-Side Swift framework. The PostgreSQL driver allows you to use a PostgreSQL database to power your models and queries. 42 | 43 | ```swift 44 | import PackageDescription 45 | 46 | let package = Package( 47 | name: "Project", 48 | dependencies: [ 49 | ... 50 | .Package(url: "https://github.com/vapor/fluent.git", majorVersion: 2), 51 | .Package(url: "https://github.com/vapor-community/postgresql-driver.git", majorVersion: 2) 52 | ], 53 | exclude: [ ... ] 54 | ) 55 | ``` 56 | 57 | Use `import PostgreSQLDriver` to access the `PostgreSQLDriver` class which you can use to initialize a Fluent `Database`. 58 | 59 | ### Just PostgreSQL 60 | 61 | At the core of the PostgreSQL provider and PostgreSQL driver is a Swift wrapper around the C PostgreSQL client. This package can be used by itself to send raw, parameterized queries to your PostgreSQL database. 62 | 63 | ```swift 64 | import PackageDescription 65 | 66 | let package = Package( 67 | name: "Project", 68 | dependencies: [ 69 | ... 70 | .Package(url: "https://github.com/vapor/postgresql.git", majorVersion: 2) 71 | ], 72 | exclude: [ ... ] 73 | ) 74 | ``` 75 | 76 | Use `import PostgreSQL` to access the `PostgreSQL.Database` class. 77 | 78 | 79 | # Examples 80 | 81 | ## Connecting to the Database 82 | 83 | ```swift 84 | import PostgreSQL 85 | 86 | let postgreSQL = PostgreSQL.Database( 87 | hostname: "localhost", 88 | database: "test", 89 | user: "root", 90 | password: "" 91 | ) 92 | ``` 93 | 94 | ## Select 95 | 96 | ```swift 97 | let version = try postgreSQL.execute("SELECT version()") 98 | ``` 99 | 100 | ## Prepared Statement 101 | 102 | The second parameter to `execute()` is an array of `PostgreSQL.Value`s. 103 | 104 | ```swift 105 | let results = try postgreSQL.execute("SELECT * FROM users WHERE age >= $1", [.int(21)]) 106 | ``` 107 | 108 | ## Listen and Notify 109 | 110 | ```swift 111 | try postgreSQL.listen(to: "test_channel") { notification in 112 | print(notification.channel) 113 | print(notification.payload) 114 | } 115 | 116 | // Allow set up time for LISTEN 117 | sleep(1) 118 | 119 | try postgreSQL.notify(channel: "test_channel", payload: "test_payload") 120 | 121 | ``` 122 | 123 | ## Connection 124 | 125 | Each call to `execute()` creates a new connection to the PostgreSQL database. This ensures thread safety since a single connection cannot be used on more than one thread. 126 | 127 | If you would like to re-use a connection between calls to execute, create a reusable connection and pass it as the third parameter to `execute()`. 128 | 129 | ```swift 130 | let connection = try postgreSQL.makeConnection() 131 | let result = try postgreSQL.execute("SELECT * FROM users WHERE age >= $1", [.int(21)]), connection) 132 | ``` 133 | 134 | ## Contributors 135 | 136 | Maintained by [Steven Roebert](https://github.com/sroebert), [Nate Bird](https://twitter.com/natesbird), [Prince Ugwuh](https://twitter.com/Prince2k3), and other members of the Vapor community. 137 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Bind/BinaryUtils.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Core 3 | 4 | extension UInt8 { 5 | var lowercaseHexPair: String { 6 | let hexString = String(self, radix: 16, uppercase: false) 7 | #if swift(>=5.0) 8 | return String(repeating: "0", count: 2 - hexString.count) + hexString 9 | #else 10 | return String(repeating: "0", count: 2 - hexString.characters.count) + hexString 11 | #endif 12 | } 13 | 14 | var lowercaseBinaryString: String { 15 | let bitString = String(self, radix: 2, uppercase: false) 16 | #if swift(>=5.0) 17 | return String(repeating: "0", count: 8 - bitString.count) + bitString 18 | #else 19 | return String(repeating: "0", count: 8 - bitString.characters.count) + bitString 20 | #endif 21 | } 22 | } 23 | 24 | extension Float32 { 25 | init(bigEndian: Float32) { 26 | let int = UInt32(bigEndian: bigEndian.bitPattern) 27 | self = Float32(bitPattern: int) 28 | } 29 | 30 | var bigEndian: Float32 { 31 | return Float32(bitPattern: bitPattern.bigEndian) 32 | } 33 | } 34 | 35 | extension Float64 { 36 | init(bigEndian: Float64) { 37 | let int = UInt64(bigEndian: bigEndian.bitPattern) 38 | self = Float64(bitPattern: int) 39 | } 40 | 41 | var bigEndian: Float64 { 42 | return Float64(bitPattern: bitPattern.bigEndian) 43 | } 44 | } 45 | 46 | /// Most information for parsing binary formats has been retrieved from the following links: 47 | /// - https://www.postgresql.org/docs/9.6/static/datatype.html (Data types) 48 | /// - https://github.com/postgres/postgres/tree/55c3391d1e6a201b5b891781d21fe682a8c64fe6/src/backend/utils/adt (Backend sending code) 49 | struct BinaryUtils { 50 | 51 | // MARK: - Formatter 52 | 53 | struct Formatters { 54 | static let timestamptz: DateFormatter = { 55 | let formatter = DateFormatter() 56 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSSX" 57 | return formatter 58 | }() 59 | 60 | static let interval: NumberFormatter = { 61 | let formatter = NumberFormatter() 62 | formatter.numberStyle = .decimal 63 | formatter.groupingSeparator = "" 64 | formatter.decimalSeparator = "." 65 | formatter.minimumIntegerDigits = 2 66 | formatter.maximumIntegerDigits = 2 67 | formatter.minimumFractionDigits = 0 68 | formatter.maximumFractionDigits = 6 69 | return formatter 70 | }() 71 | 72 | static let geometry: NumberFormatter = { 73 | let formatter = NumberFormatter() 74 | formatter.numberStyle = .decimal 75 | formatter.groupingSeparator = "" 76 | formatter.decimalSeparator = "." 77 | formatter.minimumFractionDigits = 0 78 | formatter.maximumFractionDigits = 14 79 | return formatter 80 | }() 81 | } 82 | 83 | // MARK: - Convert 84 | 85 | static func convert(_ value: UnsafeMutablePointer) -> T { 86 | return value.withMemoryRebound(to: T.self, capacity: 1) { 87 | $0.pointee 88 | } 89 | } 90 | 91 | static func parseBytes(value: UnsafeMutablePointer, length: Int) -> [UInt8] { 92 | var uint8Bytes: [UInt8] = [] 93 | uint8Bytes.reserveCapacity(length) 94 | for i in 0..(_ value: inout T) -> (UnsafeMutablePointer, Int) { 101 | let size = MemoryLayout.size(ofValue: value) 102 | return withUnsafePointer(to: &value) { valuePointer in 103 | return valuePointer.withMemoryRebound(to: Int8.self, capacity: size) { bytePointer in 104 | let bytes: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: size) 105 | bytes.assign(from: bytePointer, count: size) 106 | return (bytes, size) 107 | } 108 | } 109 | } 110 | 111 | // MARK: - String 112 | 113 | 114 | /// Parses a non-null terminated string with a given length from an Int8 pointer into a string 115 | /// 116 | /// - Parameters: 117 | /// - value: The pointer to the string byte array. 118 | /// - length: The length of the string (excluding null terminator if there is one). 119 | /// - Returns: The parsed string. 120 | static func parseString(value: UnsafeMutablePointer, length: Int) -> String { 121 | // As strings might not be null terminated, use `init(bytes:encoding:)` with buffer pointer 122 | let bufferPointer = value.withMemoryRebound(to: UInt8.self, capacity: length) { 123 | UnsafeBufferPointer(start: $0, count: length) 124 | } 125 | guard let string = String(bytes: bufferPointer, encoding: .utf8) else { 126 | print("Could not parse string as UTF8, returning an empty string.") 127 | return "" 128 | } 129 | return string 130 | } 131 | 132 | // MARK: - Int 133 | 134 | static func parseInt16(value: UnsafeMutablePointer) -> Int16 { 135 | return Int16(bigEndian: convert(value)) 136 | } 137 | 138 | static func parseInt32(value: UnsafeMutablePointer) -> Int32 { 139 | return Int32(bigEndian: convert(value)) 140 | } 141 | 142 | static func parseInt64(value: UnsafeMutablePointer) -> Int64 { 143 | return Int64(bigEndian: convert(value)) 144 | } 145 | 146 | // MARK: - Float 147 | 148 | static func parseFloat32(value: UnsafeMutablePointer) -> Float32 { 149 | return Float32(bigEndian: convert(value)) 150 | } 151 | 152 | static func parseFloat64(value: UnsafeMutablePointer) -> Float64 { 153 | return Float64(bigEndian: convert(value)) 154 | } 155 | 156 | // MARK: - Numeric 157 | 158 | struct Numeric { 159 | private static let signNaN: Int16 = -16384 160 | private static let signNegative: Int16 = 16384 161 | private static let decDigits = 4 162 | private static let NBASE: Int16 = 10000 163 | private static let halfNBASE: Int16 = 5000 164 | private static let roundPowers: [Int16] = [0, 1000, 100, 10] 165 | 166 | var sign: Int16 167 | var weight: Int 168 | var dscale: Int 169 | var numberOfDigits: Int 170 | var digits: [Int16] 171 | 172 | init(value: UnsafeMutablePointer) { 173 | sign = BinaryUtils.parseInt16(value: value.advanced(by: 4)) 174 | weight = Int(BinaryUtils.parseInt16(value: value.advanced(by: 2))) 175 | 176 | var dscale = Int(BinaryUtils.parseInt16(value: value.advanced(by: 6))) 177 | if dscale < 0 { 178 | dscale = 0 179 | } 180 | self.dscale = dscale 181 | 182 | numberOfDigits = Int(BinaryUtils.parseInt16(value: value)) 183 | digits = (0.. String { 187 | let int16: Int16 188 | if index >= 0 && index < numberOfDigits { 189 | int16 = digits[index] 190 | } else { 191 | int16 = 0 192 | } 193 | let stringDigits = String(int16) 194 | 195 | if (index == 0 && !fractional) { 196 | return stringDigits 197 | } 198 | 199 | // The number of digits should be 4 (DEC_DIGITS), 200 | // so pad if necessary. 201 | #if swift(>=5.0) 202 | return String(repeating: "0", count: Numeric.decDigits - stringDigits.count) + stringDigits 203 | #else 204 | return String(repeating: "0", count: Numeric.decDigits - stringDigits.characters.count) + stringDigits 205 | #endif 206 | } 207 | 208 | /// Function for rounding numeric values. 209 | /// The code is based on https://github.com/postgres/postgres/blob/3a0d473192b2045cbaf997df8437e7762d34f3ba/src/backend/utils/adt/numeric.c#L8594 210 | mutating func roundIfNeeded() { 211 | // Decimal digits wanted 212 | var totalDigits = (weight + 1) * Numeric.decDigits + dscale 213 | 214 | // If less than 0, result should be 0 215 | guard totalDigits >= 0 else { 216 | digits = [] 217 | weight = 0 218 | sign = 0 219 | return 220 | } 221 | 222 | // NBASE digits wanted 223 | var nbaseDigits = (totalDigits + Numeric.decDigits - 1) / Numeric.decDigits 224 | 225 | // 0, or number of decimal digits to keep in last NBASE digit 226 | totalDigits = totalDigits % Numeric.decDigits 227 | 228 | guard nbaseDigits < numberOfDigits || (nbaseDigits == numberOfDigits && totalDigits > 0) else { 229 | return 230 | } 231 | 232 | numberOfDigits = nbaseDigits 233 | 234 | var carry: Int16 235 | if totalDigits == 0 { 236 | carry = digits[0] >= Numeric.halfNBASE ? 1 : 0 237 | } else { 238 | nbaseDigits -= 1 239 | 240 | // Must round within last NBASE digit 241 | var pow10 = Numeric.roundPowers[totalDigits] 242 | let extra = digits[nbaseDigits] % pow10 243 | digits[nbaseDigits] = digits[nbaseDigits] - extra 244 | 245 | carry = 0 246 | if extra >= pow10 / 2 { 247 | pow10 += digits[nbaseDigits] 248 | if pow10 >= Numeric.NBASE { 249 | pow10 -= Numeric.NBASE 250 | carry = 1 251 | } 252 | digits[nbaseDigits] = pow10 253 | } 254 | } 255 | 256 | // Propagate carry if needed 257 | while carry > 0 { 258 | nbaseDigits -= 1 259 | if nbaseDigits < 0 { 260 | digits.insert(0, at: 0) 261 | nbaseDigits = 0 262 | 263 | numberOfDigits += 1 264 | weight += 1 265 | } 266 | 267 | carry += digits[nbaseDigits] 268 | 269 | if carry >= Numeric.NBASE { 270 | digits[nbaseDigits] = carry - Numeric.NBASE 271 | carry = 1 272 | } else { 273 | digits[nbaseDigits] = carry 274 | carry = 0 275 | } 276 | } 277 | } 278 | 279 | var string: String { 280 | // Check for NaN 281 | guard sign != Numeric.signNaN else { 282 | return "NaN" 283 | } 284 | 285 | guard !digits.isEmpty else { 286 | return "0" 287 | } 288 | 289 | var digitIndex = 0 290 | var string: String = "" 291 | 292 | // Make number negative if necessary 293 | if sign == Numeric.signNegative { 294 | string += "-" 295 | } 296 | 297 | // Add all digits before decimal point 298 | if weight < 0 { 299 | digitIndex = weight + 1 300 | string += "0" 301 | } else { 302 | while digitIndex <= weight { 303 | string += getDigit(atIndex: digitIndex) 304 | digitIndex += 1 305 | } 306 | } 307 | 308 | guard dscale > 0 else { 309 | return string 310 | } 311 | 312 | // Add digits after decimal point 313 | string += "." 314 | let decimalIndex = string.endIndex 315 | 316 | for _ in stride(from: 0, to: dscale, by: Numeric.decDigits) { 317 | string += getDigit(atIndex: digitIndex, fractional: true) 318 | digitIndex += 1 319 | } 320 | 321 | #if swift(>=3.2) 322 | let maxOffset = string.distance(from: decimalIndex, to: string.endIndex) 323 | let offset = min(maxOffset, dscale) 324 | let endIndex = string.index(decimalIndex, offsetBy: offset) 325 | 326 | return String(string[..) -> String { 335 | var numeric = Numeric(value: value) 336 | numeric.roundIfNeeded() 337 | return numeric.string 338 | } 339 | 340 | // MARK: - Date / Time 341 | 342 | struct TimestampConstants { 343 | // Foundation referenceDate is 00:00:00 UTC on 1 January 2001, 344 | // the reference date we want is 00:00:00 UTC on 1 January 2000 345 | static let offsetTimeIntervalSinceFoundationReferenceDate: TimeInterval = -31_622_400 346 | static let referenceDate = Date(timeIntervalSinceReferenceDate: offsetTimeIntervalSinceFoundationReferenceDate) 347 | } 348 | 349 | static func parseTimetamp(value: UnsafeMutablePointer, isInteger: Bool) -> Date { 350 | let interval: TimeInterval 351 | if isInteger { 352 | let microseconds = parseInt64(value: (value)) 353 | interval = TimeInterval(microseconds) / 1_000_000 354 | } else { 355 | let seconds = parseFloat64(value :value) 356 | interval = TimeInterval(seconds) 357 | } 358 | return Date(timeInterval: interval, since: TimestampConstants.referenceDate) 359 | } 360 | 361 | // MARK: - Interval 362 | 363 | private static func parseInt64TimeInterval(value: UnsafeMutablePointer) -> (hours: Int64, minutes: Int64, seconds: Double, isNegative: Bool) { 364 | var totalMicroseconds = parseInt64(value: value) 365 | let isNegative = totalMicroseconds < 0 366 | if isNegative { 367 | totalMicroseconds *= -1 368 | } 369 | 370 | let hours = totalMicroseconds / (1_000_000 * 60 * 60) 371 | totalMicroseconds -= hours * (1_000_000 * 60 * 60) 372 | 373 | let minutes = totalMicroseconds / (1_000_000 * 60) 374 | totalMicroseconds -= minutes * (1_000_000 * 60) 375 | 376 | let seconds = Double(totalMicroseconds) / 1_000_000 377 | 378 | return (hours, minutes, seconds, isNegative) 379 | } 380 | 381 | private static func parseFloat64TimeInterval(value: UnsafeMutablePointer) -> (hours: Int64, minutes: Int64, seconds: Double, isNegative: Bool) { 382 | var totalSeconds = parseFloat64(value :value) 383 | let isNegative = totalSeconds < 0 384 | if isNegative { 385 | totalSeconds *= -1 386 | } 387 | 388 | let hoursDouble = totalSeconds / (60 * 60) 389 | let hours = Int64(hoursDouble > Double(Int64.max) ? Int64.max : Int64(hoursDouble)) 390 | totalSeconds -= Float64(hours) * (60 * 60) 391 | 392 | let minutesDouble = totalSeconds / 60 393 | let minutes = Int64(minutesDouble > Double(Int64.max) ? Int64.max : Int64(minutesDouble)) 394 | totalSeconds -= Float64(minutes) * 60 395 | 396 | let seconds = Double(totalSeconds) 397 | 398 | return (hours, minutes, seconds, isNegative) 399 | } 400 | 401 | static func parseTimeInterval(value: UnsafeMutablePointer, isInteger: Bool) -> String? { 402 | let hours: Int64 403 | let minutes: Int64 404 | let seconds: Double 405 | let isNegative: Bool 406 | 407 | if isInteger { 408 | (hours, minutes, seconds, isNegative) = parseInt64TimeInterval(value: value) 409 | } else { 410 | (hours, minutes, seconds, isNegative) = parseFloat64TimeInterval(value: value) 411 | } 412 | 413 | guard hours > 0 || minutes > 0 || seconds > 0 else { 414 | return nil 415 | } 416 | 417 | let timeString = [ 418 | Formatters.interval.string(from: NSNumber(value: hours))!, 419 | Formatters.interval.string(from: NSNumber(value: minutes))!, 420 | Formatters.interval.string(from: NSNumber(value: seconds))!, 421 | ].joined(separator: ":") 422 | 423 | if isNegative { 424 | return "-\(timeString)" 425 | } 426 | return timeString 427 | } 428 | 429 | static func parseInterval(value: UnsafeMutablePointer, timeIsInteger: Bool) -> String { 430 | let days = Int(parseInt32(value: value.advanced(by: 8))) 431 | var months = Int(parseInt32(value: value.advanced(by: 12))) 432 | let hasNegativeParts = days < 0 || months < 0 433 | 434 | let years = months / 12 435 | months -= years * 12 436 | 437 | var interval: [String] = [] 438 | if years != 0 { 439 | let prefix = hasNegativeParts && years > 0 ? "+" : "" 440 | interval.append("\(prefix)\(years) \(years == 1 ? "year" : "years")") 441 | } 442 | if months != 0 { 443 | let prefix = hasNegativeParts && months > 0 ? "+" : "" 444 | interval.append("\(prefix)\(months) \(months == 1 ? "mon" : "mons")") 445 | } 446 | if days != 0 { 447 | let prefix = hasNegativeParts && days > 0 ? "+" : "" 448 | interval.append("\(prefix)\(days) \(days == 1 ? "day" : "days")") 449 | } 450 | if let timeString = parseTimeInterval(value: value, isInteger: timeIsInteger) { 451 | interval.append(timeString) 452 | } 453 | 454 | guard !interval.isEmpty else { 455 | // Fallback if all is zero 456 | return "00:00:00" 457 | } 458 | return interval.joined(separator: " ") 459 | } 460 | 461 | // MARK: - UUID 462 | 463 | static func parseUUID(value: UnsafeMutablePointer) -> String { 464 | let uuid: uuid_t = convert(value) 465 | return UUID(uuid: uuid).uuidString.lowercased() 466 | } 467 | 468 | // MARK: - Geometric Types 469 | 470 | private static func parseGeometryFloat64(value: UnsafeMutablePointer) -> String { 471 | let float = parseFloat64(value: value) 472 | return Formatters.geometry.string(from: NSNumber(value: float))! 473 | } 474 | 475 | private static func parsePoints(value: UnsafeMutablePointer, count: Int) -> String { 476 | var points: [String] = [] 477 | points.reserveCapacity(count) 478 | 479 | var value = value 480 | for _ in 0..) -> String { 489 | let x = parseGeometryFloat64(value: value) 490 | let y = parseGeometryFloat64(value: value.advanced(by: 8)) 491 | return "(\(x),\(y))" 492 | } 493 | 494 | static func parseLineSegment(value: UnsafeMutablePointer) -> String { 495 | let points = parsePoints(value: value, count: 2) 496 | return "[\(points)]" 497 | } 498 | 499 | static func parsePath(value: UnsafeMutablePointer) -> String { 500 | let isOpen: Bool = convert(value) 501 | let numberOfPoints = parseInt32(value: value.advanced(by: 1)) 502 | let points = parsePoints(value: value.advanced(by: 5), count: Int(numberOfPoints)) 503 | 504 | if isOpen { 505 | return "(\(points))" 506 | } 507 | return "[\(points)]" 508 | } 509 | 510 | static func parseBox(value: UnsafeMutablePointer) -> String { 511 | return parsePoints(value: value, count: 2) 512 | } 513 | 514 | static func parsePolygon(value: UnsafeMutablePointer) -> String { 515 | let numberOfPoints = parseInt32(value: value) 516 | let points = parsePoints(value: value.advanced(by: 4), count: Int(numberOfPoints)) 517 | return "(\(points))" 518 | } 519 | 520 | static func parseCircle(value: UnsafeMutablePointer) -> String { 521 | let centerPoint = parsePoint(value: value) 522 | let radius = parseGeometryFloat64(value: value.advanced(by: 16)) 523 | return "<\(centerPoint),\(radius)>" 524 | } 525 | 526 | // MARK: - Network Address Types 527 | 528 | // https://github.com/postgres/postgres/blob/6560407c7db2c7e32926a46f5fb52175ac10d9e5/src/port/inet_net_ntop.c#L44 529 | static let PGSQL_AF_INET6: Int32 = AF_INET + 1 530 | 531 | static func parseIPAddress(value: UnsafeMutablePointer) -> String { 532 | let psqlFamily = Int32(value[0]) 533 | let bits = UInt8(bitPattern: value[1]) 534 | let isCidr: Bool = convert(value.advanced(by: 2)) 535 | 536 | let family: Int32 537 | let length: Int 538 | let standardBits: UInt8 539 | if psqlFamily == PGSQL_AF_INET6 { 540 | family = AF_INET6 541 | length = Int(INET6_ADDRSTRLEN) 542 | standardBits = 128 543 | } else { 544 | family = AF_INET 545 | length = Int(INET_ADDRSTRLEN) 546 | standardBits = 32 547 | } 548 | 549 | var buffer = [CChar](repeating: 0, count: length) 550 | inet_ntop(family, value.advanced(by: 4), &buffer, socklen_t(length)) 551 | let inetString = String(cString: buffer) 552 | 553 | if !isCidr && bits == standardBits { 554 | return inetString 555 | } 556 | return "\(inetString)/\(bits)" 557 | } 558 | 559 | static func parseMacAddress(value: UnsafeMutablePointer) -> String { 560 | return (0..<6) 561 | .map { UInt8(bitPattern: value[$0]).lowercaseHexPair } 562 | .joined(separator: ":") 563 | } 564 | 565 | // MARK: - Bit String 566 | 567 | static func parseBitString(value: UnsafeMutablePointer, length: Int) -> String { 568 | let bitLength = parseInt32(value: value) 569 | let bitString = (4..=4.0) 576 | return String(bitString[.., length: Int) -> StructuredData { 43 | switch type { 44 | case .bool: 45 | return .bool(value[0] != 0) 46 | 47 | case .char, .name, .text, .json, .xml, .bpchar, .varchar: 48 | let string = BinaryUtils.parseString(value: value, length: length) 49 | return .string(string) 50 | 51 | case .jsonb: 52 | // Ignore jsonb version number 53 | let jsonValue = value.advanced(by: 1) 54 | let string = BinaryUtils.parseString(value: jsonValue, length: length - 1) 55 | return .string(string) 56 | 57 | case .int2: 58 | let integer = BinaryUtils.parseInt16(value: value) 59 | return .number(.int(Int(integer))) 60 | 61 | case .int4: 62 | let integer = BinaryUtils.parseInt32(value: value) 63 | return .number(.int(Int(integer))) 64 | 65 | case .int8: 66 | let integer = BinaryUtils.parseInt64(value: value) 67 | if let intValue = Int(exactly: integer) { 68 | return .number(.int(intValue)) 69 | } else { 70 | return .number(.double(Double(integer))) 71 | } 72 | 73 | case .bytea: 74 | let bytes = BinaryUtils.parseBytes(value: value, length: length) 75 | return .bytes(bytes) 76 | 77 | case .float4: 78 | let float = BinaryUtils.parseFloat32(value: value) 79 | return .number(.double(Double(float))) 80 | 81 | case .float8: 82 | let float = BinaryUtils.parseFloat64(value: value) 83 | return .number(.double(Double(float))) 84 | 85 | case .numeric: 86 | let number = BinaryUtils.parseNumeric(value: value) 87 | return .string(number) 88 | 89 | case .uuid: 90 | let uuid = BinaryUtils.parseUUID(value: value) 91 | return .string(uuid) 92 | 93 | case .timestamp, .timestamptz, .date, .time, .timetz: 94 | let date = BinaryUtils.parseTimetamp(value: value, isInteger: configuration.hasIntegerDatetimes) 95 | return .date(date) 96 | 97 | case .interval: 98 | let interval = BinaryUtils.parseInterval(value: value, timeIsInteger: configuration.hasIntegerDatetimes) 99 | return .string(interval) 100 | 101 | case .point: 102 | let point = BinaryUtils.parsePoint(value: value) 103 | return .string(point) 104 | 105 | case .lseg: 106 | let lseg = BinaryUtils.parseLineSegment(value: value) 107 | return .string(lseg) 108 | 109 | case .path: 110 | let path = BinaryUtils.parsePath(value: value) 111 | return .string(path) 112 | 113 | case .box: 114 | let box = BinaryUtils.parseBox(value: value) 115 | return .string(box) 116 | 117 | case .polygon: 118 | let polygon = BinaryUtils.parsePolygon(value: value) 119 | return .string(polygon) 120 | 121 | case .circle: 122 | let circle = BinaryUtils.parseCircle(value: value) 123 | return .string(circle) 124 | 125 | case .inet, .cidr: 126 | let inet = BinaryUtils.parseIPAddress(value: value) 127 | return .string(inet) 128 | 129 | case .macaddr: 130 | let macaddr = BinaryUtils.parseMacAddress(value: value) 131 | return .string(macaddr) 132 | 133 | case .bit, .varbit: 134 | let bitString = BinaryUtils.parseBitString(value: value, length: length) 135 | return .string(bitString) 136 | } 137 | } 138 | 139 | fileprivate static func parse(type: FieldType.ArraySupported, configuration: Configuration, value: UnsafeMutablePointer, length: Int) -> StructuredData { 140 | // Get the dimension of the array 141 | let arrayDimension = BinaryUtils.parseInt32(value: value) 142 | guard arrayDimension > 0 else { 143 | return .array([]) 144 | } 145 | 146 | var pointer = value.advanced(by: 12) 147 | 148 | // Get all dimension lengths 149 | var dimensionLengths: [Int] = [] 150 | for _ in 0..) -> StructuredData { 160 | // Get the length of the array 161 | let arrayLength = dimensionLengths[0] 162 | 163 | // Create elements array 164 | var values: [StructuredData] = [] 165 | values.reserveCapacity(arrayLength) 166 | 167 | // Loop through array and convert each item 168 | let supportedType = type.supported 169 | for _ in 0.. 1 { 173 | 174 | var subDimensionLengths = dimensionLengths 175 | subDimensionLengths.removeFirst() 176 | 177 | let array = parse(type: type, configuration: configuration, dimensionLengths: subDimensionLengths, pointer: &pointer) 178 | values.append(array) 179 | 180 | } else { 181 | 182 | let elementLength = Int(BinaryUtils.parseInt32(value: pointer)) 183 | pointer = pointer.advanced(by: 4) 184 | 185 | // Check if the element is null 186 | guard elementLength != -1 else { 187 | values.append(.null) 188 | continue 189 | } 190 | 191 | // Parse to node 192 | let item = parse(type: supportedType, configuration: configuration, value: pointer, length: elementLength) 193 | values.append(item) 194 | pointer = pointer.advanced(by: elementLength) 195 | } 196 | } 197 | 198 | return .array(values) 199 | } 200 | } 201 | 202 | 203 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Bind/Bind.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Core 3 | 4 | public final class Bind { 5 | private class SmartPointer { 6 | let bytes: UnsafeMutablePointer 7 | let length: Int 8 | let ownsMemory: Bool 9 | init(bytes: UnsafeMutablePointer, length: Int, ownsMemory: Bool) { 10 | self.bytes = bytes 11 | self.length = length 12 | self.ownsMemory = ownsMemory 13 | } 14 | 15 | deinit { 16 | if ownsMemory { 17 | #if swift(>=5.0) 18 | bytes.deallocate() 19 | #else 20 | bytes.deallocate(capacity: length) 21 | #endif 22 | } 23 | } 24 | } 25 | 26 | // MARK: - Enums 27 | 28 | public enum Format : Int32 { 29 | case string = 0 30 | case binary = 1 31 | } 32 | 33 | // MARK: - Properties 34 | 35 | public var bytes: UnsafeMutablePointer? { 36 | get { 37 | return buffer?.bytes 38 | } 39 | } 40 | public var length: Int { 41 | get { 42 | return buffer?.length ?? 0 43 | } 44 | } 45 | 46 | private let buffer: SmartPointer? 47 | 48 | public let type: FieldType 49 | public let format: Format 50 | 51 | public let configuration: Configuration 52 | public let result: Result? 53 | 54 | // MARK: - Init 55 | 56 | /** 57 | Creates a NULL input binding. 58 | 59 | PQexecParams converts nil pointer to NULL. 60 | see: https://www.postgresql.org/docs/9.1/static/libpq-exec.html 61 | */ 62 | public init(configuration: Configuration) { 63 | self.configuration = configuration 64 | 65 | buffer = nil 66 | 67 | type = nil 68 | format = .string 69 | 70 | result = nil 71 | } 72 | 73 | /** 74 | Creates an input binding from a String. 75 | */ 76 | public convenience init(string: String, configuration: Configuration) { 77 | let utf8CString = string.utf8CString 78 | let count = utf8CString.count 79 | 80 | let bytes = UnsafeMutablePointer.allocate(capacity: count) 81 | for (i, char) in utf8CString.enumerated() { 82 | bytes[i] = char 83 | } 84 | 85 | self.init(bytes: bytes, length: count, ownsMemory: true, type: nil, format: .string, configuration: configuration) 86 | } 87 | 88 | /** 89 | Creates an input binding from a UInt. 90 | */ 91 | public convenience init(bool: Bool, configuration: Configuration) { 92 | let bytes = UnsafeMutablePointer.allocate(capacity: 1) 93 | bytes.initialize(to: bool ? 1 : 0) 94 | 95 | self.init(bytes: bytes, length: 1, ownsMemory: true, type: FieldType(.bool), format: .binary, configuration: configuration) 96 | } 97 | 98 | /** 99 | Creates an input binding from an Int. 100 | */ 101 | public convenience init(int: Int, configuration: Configuration) { 102 | let count = MemoryLayout.size(ofValue: int) 103 | 104 | let type: FieldType 105 | switch count { 106 | case 2: 107 | type = FieldType(.int2) 108 | case 4: 109 | type = FieldType(.int4) 110 | case 8: 111 | type = FieldType(.int8) 112 | default: 113 | // Unsupported integer size, use string instead 114 | self.init(string: int.description, configuration: configuration) 115 | return 116 | } 117 | 118 | var value = int.bigEndian 119 | let (bytes, length) = BinaryUtils.valueToBytes(&value) 120 | self.init(bytes: bytes, length: length, ownsMemory: true, type: type, format: .binary, configuration: configuration) 121 | } 122 | 123 | /** 124 | Creates an input binding from a UInt. 125 | */ 126 | public convenience init(uint: UInt, configuration: Configuration) { 127 | let int: Int 128 | if uint >= UInt(Int.max) { 129 | int = Int.max 130 | } 131 | else { 132 | int = Int(uint) 133 | } 134 | 135 | self.init(int: int, configuration: configuration) 136 | } 137 | 138 | /** 139 | Creates an input binding from an Double. 140 | */ 141 | public convenience init(double: Double, configuration: Configuration) { 142 | let count = MemoryLayout.size(ofValue: double) 143 | 144 | let type: FieldType 145 | switch count { 146 | case 4: 147 | type = FieldType(.float4) 148 | case 8: 149 | type = FieldType(.float8) 150 | default: 151 | // Unsupported float size, use string instead 152 | self.init(string: double.description, configuration: configuration) 153 | return 154 | } 155 | 156 | var value = double.bigEndian 157 | let (bytes, length) = BinaryUtils.valueToBytes(&value) 158 | self.init(bytes: bytes, length: length, ownsMemory: true, type: type, format: .binary, configuration: configuration) 159 | } 160 | 161 | /** 162 | Creates an input binding from an array of bytes. 163 | */ 164 | public convenience init(bytes: Bytes, configuration: Configuration) { 165 | let int8Bytes: UnsafeMutablePointer = UnsafeMutablePointer.allocate(capacity: bytes.count) 166 | for (i, byte) in bytes.enumerated() { 167 | int8Bytes[i] = Int8(bitPattern: byte) 168 | } 169 | 170 | self.init(bytes: int8Bytes, length: bytes.count, ownsMemory: true, type: nil, format: .binary, configuration: configuration) 171 | } 172 | 173 | /** 174 | Creates an input binding from a Date. 175 | */ 176 | public convenience init(date: Date, configuration: Configuration) { 177 | let interval = date.timeIntervalSince(BinaryUtils.TimestampConstants.referenceDate) 178 | 179 | if configuration.hasIntegerDatetimes { 180 | let microseconds = Int64(interval * 1_000_000) 181 | var value = microseconds.bigEndian 182 | let (bytes, length) = BinaryUtils.valueToBytes(&value) 183 | self.init(bytes: bytes, length: length, ownsMemory: true, type: FieldType(.timestamptz), format: .binary, configuration: configuration) 184 | } 185 | else { 186 | let seconds = Float64(interval) 187 | var value = seconds.bigEndian 188 | let (bytes, length) = BinaryUtils.valueToBytes(&value) 189 | self.init(bytes: bytes, length: length, ownsMemory: true, type: FieldType(.timestamptz), format: .binary, configuration: configuration) 190 | } 191 | } 192 | 193 | /** 194 | Creates an input binding from an array. 195 | */ 196 | public convenience init(array: [StructuredData], configuration: Configuration) { 197 | let elements = array.map { $0.postgresArrayElementString } 198 | let arrayString = "{\(elements.joined(separator: ","))}" 199 | self.init(string: arrayString, configuration: configuration) 200 | } 201 | 202 | public init(bytes: UnsafeMutablePointer?, length: Int, ownsMemory: Bool, type: FieldType, format: Format, configuration: Configuration) { 203 | self.buffer = bytes.map { SmartPointer(bytes: $0, length: length, ownsMemory: ownsMemory) } 204 | 205 | self.type = type 206 | self.format = format 207 | 208 | self.configuration = configuration 209 | 210 | result = nil 211 | } 212 | 213 | public init(result: Result, bytes: UnsafeMutablePointer, length: Int, ownsMemory: Bool, type: FieldType, format: Format, configuration: Configuration) { 214 | self.result = result 215 | 216 | self.buffer = SmartPointer(bytes: bytes, length: length, ownsMemory: ownsMemory) 217 | 218 | self.type = type 219 | self.format = format 220 | 221 | self.configuration = configuration 222 | } 223 | } 224 | 225 | extension Node { 226 | /** 227 | Creates in input binding from a PostgreSQL Value. 228 | */ 229 | public func bind(with configuration: Configuration) -> Bind { 230 | switch wrapped { 231 | case .number(let number): 232 | switch number { 233 | case .int(let int): 234 | return Bind(int: int, configuration: configuration) 235 | case .double(let double): 236 | return Bind(double: double, configuration: configuration) 237 | case .uint(let uint): 238 | return Bind(uint: uint, configuration: configuration) 239 | } 240 | case .string(let string): 241 | return Bind(string: string, configuration: configuration) 242 | case .null: 243 | return Bind(configuration: configuration) 244 | case .array(let array): 245 | return Bind(array: array, configuration: configuration) 246 | case .bytes(let bytes): 247 | return Bind(bytes: bytes, configuration: configuration) 248 | case .object(_): 249 | print("Unsupported Node type for PostgreSQL binding, everything except for .object is supported.") 250 | return Bind(configuration: configuration) 251 | case .bool(let bool): 252 | return Bind(bool: bool, configuration: configuration) 253 | case .date(let date): 254 | return Bind(date: date, configuration: configuration) 255 | } 256 | } 257 | } 258 | 259 | extension StructuredData { 260 | var postgresArrayElementString: String { 261 | switch self { 262 | case .null: 263 | return "NULL" 264 | 265 | case .bytes(let bytes): 266 | let hexString = bytes.map { $0.lowercaseHexPair }.joined() 267 | return "\"\\\\x\(hexString)\"" 268 | 269 | case .bool(let bool): 270 | return bool ? "t" : "f" 271 | 272 | case .number(let number): 273 | return number.description 274 | 275 | case .string(let string): 276 | let escapedString = string 277 | .replacingOccurrences(of: "\\", with: "\\\\") 278 | .replacingOccurrences(of: "\"", with: "\\\"") 279 | return "\"\(escapedString)\"" 280 | 281 | case .array(let array): 282 | let elements = array.map { $0.postgresArrayElementString } 283 | return "{\(elements.joined(separator: ","))}" 284 | 285 | case .object(_): 286 | print("Unsupported Node array type for PostgreSQL binding, everything except for .object is supported.") 287 | return "NULL" 288 | 289 | case .date(let date): 290 | return BinaryUtils.Formatters.timestamptz.string(from: date) 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Bind/FieldType.swift: -------------------------------------------------------------------------------- 1 | import CPostgreSQL 2 | 3 | public enum FieldType : ExpressibleByNilLiteral, Equatable { 4 | case supported(Supported) 5 | case array(ArraySupported) 6 | case unsupported(Oid) 7 | case null 8 | 9 | // MARK: - Init 10 | 11 | public init(_ oid: Oid) { 12 | if let supported = Supported(rawValue: oid) { 13 | self = .supported(supported) 14 | } 15 | else if let arraySupported = ArraySupported(rawValue: oid) { 16 | self = .array(arraySupported) 17 | } 18 | else { 19 | self = .unsupported(oid) 20 | } 21 | } 22 | 23 | public init(_ oid: Oid?) { 24 | if let oid = oid { 25 | self.init(oid) 26 | } 27 | else { 28 | self.init(nilLiteral: ()) 29 | } 30 | } 31 | 32 | public init(_ supported: Supported) { 33 | self = .supported(supported) 34 | } 35 | 36 | // MARK: - ExpressibleByNilLiteral 37 | 38 | public init(nilLiteral: ()) { 39 | self = .null 40 | } 41 | 42 | // MARK: - Equatable 43 | 44 | public static func ==(lhs: FieldType, rhs: FieldType) -> Bool { 45 | return lhs.oid == rhs.oid 46 | } 47 | 48 | // MARK: - Oid 49 | 50 | public var oid: Oid? { 51 | switch self { 52 | case .supported(let supported): 53 | return supported.rawValue 54 | 55 | case .unsupported(let oid): 56 | return oid 57 | 58 | case .array(let supported): 59 | return supported.rawValue 60 | 61 | case .null: 62 | return nil 63 | } 64 | } 65 | } 66 | 67 | extension FieldType { 68 | /// Oid values can be found in the following file: 69 | /// https://github.com/postgres/postgres/blob/55c3391d1e6a201b5b891781d21fe682a8c64fe6/src/include/catalog/pg_type.h 70 | public enum Supported: Oid { 71 | case bool = 16 72 | 73 | case int2 = 21 74 | case int4 = 23 75 | case int8 = 20 76 | 77 | case bytea = 17 78 | 79 | case char = 18 80 | case name = 19 81 | case text = 25 82 | case bpchar = 1042 83 | case varchar = 1043 84 | 85 | case json = 114 86 | case jsonb = 3802 87 | case xml = 142 88 | 89 | case float4 = 700 90 | case float8 = 701 91 | 92 | case numeric = 1700 93 | 94 | case date = 1082 95 | case time = 1083 96 | case timetz = 1266 97 | case timestamp = 1114 98 | case timestamptz = 1184 99 | case interval = 1186 100 | 101 | case uuid = 2950 102 | 103 | case point = 600 104 | case lseg = 601 105 | case path = 602 106 | case box = 603 107 | case polygon = 604 108 | case circle = 718 109 | 110 | case cidr = 650 111 | case inet = 869 112 | case macaddr = 829 113 | 114 | case bit = 1560 115 | case varbit = 1562 116 | } 117 | } 118 | 119 | extension FieldType { 120 | public enum ArraySupported: Oid { 121 | case bool = 1000 122 | 123 | case int2 = 1005 124 | case int4 = 1007 125 | case int8 = 1016 126 | 127 | case bytea = 1001 128 | 129 | case char = 1002 130 | case name = 1003 131 | case text = 1009 132 | case bpchar = 1014 133 | case varchar = 1015 134 | 135 | case json = 199 136 | case jsonb = 3807 137 | case xml = 143 138 | 139 | case float4 = 1021 140 | case float8 = 1022 141 | 142 | case numeric = 1231 143 | 144 | case date = 1182 145 | case time = 1183 146 | case timetz = 1270 147 | case timestamp = 1115 148 | case timestamptz = 1185 149 | case interval = 1187 150 | 151 | case uuid = 2951 152 | 153 | case point = 1017 154 | case lseg = 1018 155 | case path = 1019 156 | case box = 1020 157 | case polygon = 1027 158 | case circle = 719 159 | 160 | case cidr = 651 161 | case inet = 1041 162 | case macaddr = 1040 163 | 164 | case bit = 1561 165 | case varbit = 1563 166 | 167 | // MARK: - Supported 168 | 169 | public init(_ supported: Supported) { 170 | switch supported { 171 | case .bool: self = .bool 172 | case .int2: self = .int2 173 | case .int4: self = .int4 174 | case .int8: self = .int8 175 | case .bytea: self = .bytea 176 | case .char: self = .char 177 | case .name: self = .name 178 | case .text: self = .text 179 | case .bpchar: self = .bpchar 180 | case .varchar: self = .varchar 181 | case .json: self = .json 182 | case .jsonb: self = .jsonb 183 | case .xml: self = .xml 184 | case .float4: self = .float4 185 | case .float8: self = .float8 186 | case .numeric: self = .numeric 187 | case .date: self = .date 188 | case .time: self = .time 189 | case .timetz: self = .timetz 190 | case .timestamp: self = .timestamp 191 | case .timestamptz: self = .timestamptz 192 | case .interval: self = .interval 193 | case .uuid: self = .uuid 194 | case .point: self = .point 195 | case .lseg: self = .lseg 196 | case .path: self = .path 197 | case .box: self = .box 198 | case .polygon: self = .polygon 199 | case .circle: self = .circle 200 | case .cidr: self = .cidr 201 | case .inet: self = .inet 202 | case .macaddr: self = .macaddr 203 | case .bit: self = .bit 204 | case .varbit: self = .varbit 205 | } 206 | } 207 | 208 | public var supported: Supported { 209 | switch self { 210 | case .bool: return .bool 211 | case .int2: return .int2 212 | case .int4: return .int4 213 | case .int8: return .int8 214 | case .bytea: return .bytea 215 | case .char: return .char 216 | case .name: return .name 217 | case .text: return .text 218 | case .bpchar: return .bpchar 219 | case .varchar: return .varchar 220 | case .json: return .json 221 | case .jsonb: return .jsonb 222 | case .xml: return .xml 223 | case .float4: return .float4 224 | case .float8: return .float8 225 | case .numeric: return .numeric 226 | case .date: return .date 227 | case .time: return .time 228 | case .timetz: return .timetz 229 | case .timestamp: return .timestamp 230 | case .timestamptz: return .timestamptz 231 | case .interval: return .interval 232 | case .uuid: return .uuid 233 | case .point: return .point 234 | case .lseg: return .lseg 235 | case .path: return .path 236 | case .box: return .box 237 | case .polygon: return .polygon 238 | case .circle: return .circle 239 | case .cidr: return .cidr 240 | case .inet: return .inet 241 | case .macaddr: return .macaddr 242 | case .bit: return .bit 243 | case .varbit: return .varbit 244 | } 245 | } 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Configuration.swift: -------------------------------------------------------------------------------- 1 | public struct Configuration { 2 | // Indicates whether date and time values are stored as Int64 or Float64 3 | public var hasIntegerDatetimes: Bool 4 | } 5 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Connection.swift: -------------------------------------------------------------------------------- 1 | import CPostgreSQL 2 | import Dispatch 3 | 4 | // This structure represents a handle to one database connection. 5 | // It is used for almost all PostgreSQL functions. 6 | // Do not try to make a copy of a PostgreSQL structure. 7 | // There is no guarantee that such a copy will be usable. 8 | public final class Connection: ConnInfoInitializable { 9 | 10 | // MARK: - CConnection 11 | 12 | public typealias CConnection = OpaquePointer 13 | 14 | public let cConnection: CConnection 15 | 16 | // MARK: - Init 17 | 18 | public init(connInfo: ConnInfo) throws { 19 | let string: String 20 | 21 | switch connInfo { 22 | case .raw(let info): 23 | string = info 24 | case .params(let params): 25 | string = params.map({ "\($0)='\($1)'" }).joined() 26 | case .basic(let hostname, let port, let database, let user, let password): 27 | string = "host='\(hostname)' port='\(port)' dbname='\(database)' user='\(user)' password='\(password)' client_encoding='UTF8'" 28 | } 29 | 30 | cConnection = PQconnectdb(string) 31 | try validateConnection() 32 | } 33 | 34 | // MARK: - Deinit 35 | 36 | deinit { 37 | try? close() 38 | } 39 | 40 | // MARK: - Execute 41 | 42 | @discardableResult 43 | public func execute(_ query: String, _ values: [Node] = []) throws -> Node { 44 | let binds = values.map { $0.bind(with: configuration) } 45 | return try execute(query, binds) 46 | } 47 | 48 | @discardableResult 49 | public func execute(_ query: String, _ binds: [Bind]) throws -> Node { 50 | var types: [Oid] = [] 51 | types.reserveCapacity(binds.count) 52 | 53 | var formats: [Int32] = [] 54 | formats.reserveCapacity(binds.count) 55 | 56 | var values: [UnsafePointer?] = [] 57 | values.reserveCapacity(binds.count) 58 | 59 | var lengths: [Int32] = [] 60 | lengths.reserveCapacity(binds.count) 61 | 62 | for bind in binds { 63 | 64 | types.append(bind.type.oid ?? 0) 65 | formats.append(bind.format.rawValue) 66 | values.append(bind.bytes) 67 | lengths.append(Int32(bind.length)) 68 | } 69 | 70 | let resultPointer: Result.Pointer? = PQexecParams( 71 | cConnection, 72 | query, 73 | Int32(binds.count), 74 | types, 75 | values, 76 | lengths, 77 | formats, 78 | Bind.Format.binary.rawValue 79 | ) 80 | 81 | let result = Result(pointer: resultPointer, connection: self) 82 | return try result.parseData() 83 | } 84 | 85 | // MARK: - Connection Status 86 | 87 | public var isConnected: Bool { 88 | return PQstatus(cConnection) == CONNECTION_OK 89 | } 90 | 91 | public var status: ConnStatusType { 92 | return PQstatus(cConnection) 93 | } 94 | 95 | private func validateConnection() throws { 96 | guard isConnected else { 97 | throw PostgreSQLError(code: .connectionFailure, connection: self) 98 | } 99 | } 100 | 101 | public func reset() throws { 102 | try validateConnection() 103 | PQreset(cConnection) 104 | } 105 | 106 | public func close() throws { 107 | try validateConnection() 108 | PQfinish(cConnection) 109 | } 110 | 111 | // MARK: - Transaction 112 | 113 | public enum TransactionIsolationLevel { 114 | case readCommitted 115 | case repeatableRead 116 | case serializable 117 | 118 | var sqlName: String { 119 | switch self { 120 | case .readCommitted: 121 | return "READ COMMITTED" 122 | 123 | case .repeatableRead: 124 | return "REPEATABLE READ" 125 | 126 | case .serializable: 127 | return "SERIALIZABLE" 128 | } 129 | } 130 | } 131 | 132 | public func transaction(isolationLevel: TransactionIsolationLevel = .readCommitted, closure: () throws -> R) throws -> R { 133 | try execute("BEGIN TRANSACTION ISOLATION LEVEL \(isolationLevel.sqlName)") 134 | 135 | let value: R 136 | do { 137 | value = try closure() 138 | } catch { 139 | // rollback changes and then rethrow the error 140 | try execute("ROLLBACK") 141 | throw error 142 | } 143 | 144 | try execute("COMMIT") 145 | return value 146 | } 147 | 148 | // MARK: - LISTEN/NOTIFY 149 | 150 | public struct Notification { 151 | public let pid: Int 152 | public let channel: String 153 | public let payload: String? 154 | 155 | init(pgNotify: PGnotify) { 156 | channel = String(cString: pgNotify.relname) 157 | pid = Int(pgNotify.be_pid) 158 | 159 | if pgNotify.extra != nil { 160 | let string = String(cString: pgNotify.extra) 161 | if !string.isEmpty { 162 | payload = string 163 | } 164 | else { 165 | payload = nil 166 | } 167 | } 168 | else { 169 | payload = nil 170 | } 171 | } 172 | } 173 | 174 | /// Registers as a listener on a specific notification channel. 175 | /// 176 | /// - Parameters: 177 | /// - channel: The channel to register for. 178 | /// - queue: The queue to perform the listening on. 179 | /// - callback: Callback containing any received notification or error and a boolean which can be set to true to stop listening. 180 | public func listen(toChannel channel: String, on queue: DispatchQueue = DispatchQueue.global(), callback: @escaping (Notification?, Error?, inout Bool) -> Void) { 181 | queue.async { 182 | var stop: Bool = false 183 | 184 | do { 185 | try self.execute("LISTEN \(channel)") 186 | 187 | while !stop { 188 | try self.validateConnection() 189 | 190 | // Sleep to avoid looping continuously on cpu 191 | sleep(1) 192 | 193 | PQconsumeInput(self.cConnection) 194 | 195 | while !stop, let pgNotify = PQnotifies(self.cConnection) { 196 | let notification = Notification(pgNotify: pgNotify.pointee) 197 | 198 | callback(notification, nil, &stop) 199 | 200 | PQfreemem(pgNotify) 201 | } 202 | } 203 | } 204 | catch { 205 | callback(nil, error, &stop) 206 | } 207 | } 208 | } 209 | 210 | public func notify(channel: String, payload: String? = nil) throws { 211 | if let payload = payload { 212 | try execute("NOTIFY \(channel), '\(payload)'") 213 | } 214 | else { 215 | try execute("NOTIFY \(channel)") 216 | } 217 | } 218 | 219 | // MARK: - Configuration 220 | 221 | private var cachedConfiguration: Configuration? 222 | 223 | public var configuration: Configuration { 224 | if let configuration = cachedConfiguration { 225 | return configuration 226 | } 227 | 228 | let hasIntegerDatetimes = getBooleanParameterStatus(key: "integer_datetimes", default: true) 229 | 230 | let configuration = Configuration(hasIntegerDatetimes: hasIntegerDatetimes) 231 | cachedConfiguration = configuration 232 | 233 | return configuration 234 | } 235 | 236 | private func getBooleanParameterStatus(key: String, `default` defaultValue: Bool = false) -> Bool { 237 | guard let value = PQparameterStatus(cConnection, "integer_datetimes") else { 238 | return defaultValue 239 | } 240 | return String(cString: value) == "on" 241 | } 242 | } 243 | 244 | extension Connection { 245 | @discardableResult 246 | public func execute(_ query: String, _ representable: [NodeRepresentable]) throws -> Node { 247 | let values = try representable.map { 248 | return try $0.makeNode(in: PostgreSQLContext.shared) 249 | } 250 | 251 | return try execute(query, values) 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/ConnectionInfo.swift: -------------------------------------------------------------------------------- 1 | public enum ConnInfo { 2 | case raw(String) 3 | case params([String: String]) 4 | case basic(hostname: String, port: Int, database: String, user: String, password: String) 5 | } 6 | 7 | public protocol ConnInfoInitializable { 8 | init(connInfo: ConnInfo) throws 9 | } 10 | 11 | extension ConnInfoInitializable { 12 | public init(connInfo: String) throws { 13 | try self.init(connInfo: .raw(connInfo)) 14 | } 15 | 16 | public init(params: [String: String]) throws { 17 | try self.init(connInfo: .params(params)) 18 | } 19 | 20 | public init(hostname: String, port: Int = 5432, database: String, user: String, password: String) throws { 21 | try self.init(connInfo: .basic(hostname: hostname, port: port, database: database, user: user, password: password)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Context.swift: -------------------------------------------------------------------------------- 1 | import Node 2 | 3 | public final class PostgreSQLContext: Context { 4 | internal static let shared = PostgreSQLContext() 5 | fileprivate init() {} 6 | } 7 | 8 | extension Context { 9 | public var isPostgreSQL: Bool { 10 | guard let _ = self as? PostgreSQLContext else { return false } 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Database.swift: -------------------------------------------------------------------------------- 1 | import CPostgreSQL 2 | 3 | public final class Database: ConnInfoInitializable { 4 | 5 | // MARK: - Properties 6 | 7 | public let connInfo: ConnInfo 8 | 9 | // MARK: - Init 10 | 11 | public init(connInfo: ConnInfo) throws { 12 | self.connInfo = connInfo 13 | } 14 | 15 | /// Creates a new connection to 16 | /// the database that can be reused between executions. 17 | /// 18 | /// The connection will close automatically when deinitialized. 19 | public func makeConnection() throws -> Connection { 20 | return try Connection(connInfo: connInfo) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Error.swift: -------------------------------------------------------------------------------- 1 | import CPostgreSQL 2 | 3 | /// A list of all Error messages that 4 | /// can be thrown from calls to `Database`. 5 | /// 6 | /// All Error objects contain a String which 7 | /// contains PostgreSQL's last error message. 8 | public struct PostgreSQLError: Error { 9 | public let code: Code 10 | public let reason: String 11 | } 12 | 13 | public enum PostgresSQLStatusError: Error { 14 | case emptyQuery 15 | case badResponse 16 | } 17 | 18 | extension PostgreSQLError { 19 | public enum Code: String { 20 | // Class 01 — Warning 21 | case warning = "01000" 22 | case dynamicResultSetsReturned = "0100C" 23 | case implicitZeroBitPadding = "01008" 24 | case nullValueEliminatedInSetFunction = "01003" 25 | case privilegeNotGranted = "01007" 26 | case privilegeNotRevoked = "01006" 27 | case stringDataRightTruncationWarning = "01004" 28 | case deprecatedFeature = "01P01" 29 | // Class 02 — No Data (this is also a warning class per the SQL standard) 30 | case noData = "02000" 31 | case noAdditionalDynamicResultSetsReturned = "02001" 32 | // Class 03 — SQL Statement Not Yet Complete 33 | case sqlStatementNotYetComplete = "03000" 34 | // Class 08 — Connection Exception 35 | case connectionException = "08000" 36 | case connectionDoesNotExist = "08003" 37 | case connectionFailure = "08006" 38 | case sqlclientUnableToEstablishSqlconnection = "08001" 39 | case sqlserverRejectedEstablishmentOfSqlconnection = "08004" 40 | case transactionResolutionUnknown = "08007" 41 | case protocolViolation = "08P01" 42 | // Class 09 — Triggered Action Exception 43 | case triggeredActionException = "09000" 44 | // Class 0A — Feature Not Supported 45 | case featureNotSupported = "0A000" 46 | // Class 0B — Invalid Transaction Initiation 47 | case invalidTransactionInitiation = "0B000" 48 | // Class 0F — Locator Exception 49 | case locatorException = "0F000" 50 | case invalidLocatorSpecification = "0F001" 51 | // Class 0L — Invalid Grantor 52 | case invalidGrantor = "0L000" 53 | case invalidGrantOperation = "0LP01" 54 | // Class 0P — Invalid Role Specification 55 | case invalidRoleSpecification = "0P000" 56 | // Class 0Z — Diagnostics Exception 57 | case diagnosticsException = "0Z000" 58 | case stackedDiagnosticsAccessedWithoutActiveHandler = "0Z002" 59 | // Class 20 — Case Not Found 60 | case caseNotFound = "20000" 61 | // Class 21 — Cardinality Violation 62 | case cardinalityViolation = "21000" 63 | // Class 22 — Data Exception 64 | case dataException = "22000" 65 | case arraySubscriptError = "2202E" 66 | case characterNotInRepertoire = "22021" 67 | case datetimeFieldOverflow = "22008" 68 | case divisionByZero = "22012" 69 | case errorInAssignment = "22005" 70 | case escapeCharacterConflict = "2200B" 71 | case indicatorOverflow = "22022" 72 | case intervalFieldOverflow = "22015" 73 | case invalidArgumentForLogarithm = "2201E" 74 | case invalidArgumentForNtileFunction = "22014" 75 | case invalidArgumentForNthValueFunction = "22016" 76 | case invalidArgumentForPowerFunction = "2201F" 77 | case invalidArgumentForWidthBucketFunction = "2201G" 78 | case invalidCharacterValueForCast = "22018" 79 | case invalidDatetimeFormat = "22007" 80 | case invalidEscapeCharacter = "22019" 81 | case invalidEscapeOctet = "2200D" 82 | case invalidEscapeSequence = "22025" 83 | case nonstandardUseOfEscapeCharacter = "22P06" 84 | case invalidIndicatorParameterValue = "22010" 85 | case invalidParameterValue = "22023" 86 | case invalidRegularExpression = "2201B" 87 | case invalidRowCountInLimitClause = "2201W" 88 | case invalidRowCountInResultOffsetClause = "2201X" 89 | case invalidTablesampleArgument = "2202H" 90 | case invalidTablesampleRepeat = "2202G" 91 | case invalidTimeZoneDisplacementValue = "22009" 92 | case invalidUseOfEscapeCharacter = "2200C" 93 | case mostSpecificTypeMismatch = "2200G" 94 | case nullValueNotAllowed = "22004" 95 | case nullValueNoIndicatorParameter = "22002" 96 | case numericValueOutOfRange = "22003" 97 | case stringDataLengthMismatch = "22026" 98 | case stringDataRightTruncationException = "22001" 99 | case substringError = "22011" 100 | case trimError = "22027" 101 | case unterminatedCString = "22024" 102 | case zeroLengthCharacterString = "2200F" 103 | case floatingPointException = "22P01" 104 | case invalidTextRepresentation = "22P02" 105 | case invalidBinaryRepresentation = "22P03" 106 | case badCopyFileFormat = "22P04" 107 | case untranslatableCharacter = "22P05" 108 | case notAnXmlDocument = "2200L" 109 | case invalidXmlDocument = "2200M" 110 | case invalidXmlContent = "2200N" 111 | case invalidXmlComment = "2200S" 112 | case invalidXmlProcessingInstruction = "2200T" 113 | // Class 23 — Integrity Constraint Violation 114 | case integrityConstraintViolation = "23000" 115 | case restrictViolation = "23001" 116 | case notNullViolation = "23502" 117 | case foreignKeyViolation = "23503" 118 | case uniqueViolation = "23505" 119 | case checkViolation = "23514" 120 | case exclusionViolation = "23P01" 121 | // Class 24 — Invalid Cursor State 122 | case invalidCursorState = "24000" 123 | // Class 25 — Invalid Transaction State 124 | case invalidTransactionState = "25000" 125 | case activeSqlTransaction = "25001" 126 | case branchTransactionAlreadyActive = "25002" 127 | case heldCursorRequiresSameIsolationLevel = "25008" 128 | case inappropriateAccessModeForBranchTransaction = "25003" 129 | case inappropriateIsolationLevelForBranchTransaction = "25004" 130 | case noActiveSqlTransactionForBranchTransaction = "25005" 131 | case readOnlySqlTransaction = "25006" 132 | case schemaAndDataStatementMixingNotSupported = "25007" 133 | case noActiveSqlTransaction = "25P01" 134 | case inFailedSqlTransaction = "25P02" 135 | case idleInTransactionSessionTimeout = "25P03" 136 | // Class 26 — Invalid SQL Statement Name 137 | case invalidSqlStatementName = "26000" 138 | // Class 27 — Triggered Data Change Violation 139 | case triggeredDataChangeViolation = "27000" 140 | // Class 28 — Invalid Authorization Specification 141 | case invalidAuthorizationSpecification = "28000" 142 | case invalidPassword = "28P01" 143 | // Class 2B — Dependent Privilege Descriptors Still Exist 144 | case dependentPrivilegeDescriptorsStillExist = "2B000" 145 | case dependentObjectsStillExist = "2BP01" 146 | // Class 2D — Invalid Transaction Termination 147 | case invalidTransactionTermination = "2D000" 148 | // Class 2F — SQL Routine Exception 149 | case sqlRoutineException = "2F000" 150 | case functionExecutedNoReturnStatement = "2F005" 151 | case modifyingSqlDataNotPermittedSQL = "2F002" 152 | case prohibitedSqlStatementAttemptedSQL = "2F003" 153 | case readingSqlDataNotPermittedSQL = "2F004" 154 | // Class 34 — Invalid Cursor Name 155 | case invalidCursorName = "34000" 156 | // Class 38 — External Routine Exception 157 | case externalRoutineException = "38000" 158 | case containingSqlNotPermitted = "38001" 159 | case modifyingSqlDataNotPermittedExternal = "38002" 160 | case prohibitedSqlStatementAttemptedExternal = "38003" 161 | case readingSqlDataNotPermittedExternal = "38004" 162 | // Class 39 — External Routine Invocation Exception 163 | case externalRoutineInvocationException = "39000" 164 | case invalidSqlstateReturned = "39001" 165 | // case null_value_not_allowed = "39004" 166 | case triggerProtocolViolated = "39P01" 167 | case srfProtocolViolated = "39P02" 168 | case event_trigger_protocol_violated = "39P03" 169 | // Class 3B — Savepoint Exception 170 | case savepointException = "3B000" 171 | case invalidSavepointSpecification = "3B001" 172 | // Class 3D — Invalid Catalog Name 173 | case invalidCatalogName = "3D000" 174 | // Class 3F — Invalid Schema Name 175 | case invalidSchemaName = "3F000" 176 | // Class 40 — Transaction Rollback 177 | case transactionRollback = "40000" 178 | case transactionIntegrityConstraintViolation = "40002" 179 | case serializationFailure = "40001" 180 | case statementCompletionUnknown = "40003" 181 | case deadlockDetected = "40P01" 182 | // Class 42 — Syntax Error or Access Rule Violation 183 | case syntaxErrorOrAccessRuleViolation = "42000" 184 | case syntaxError = "42601" 185 | case insufficientPrivilege = "42501" 186 | case cannotCoerce = "42846" 187 | case groupingError = "42803" 188 | case windowingError = "42P20" 189 | case invalidRecursion = "42P19" 190 | case invalidForeignKey = "42830" 191 | case invalidName = "42602" 192 | case nameTooLong = "42622" 193 | case reservedName = "42939" 194 | case datatypeMismatch = "42804" 195 | case indeterminateDatatype = "42P18" 196 | case collationMismatch = "42P21" 197 | case indeterminateCollation = "42P22" 198 | case wrongObjectType = "42809" 199 | case undefinedColumn = "42703" 200 | case undefinedFunction = "42883" 201 | case undefinedTable = "42P01" 202 | case undefinedParameter = "42P02" 203 | case undefinedObject = "42704" 204 | case duplicateColumn = "42701" 205 | case duplicateCursor = "42P03" 206 | case duplicateDatabase = "42P04" 207 | case duplicateFunction = "42723" 208 | case duplicatePreparedStatement = "42P05" 209 | case duplicateSchema = "42P06" 210 | case duplicateTable = "42P07" 211 | case duplicateAlias = "42712" 212 | case duplicateObject = "42710" 213 | case ambiguousColumn = "42702" 214 | case ambiguousFunction = "42725" 215 | case ambiguousParameter = "42P08" 216 | case ambiguousAlias = "42P09" 217 | case invalidColumnReference = "42P10" 218 | case invalidColumnDefinition = "42611" 219 | case invalidCursorDefinition = "42P11" 220 | case invalidDatabaseDefinition = "42P12" 221 | case invalidFunctionDefinition = "42P13" 222 | case invalidPreparedStatementDefinition = "42P14" 223 | case invalidSchemaDefinition = "42P15" 224 | case invalidTableDefinition = "42P16" 225 | case invalidObjectDefinition = "42P17" 226 | // Class 44 — WITH CHECK OPTION Violation 227 | case withCheckOptionViolation = "44000" 228 | // Class 53 — Insufficient Resources 229 | case insufficientResources = "53000" 230 | case diskFull = "53100" 231 | case outOfMemory = "53200" 232 | case tooManyConnections = "53300" 233 | case configurationLimitExceeded = "53400" 234 | // Class 54 — Program Limit Exceeded 235 | case programLimitExceeded = "54000" 236 | case statementTooComplex = "54001" 237 | case tooManyColumns = "54011" 238 | case tooManyArguments = "54023" 239 | // Class 55 — Object Not In Prerequisite State 240 | case objectNotInPrerequisiteState = "55000" 241 | case objectInUse = "55006" 242 | case cantChangeRuntimeParam = "55P02" 243 | case lockNotAvailable = "55P03" 244 | // Class 57 — Operator Intervention 245 | case operatorIntervention = "57000" 246 | case queryCanceled = "57014" 247 | case adminShutdown = "57P01" 248 | case crashShutdown = "57P02" 249 | case cannotConnectNow = "57P03" 250 | case databaseDropped = "57P04" 251 | // Class 58 — System Error (errors external to PostgreSQL itself) 252 | case systemError = "58000" 253 | case ioError = "58030" 254 | case undefinedFile = "58P01" 255 | case duplicateFile = "58P02" 256 | // Class 72 — Snapshot Failure 257 | case snapshotTooOld = "72000" 258 | // Class F0 — Configuration File Error 259 | case configFileError = "F0000" 260 | case lockFileExists = "F0001" 261 | // Class HV — Foreign Data Wrapper Error (SQL/MED) 262 | case fdwError = "HV000" 263 | case fdwColumnNameNotFound = "HV005" 264 | case fdwDynamicParameterValueNeeded = "HV002" 265 | case fdwFunctionSequenceError = "HV010" 266 | case fdwInconsistentDescriptorInformation = "HV021" 267 | case fdwInvalidAttributeValue = "HV024" 268 | case fdwInvalidColumnName = "HV007" 269 | case fdwInvalidColumnNumber = "HV008" 270 | case fdwInvalidDataType = "HV004" 271 | case fdwInvalidDataTypeDescriptors = "HV006" 272 | case fdwInvalidDescriptorFieldIdentifier = "HV091" 273 | case fdwInvalidHandle = "HV00B" 274 | case fdwInvalidOptionIndex = "HV00C" 275 | case fdwInvalidOptionName = "HV00D" 276 | case fdwInvalidStringLengthOrBufferLength = "HV090" 277 | case fdwInvalidStringFormat = "HV00A" 278 | case fdwInvalidUseOfNullPointer = "HV009" 279 | case fdwTooManyHandles = "HV014" 280 | case fdwOutOfMemory = "HV001" 281 | case fdwNoSchemas = "HV00P" 282 | case fdwOptionNameNotFound = "HV00J" 283 | case fdwReplyHandle = "HV00K" 284 | case fdwSchemaNotFound = "HV00Q" 285 | case fdwTableNotFound = "HV00R" 286 | case fdwUnableToCreateExecution = "HV00L" 287 | case fdwUnableToCreateReply = "HV00M" 288 | case fdwUnableToEstablishConnection = "HV00N" 289 | case plpgsqlError = "P0000" 290 | case raiseException = "P0001" 291 | case noDataFound = "P0002" 292 | case tooManyRows = "P0003" 293 | case assertFailure = "P0004" 294 | // Class XX — Internal Error 295 | case internalError = "XX000" 296 | case dataCorrupted = "XX001" 297 | case indexCorrupted = "XX002" 298 | 299 | case unknown 300 | } 301 | } 302 | 303 | // MARK: Inits 304 | 305 | extension PostgreSQLError { 306 | public init(code: Code, connection: Connection) { 307 | let reason: String 308 | if let error = PQerrorMessage(connection.cConnection) { 309 | reason = String(cString: error) 310 | } 311 | else { 312 | reason = "Unknown" 313 | } 314 | 315 | self.init(code: code, reason: reason) 316 | } 317 | 318 | public init(result: Result) { 319 | guard let pointer = result.pointer else { 320 | self.init(code: .unknown, reason: "Unknown") 321 | return 322 | } 323 | 324 | let code: Code 325 | if let rawCodePointer = PQresultErrorField(pointer, 67) { // 67 == 'C' == PG_DIAG_SQLSTATE 326 | let rawCode = String(cString: rawCodePointer) 327 | code = Code(rawValue: rawCode) ?? .unknown 328 | } 329 | else { 330 | code = .unknown 331 | } 332 | 333 | let reason: String 334 | if let messagePointer = PQresultErrorMessage(pointer) { 335 | reason = String(cString: messagePointer) 336 | } 337 | else { 338 | reason = "Unknown" 339 | } 340 | 341 | self.init(code: code, reason: reason) 342 | } 343 | } 344 | 345 | // MARK: Debuggable 346 | import Debugging 347 | 348 | extension PostgreSQLError: Debuggable { 349 | public static var readableName: String { 350 | return "PostgreSQL Error" 351 | } 352 | 353 | public var identifier: String { 354 | return "\(code.rawValue) (\(code))" 355 | } 356 | 357 | public var possibleCauses: [String] { 358 | switch code { 359 | case .connectionException, .connectionDoesNotExist, .connectionFailure: 360 | return [ 361 | "The connection to the server degraded during the query", 362 | "The connection has been open for too long", 363 | "Too much data has been sent through the connection" 364 | ] 365 | default: 366 | return [] 367 | } 368 | } 369 | 370 | public var suggestedFixes: [String] { 371 | switch code { 372 | case .syntaxError: 373 | return [ 374 | "Fix the invalid syntax in your query", 375 | "If an ORM has generated this error, report the issue to its GitHub page" 376 | ] 377 | case .connectionException, .connectionFailure: 378 | return [ 379 | "Make sure you have entered the correct username and password", 380 | "Make sure the database has been created" 381 | ] 382 | default: 383 | return [] 384 | } 385 | } 386 | 387 | public var stackOverflowQuestions: [String] { 388 | return [] 389 | } 390 | 391 | public var documentationLinks: [String] { 392 | return [] 393 | } 394 | } 395 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import Node 2 | -------------------------------------------------------------------------------- /Sources/PostgreSQL/Result.swift: -------------------------------------------------------------------------------- 1 | import CPostgreSQL 2 | 3 | public class Result { 4 | 5 | // MARK: - Pointer 6 | 7 | public typealias Pointer = OpaquePointer 8 | 9 | // MARK: - Status 10 | 11 | public enum Status { 12 | case commandOk 13 | case tuplesOk 14 | case copyOut 15 | case copyIn 16 | case copyBoth 17 | case badResponse 18 | case nonFatalError 19 | case fatalError 20 | case emptyQuery 21 | 22 | init(_ pointer: Pointer?) { 23 | guard let pointer = pointer else { 24 | self = .fatalError 25 | return 26 | } 27 | 28 | switch PQresultStatus(pointer) { 29 | case PGRES_COMMAND_OK: 30 | self = .commandOk 31 | case PGRES_TUPLES_OK: 32 | self = .tuplesOk 33 | case PGRES_COPY_OUT: 34 | self = .copyOut 35 | case PGRES_COPY_IN: 36 | self = .copyIn 37 | case PGRES_COPY_BOTH: 38 | self = .copyBoth 39 | case PGRES_BAD_RESPONSE: 40 | self = .badResponse 41 | case PGRES_NONFATAL_ERROR: 42 | self = .nonFatalError 43 | case PGRES_FATAL_ERROR: 44 | self = .fatalError 45 | case PGRES_EMPTY_QUERY: 46 | self = .emptyQuery 47 | default: 48 | self = .fatalError 49 | } 50 | } 51 | } 52 | 53 | // MARK: - Properties 54 | 55 | public let pointer: Pointer? 56 | public let connection: Connection 57 | public let status: Status 58 | 59 | // MARK: - Init 60 | 61 | public init(pointer: Pointer?, connection: Connection) { 62 | self.pointer = pointer 63 | self.connection = connection 64 | status = Status(pointer) 65 | } 66 | 67 | // MARK: - Deinit 68 | 69 | deinit { 70 | if let pointer = pointer { 71 | PQclear(pointer) 72 | } 73 | } 74 | 75 | // MARK: - Value 76 | 77 | public func parseData() throws -> Node { 78 | switch status { 79 | case .nonFatalError, .fatalError: 80 | throw PostgreSQLError(result: self) 81 | 82 | case .badResponse: 83 | throw PostgresSQLStatusError.badResponse 84 | 85 | case .emptyQuery: 86 | throw PostgresSQLStatusError.emptyQuery 87 | 88 | case .copyOut, .copyIn, .copyBoth, .commandOk: 89 | // No data to parse 90 | return Node(.null, in: PostgreSQLContext.shared) 91 | 92 | case .tuplesOk: 93 | break 94 | } 95 | 96 | var results: [StructuredData] = [] 97 | 98 | // This single dictionary is reused for all rows in the result set 99 | // to avoid the runtime overhead of (de)allocating one per row. 100 | var parsed: [String: StructuredData] = [:] 101 | 102 | let rowCount = PQntuples(pointer) 103 | let columnCount = PQnfields(pointer) 104 | 105 | if rowCount > 0 && columnCount > 0 { 106 | for row in 0..=3.2) 221 | XCTAssertEqual(timestamp.timeIntervalSince1970, parsedDate.timeIntervalSince1970, accuracy: 0.001) 222 | #else 223 | XCTAssertEqualWithAccuracy(timestamp.timeIntervalSince1970, parsedDate.timeIntervalSince1970, accuracy: 0.001) 224 | #endif 225 | } 226 | } 227 | 228 | func testParseFloatTimestamp() { 229 | let floatTimestampTests = [ 230 | ("41bfaa1fdbb783e0", "2016-10-31 15:29:31.716856".postgreSQLParsedDate), 231 | ("0000000000000000", "2000-01-01 00:00:00.000".postgreSQLParsedDate), 232 | ("424d63c2d6400000", "9999-12-31 00:00:00.000".postgreSQLParsedDate), 233 | ("c216804b02980000", "1234-05-21 12:13:14.000".postgreSQLParsedDate), 234 | ] 235 | 236 | for (hexString, timestamp) in floatTimestampTests { 237 | var bytes = hexString.hexStringBytes 238 | let parsedDate = BinaryUtils.parseTimetamp(value: &bytes, isInteger: false) 239 | 240 | #if swift(>=3.2) 241 | XCTAssertEqual(timestamp.timeIntervalSince1970, parsedDate.timeIntervalSince1970, accuracy: 0.001) 242 | #else 243 | XCTAssertEqualWithAccuracy(timestamp.timeIntervalSince1970, parsedDate.timeIntervalSince1970, accuracy: 0.001) 244 | #endif 245 | } 246 | } 247 | 248 | func testParseIntegerInterval() { 249 | let intervalTests = [ 250 | ("00000000000f42400000000000000000", ["00:00:01", "0:0:1"]), 251 | ("00000000000000000000000000000000", ["00:00:00", "0:0:0"]), 252 | ("0000000000000000000000020000002d", ["3 years 9 mons 2 days"]), 253 | ("0000000000b8fb960000000100000011", ["1 year 5 mons 1 day 00:00:12.12303", "1 year 5 mons 1 day 0:0:12.123"]), 254 | ("0000000000000000000000000000000c", ["1 year"]), 255 | ("00000000000000000000000000000018", ["2 years"]), 256 | ("00000000000000000000000100000000", ["1 day"]), 257 | ("00000000000000000000000200000000", ["2 days"]), 258 | ("00000000000000000000000000000001", ["1 mon"]), 259 | ("00000000000000000000000000000002", ["2 mons"]), 260 | ("fffffffffff0bdc00000000000000000", ["-00:00:01", "-0:0:1"]), 261 | ("0000000000000000ffffffff00000000", ["-1 days"]), 262 | ("000000000000000000000001fffffff5", ["-11 mons +1 day"]), 263 | ] 264 | 265 | for (hexString, intervals) in intervalTests { 266 | var bytes = hexString.hexStringBytes 267 | let parsedString = BinaryUtils.parseInterval(value: &bytes, timeIsInteger: true) 268 | XCTAssertTrue(intervals.contains(parsedString)) 269 | } 270 | } 271 | 272 | func testParseFloatInterval() { 273 | let intervalTests = [ 274 | ("3ff00000000000000000000000000000", ["00:00:01", "0:0:1"]), 275 | ("00000000000000000000000000000000", ["00:00:00", "0:0:0"]), 276 | ("0000000000000000000000020000002d", ["3 years 9 mons 2 days"]), 277 | ("40283efdc9c4da900000000100000011", ["1 year 5 mons 1 day 00:00:12.12303", "1 year 5 mons 1 day 0:0:12.123"]), 278 | ("0000000000000000000000000000000c", ["1 year"]), 279 | ("00000000000000000000000000000018", ["2 years"]), 280 | ("00000000000000000000000100000000", ["1 day"]), 281 | ("00000000000000000000000200000000", ["2 days"]), 282 | ("00000000000000000000000000000001", ["1 mon"]), 283 | ("00000000000000000000000000000002", ["2 mons"]), 284 | ("bff00000000000000000000000000000", ["-00:00:01", "-0:0:1"]), 285 | ("0000000000000000ffffffff00000000", ["-1 days"]), 286 | ("000000000000000000000001fffffff5", ["-11 mons +1 day"]), 287 | ] 288 | 289 | for (hexString, intervals) in intervalTests { 290 | var bytes = hexString.hexStringBytes 291 | let parsedString = BinaryUtils.parseInterval(value: &bytes, timeIsInteger: false) 292 | XCTAssertTrue(intervals.contains(parsedString)) 293 | } 294 | } 295 | 296 | func testParseUUID() { 297 | let uuids = [ 298 | "5a9279a1-ce1a-429c-8b3c-21c101e748a9", 299 | "74da9128-6aa6-43ec-84be-f564434ff4f1", 300 | "c6508ddd-c9dd-40ed-a378-f30c21022d36", 301 | "fdb8e723-a456-4ab3-b074-ea270a46322d", 302 | "bbee7f8e-1e39-4504-9565-e5df447b7a3e", 303 | "5d0bf4f5-c924-438e-9664-fcf302d9c793", 304 | "9eb40cb4-9520-4abd-ada6-4a2c9b9b06b9", 305 | "b5434cf4-05dd-4d62-bd52-bcc02e270ed0", 306 | "d839789e-f6fe-446f-93b1-5f9521c188fd", 307 | "58ffb8e9-8530-490a-afc1-d01b93b29264", 308 | ] 309 | 310 | for uuid in uuids { 311 | var bytes = uuid.replacingOccurrences(of: "-", with: "").hexStringBytes 312 | let parsedString = BinaryUtils.parseUUID(value: &bytes) 313 | XCTAssertEqual(uuid, parsedString) 314 | } 315 | } 316 | 317 | func testParsePoint() { 318 | let points = [ 319 | ("3ff3333333333333400b333333333333", "(1.2,3.4)"), 320 | ("bff3333333333333c00b333333333333", "(-1.2,-3.4)"), 321 | ("405edd2f1a9fbe77c078e5999999999a", "(123.456,-398.35)"), 322 | ] 323 | 324 | for (hexString, point) in points { 325 | var bytes = hexString.hexStringBytes 326 | let parsedString = BinaryUtils.parsePoint(value: &bytes) 327 | XCTAssertEqual(point, parsedString) 328 | } 329 | } 330 | 331 | func testParseLineSegment() { 332 | let lineSegments = [ 333 | ("3ff3333333333333400b333333333333bff3333333333333c00b333333333333", "[(1.2,3.4),(-1.2,-3.4)]"), 334 | ("bff3333333333333c00b333333333333405edd2f1a9fbe77c078e5999999999a", "[(-1.2,-3.4),(123.456,-398.35)]"), 335 | ("405edd2f1a9fbe77c078e5999999999a3ff3333333333333400b333333333333", "[(123.456,-398.35),(1.2,3.4)]"), 336 | ] 337 | 338 | for (hexString, lineSegment) in lineSegments { 339 | var bytes = hexString.hexStringBytes 340 | let parsedString = BinaryUtils.parseLineSegment(value: &bytes) 341 | XCTAssertEqual(lineSegment, parsedString) 342 | } 343 | } 344 | 345 | func testParsePath() { 346 | let paths = [ 347 | ("00000000033ff3333333333333400b333333333333bff3333333333333c00b333333333333405edd2f1a9fbe77c078e5999999999a", "[(1.2,3.4),(-1.2,-3.4),(123.456,-398.35)]"), 348 | ("0100000002bff3333333333333c00b333333333333405edd2f1a9fbe77c078e5999999999a", "((-1.2,-3.4),(123.456,-398.35))"), 349 | ("0100000002405edd2f1a9fbe77c078e5999999999a3ff3333333333333400b333333333333", "((123.456,-398.35),(1.2,3.4))"), 350 | ("00000000013ff3333333333333400b333333333333", "[(1.2,3.4)]"), 351 | ("0000000000", "[]"), 352 | ("0100000000", "()"), 353 | ] 354 | 355 | for (hexString, path) in paths { 356 | var bytes = hexString.hexStringBytes 357 | let parsedString = BinaryUtils.parsePath(value: &bytes) 358 | XCTAssertEqual(path, parsedString) 359 | } 360 | } 361 | 362 | func testParseBox() { 363 | let boxes = [ 364 | ("3ff3333333333333400b333333333333bff3333333333333c00b333333333333", "(1.2,3.4),(-1.2,-3.4)"), 365 | ("bff3333333333333c00b333333333333405edd2f1a9fbe77c078e5999999999a", "(-1.2,-3.4),(123.456,-398.35)"), 366 | ("405edd2f1a9fbe77c078e5999999999a3ff3333333333333400b333333333333", "(123.456,-398.35),(1.2,3.4)"), 367 | ] 368 | 369 | for (hexString, box) in boxes { 370 | var bytes = hexString.hexStringBytes 371 | let parsedString = BinaryUtils.parseBox(value: &bytes) 372 | XCTAssertEqual(box, parsedString) 373 | } 374 | } 375 | 376 | func testParsePolygon() { 377 | let polygons = [ 378 | ("000000033ff3333333333333400b333333333333bff3333333333333c00b333333333333405edd2f1a9fbe77c078e5999999999a", "((1.2,3.4),(-1.2,-3.4),(123.456,-398.35))"), 379 | ("00000002bff3333333333333c00b333333333333405edd2f1a9fbe77c078e5999999999a", "((-1.2,-3.4),(123.456,-398.35))"), 380 | ("00000002405edd2f1a9fbe77c078e5999999999a3ff3333333333333400b333333333333", "((123.456,-398.35),(1.2,3.4))"), 381 | ("000000013ff3333333333333400b333333333333", "((1.2,3.4))"), 382 | ("00000000", "()"), 383 | ] 384 | 385 | for (hexString, polygon) in polygons { 386 | var bytes = hexString.hexStringBytes 387 | let parsedString = BinaryUtils.parsePolygon(value: &bytes) 388 | XCTAssertEqual(polygon, parsedString) 389 | } 390 | } 391 | 392 | func testParseCircle() { 393 | let points = [ 394 | ("3ff3333333333333400b333333333333407c8b3333333333", "<(1.2,3.4),456.7>"), 395 | ("bff3333333333333c00b3333333333334058800000000000", "<(-1.2,-3.4),98>"), 396 | ("405edd2f1a9fbe77c078e5999999999a3fbf7ced916872b0", "<(123.456,-398.35),0.123>"), 397 | ] 398 | 399 | for (hexString, point) in points { 400 | var bytes = hexString.hexStringBytes 401 | let parsedString = BinaryUtils.parseCircle(value: &bytes) 402 | XCTAssertEqual(point, parsedString) 403 | } 404 | } 405 | 406 | func testParseIPAddress() { 407 | let ipAddressess = [ 408 | ("02200004c0a86480", "192.168.100.128"), 409 | ("02190004c0a86480", "192.168.100.128/25"), 410 | ("03400010200104f8000300ba0000000000000000", "2001:4f8:3:ba::/64"), 411 | ("03800010200104f8000300ba02e081fffe22d1f1", "2001:4f8:3:ba:2e0:81ff:fe22:d1f1"), 412 | ("02200004503c7bff", "80.60.123.255"), 413 | ("0220000400000000", "0.0.0.0"), 414 | ("022000047f000001", "127.0.0.1"), 415 | ("02200104c0a86480", "192.168.100.128/32"), 416 | ("02190104c0a86480", "192.168.100.128/25"), 417 | ("03400110200104f8000300ba0000000000000000", "2001:4f8:3:ba::/64"), 418 | ("03800110200104f8000300ba02e081fffe22d1f1", "2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128"), 419 | ("02200104503c7bff", "80.60.123.255/32"), 420 | ("0220010400000000", "0.0.0.0/32"), 421 | ("022001047f000001", "127.0.0.1/32"), 422 | ] 423 | 424 | for (hexString, ipAddress) in ipAddressess { 425 | var bytes = hexString.hexStringBytes 426 | let parsedString = BinaryUtils.parseIPAddress(value: &bytes) 427 | XCTAssertEqual(ipAddress, parsedString) 428 | } 429 | } 430 | 431 | func testParseMacAddress() { 432 | let macAddressess = [ 433 | "5a:92:79:a1:ce:1a", 434 | "74:da:91:28:6a:a6", 435 | "c6:50:8d:dd:c9:dd", 436 | "fd:b8:e7:23:a4:56", 437 | "bb:ee:7f:8e:1e:39", 438 | "5d:0b:f4:f5:c9:24", 439 | "9e:b4:0c:b4:95:20", 440 | "b5:43:4c:f4:05:dd", 441 | "d8:39:78:9e:f6:fe", 442 | "58:ff:b8:e9:85:30", 443 | ] 444 | 445 | for macAddress in macAddressess { 446 | var bytes = macAddress.replacingOccurrences(of: ":", with: "").hexStringBytes 447 | let parsedString = BinaryUtils.parseMacAddress(value: &bytes) 448 | XCTAssertEqual(macAddress, parsedString) 449 | } 450 | } 451 | 452 | func testParseBitString() { 453 | let bitStrings = [ 454 | ("0000000100", "0"), 455 | ("0000000180", "1"), 456 | ("0000000240", "01"), 457 | ("00000004b0", "1011"), 458 | ("0000000430", "0011"), 459 | ("0000000865", "01100101"), 460 | ("0000002cd238ac8d89e0", "11010010001110001010110010001101100010011110"), 461 | ("0000000800", "00000000"), 462 | ("00000008ff", "11111111"), 463 | ("0000000b0000", "00000000000"), 464 | ("0000000affc0", "1111111111"), 465 | ] 466 | 467 | for (hexString, bitString) in bitStrings { 468 | var bytes = hexString.hexStringBytes 469 | let parsedString = BinaryUtils.parseBitString(value: &bytes, length: bytes.count) 470 | XCTAssertEqual(bitString, parsedString) 471 | } 472 | } 473 | } 474 | -------------------------------------------------------------------------------- /Tests/PostgreSQLTests/ConnectionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import CPostgreSQL 3 | @testable import PostgreSQL 4 | 5 | class ConnectionTests: XCTestCase { 6 | static let allTests = [ 7 | ("testConnection", testConnection), 8 | ("testConnInfoParams", testConnInfoParams), 9 | ("testConnInfoRaw", testConnInfoRaw), 10 | ("testConnectionFailure", testConnectionFailure), 11 | ("testConnectionSuccess", testConnectionSuccess), 12 | ] 13 | 14 | var postgreSQL: PostgreSQL.Database! 15 | 16 | func testConnection() throws { 17 | postgreSQL = PostgreSQL.Database.makeTest() 18 | let conn = try postgreSQL.makeConnection() 19 | 20 | let connection = try postgreSQL.makeConnection() 21 | XCTAssert(conn.status == CONNECTION_OK) 22 | XCTAssertTrue(connection.isConnected) 23 | 24 | try connection.reset() 25 | try connection.close() 26 | XCTAssertFalse(connection.isConnected) 27 | } 28 | 29 | func testConnInfoParams() { 30 | do { 31 | let postgreSQL = try PostgreSQL.Database( 32 | params: ["host": "127.0.0.1", 33 | "port": "5432", 34 | "dbname": "test", 35 | "user": "postgres", 36 | "password": ""]) 37 | let conn = try postgreSQL.makeConnection() 38 | try conn.execute("SELECT version()") 39 | } catch { 40 | XCTFail("Could not connect to database") 41 | } 42 | } 43 | 44 | func testConnInfoRaw() { 45 | do { 46 | let postgreSQL = try PostgreSQL.Database( 47 | connInfo: "host='127.0.0.1' port='5432' dbname='test' user='postgres' password=''") 48 | let conn = try postgreSQL.makeConnection() 49 | try conn.execute("SELECT version()") 50 | } catch { 51 | XCTFail("Could not connect to database") 52 | } 53 | } 54 | 55 | func testConnectionFailure() throws { 56 | let database = try PostgreSQL.Database( 57 | hostname: "127.0.0.1", 58 | port: 5432, 59 | database: "some_long_db_name_that_does_not_exist", 60 | user: "postgres", 61 | password: "" 62 | ) 63 | 64 | try XCTAssertThrowsError(database.makeConnection()) { error in 65 | switch error { 66 | case let postgreSQLError as PostgreSQLError: 67 | XCTAssertEqual(postgreSQLError.code, PostgreSQLError.Code.connectionFailure) 68 | default: 69 | XCTFail("Invalid error") 70 | } 71 | } 72 | } 73 | 74 | func testConnectionSuccess() throws { 75 | do { 76 | let postgreSQL = try PostgreSQL.Database( 77 | hostname: "127.0.0.1", 78 | port: 5432, 79 | database: "test", 80 | user: "postgres", 81 | password: "" 82 | ) 83 | let conn = try postgreSQL.makeConnection() 84 | try conn.execute("SELECT version()") 85 | } catch { 86 | XCTFail("Could not connect to database") 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Tests/PostgreSQLTests/MiscTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PostgreSQL 3 | 4 | class MiscTests: XCTestCase { 5 | static let allTests = [ 6 | ("testContext", testContext) 7 | ] 8 | 9 | func testContext() throws { 10 | let context = PostgreSQLContext.shared.isPostgreSQL 11 | XCTAssert(context == true) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Tests/PostgreSQLTests/PostgreSQLTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import PostgreSQL 3 | import Foundation 4 | 5 | class PostgreSQLTests: XCTestCase { 6 | static let allTests = [ 7 | ("testSelectVersion", testSelectVersion), 8 | ("testTables", testTables), 9 | ("testParameterization", testParameterization), 10 | ("testDataType", testDataType), 11 | ("testDates", testDates), 12 | ("testUUID", testUUID), 13 | ("testInts", testInts), 14 | ("testFloats", testFloats), 15 | ("testNumeric", testNumeric), 16 | ("testNumericAverage", testNumericAverage), 17 | ("testJSON", testJSON), 18 | ("testIntervals", testIntervals), 19 | ("testPoints", testPoints), 20 | ("testLineSegments", testLineSegments), 21 | ("testPaths", testPaths), 22 | ("testBoxes", testBoxes), 23 | ("testPolygons", testPolygons), 24 | ("testCircles", testCircles), 25 | ("testInets", testInets), 26 | ("testCidrs", testCidrs), 27 | ("testMacAddresses", testMacAddresses), 28 | ("testBitStrings", testBitStrings), 29 | ("testVarBitStrings", testVarBitStrings), 30 | ("testUnsupportedObject", testUnsupportedObject), 31 | ("testNotification", testNotification), 32 | ("testNotificationWithPayload", testNotificationWithPayload), 33 | ("testQueryToNode", testQueryToNode) 34 | ] 35 | 36 | var postgreSQL: PostgreSQL.Database! 37 | 38 | override func setUp() { 39 | postgreSQL = PostgreSQL.Database.makeTest() 40 | } 41 | 42 | func testSelectVersion() throws { 43 | let conn = try postgreSQL.makeConnection() 44 | 45 | do { 46 | let results = try conn.execute("SELECT version(), version(), 1337, 3.14, 'what up', NULL") 47 | guard let version = results[0, "version"] else { 48 | XCTFail("Version not in results") 49 | return 50 | } 51 | 52 | guard let string = version.string else { 53 | XCTFail("Version not in results") 54 | return 55 | } 56 | 57 | XCTAssert(string.hasPrefix("PostgreSQL")) 58 | } catch { 59 | XCTFail("Could not select version: \(error)") 60 | } 61 | } 62 | 63 | func testTables() throws { 64 | let conn = try postgreSQL.makeConnection() 65 | 66 | do { 67 | try conn.execute("DROP TABLE IF EXISTS foo") 68 | try conn.execute("CREATE TABLE foo (bar INT, baz VARCHAR(16), bla BOOLEAN)") 69 | try conn.execute("INSERT INTO foo VALUES (42, 'Life', true)") 70 | try conn.execute("INSERT INTO foo VALUES (1337, 'Elite', false)") 71 | try conn.execute("INSERT INTO foo VALUES (9, NULL, true)") 72 | 73 | if let resultBar = try conn.execute("SELECT * FROM foo WHERE bar = 42")[0] { 74 | XCTAssertEqual(resultBar["bar"]?.int, 42) 75 | XCTAssertEqual(resultBar["baz"]?.string, "Life") 76 | XCTAssertEqual(resultBar["bla"]?.bool, true) 77 | } else { 78 | XCTFail("Could not get bar result") 79 | } 80 | 81 | if let resultBaz = try conn.execute("SELECT * FROM foo where baz = 'Elite'")[0] { 82 | XCTAssertEqual(resultBaz["bar"]?.int, 1337) 83 | XCTAssertEqual(resultBaz["baz"]?.string, "Elite") 84 | XCTAssertEqual(resultBaz["bla"]?.bool, false) 85 | } else { 86 | XCTFail("Could not get baz result") 87 | } 88 | 89 | if let resultBaz = try conn.execute("SELECT * FROM foo where bar = 9")[0] { 90 | XCTAssertEqual(resultBaz["bar"]?.int, 9) 91 | XCTAssertEqual(resultBaz["baz"]?.string, nil) 92 | XCTAssertEqual(resultBaz["bla"]?.bool, true) 93 | } else { 94 | XCTFail("Could not get null result") 95 | } 96 | } catch { 97 | XCTFail("Testing tables failed: \(error)") 98 | } 99 | } 100 | 101 | func testParameterization() throws { 102 | let conn = try postgreSQL.makeConnection() 103 | 104 | do { 105 | try conn.execute("DROP TABLE IF EXISTS parameterization") 106 | try conn.execute("CREATE TABLE parameterization (d FLOAT8, i INT, s VARCHAR(16), u INT)") 107 | 108 | try conn.execute("INSERT INTO parameterization VALUES ($1, $2, $3, $4)", [.null, .null, "life".makeNode(in: nil), .null]) 109 | 110 | try conn.execute("INSERT INTO parameterization VALUES (3.14, NULL, 'pi', NULL)") 111 | try conn.execute("INSERT INTO parameterization VALUES (NULL, NULL, 'life', 42)") 112 | try conn.execute("INSERT INTO parameterization VALUES (NULL, -1, 'test', NULL)") 113 | 114 | if let result = try conn.execute("SELECT * FROM parameterization WHERE d = $1", [3.14])[0] { 115 | XCTAssertEqual(result["d"]?.double, 3.14) 116 | XCTAssertEqual(result["i"]?.int, nil) 117 | XCTAssertEqual(result["s"]?.string, "pi") 118 | XCTAssertEqual(result["u"]?.int, nil) 119 | } else { 120 | XCTFail("Could not get pi result") 121 | } 122 | 123 | if let result = try conn.execute("SELECT * FROM parameterization WHERE u = $1", [42])[0] { 124 | XCTAssertEqual(result["d"]?.double, nil) 125 | XCTAssertEqual(result["i"]?.int, nil) 126 | XCTAssertEqual(result["s"]?.string, "life") 127 | XCTAssertEqual(result["u"]?.int, 42) 128 | } else { 129 | XCTFail("Could not get life result") 130 | } 131 | 132 | if let result = try conn.execute("SELECT * FROM parameterization WHERE i = $1", [-1])[0] { 133 | XCTAssertEqual(result["d"]?.double, nil) 134 | XCTAssertEqual(result["i"]?.int, -1) 135 | XCTAssertEqual(result["s"]?.string, "test") 136 | XCTAssertEqual(result["u"]?.int, nil) 137 | } else { 138 | XCTFail("Could not get test by int result") 139 | } 140 | 141 | if let result = try conn.execute("SELECT * FROM parameterization WHERE s = $1", ["test"])[0] { 142 | XCTAssertEqual(result["d"]?.double, nil) 143 | XCTAssertEqual(result["i"]?.int, -1) 144 | XCTAssertEqual(result["s"]?.string, "test") 145 | XCTAssertEqual(result["u"]?.int, nil) 146 | } else { 147 | XCTFail("Could not get test by string result") 148 | } 149 | } catch { 150 | XCTFail("Testing parameterization failed: \(error)") 151 | } 152 | } 153 | 154 | func testDataType() throws { 155 | let conn = try postgreSQL.makeConnection() 156 | 157 | let data: [UInt8] = [1, 2, 3, 4, 5, 0, 6, 7, 8, 9, 0] 158 | 159 | try conn.execute("DROP TABLE IF EXISTS foo") 160 | try conn.execute("CREATE TABLE foo (bar BYTEA)") 161 | try conn.execute("INSERT INTO foo VALUES ($1)", [.bytes(data)]) 162 | 163 | let result = try conn.execute("SELECT * FROM foo")[0] 164 | XCTAssertNotNil(result) 165 | 166 | let resultBytesNode = result!["bar"] 167 | XCTAssertNotNil(resultBytesNode) 168 | 169 | XCTAssertEqual(resultBytesNode!, .bytes(data)) 170 | } 171 | 172 | func testDates() throws { 173 | let conn = try postgreSQL.makeConnection() 174 | 175 | let rows: [Date] = [ 176 | Date(timeIntervalSince1970: 0), 177 | Date(timeIntervalSince1970: 1), 178 | Date(timeIntervalSince1970: 60 * 60 * 24 * 365 * 31), 179 | Date(timeIntervalSince1970: 60 * 60 * 24 * 4), 180 | Date(timeIntervalSince1970: 1.52), 181 | Date(timeIntervalSince1970: 60 * 60 * 24 * 365 + 0.123), 182 | Date(timeIntervalSince1970: -60 * 60 * 24 * 365 + 0.123), 183 | ] 184 | 185 | try conn.execute("DROP TABLE IF EXISTS foo") 186 | try conn.execute("CREATE TABLE foo (id serial, date TIMESTAMP WITH TIME ZONE)") 187 | for row in rows { 188 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 189 | } 190 | 191 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 192 | XCTAssertEqual(result.count, rows.count) 193 | for (i, resultRow) in result.enumerated() { 194 | let node = resultRow["date"] 195 | XCTAssertNotNil(node?.date) 196 | XCTAssertEqual(node!.date!, rows[i]) 197 | } 198 | } 199 | 200 | func testUUID() throws { 201 | let conn = try postgreSQL.makeConnection() 202 | 203 | let uuidString = "7fe1743a-96a8-417c-b6c2-c8bb20d3017e" 204 | 205 | try conn.execute("DROP TABLE IF EXISTS foo") 206 | try conn.execute("CREATE TABLE foo (uuid UUID)") 207 | try conn.execute("INSERT INTO foo VALUES ($1)", [.string(uuidString)]) 208 | 209 | let result = try conn.execute("SELECT * FROM foo")[0] 210 | XCTAssertNotNil(result) 211 | XCTAssertEqual(result!["uuid"]?.string, uuidString) 212 | } 213 | 214 | func testInts() throws { 215 | let conn = try postgreSQL.makeConnection() 216 | 217 | let rows: [(Int16, Int32, Int64)] = [ 218 | (1, 2, 3), 219 | (-1, -2, -3), 220 | (Int16.min, Int32.min, Int64.min), 221 | (Int16.max, Int32.max, Int64.max), 222 | ] 223 | 224 | try conn.execute("DROP TABLE IF EXISTS foo") 225 | try conn.execute("CREATE TABLE foo (id serial, int2 int2, int4 int4, int8 int8)") 226 | for row in rows { 227 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1, $2, $3)", [row.0.makeNode(in: nil), row.1.makeNode(in: nil), row.2.makeNode(in: nil)]) 228 | } 229 | 230 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 231 | XCTAssertEqual(result.count, rows.count) 232 | for (i, resultRow) in result.enumerated() { 233 | let int2 = resultRow["int2"] 234 | XCTAssertNotNil(int2?.int) 235 | XCTAssertEqual(int2!.int!, Int(rows[i].0)) 236 | 237 | let int4 = resultRow["int4"] 238 | XCTAssertNotNil(int4?.int) 239 | XCTAssertEqual(int4!.int!, Int(rows[i].1)) 240 | 241 | let int8 = resultRow["int8"] 242 | XCTAssertNotNil(int8?.double) 243 | XCTAssertEqual(int8!.double!, Double(rows[i].2)) 244 | } 245 | } 246 | 247 | func testFloats() throws { 248 | let conn = try postgreSQL.makeConnection() 249 | 250 | let rows: [(Float32, Float64)] = [ 251 | (1, 2), 252 | (-1, -2), 253 | (1.23, 2.45), 254 | (-1.23, -2.45), 255 | (Float32.min, Float64.min), 256 | (Float32.max, Float64.max), 257 | ] 258 | 259 | try conn.execute("DROP TABLE IF EXISTS foo") 260 | try conn.execute("CREATE TABLE foo (id serial, float4 float4, float8 float8)") 261 | for row in rows { 262 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1, $2)", [row.0.makeNode(in: nil), row.1.makeNode(in: nil)]) 263 | } 264 | 265 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 266 | XCTAssertEqual(result.count, rows.count) 267 | for (i, resultRow) in result.enumerated() { 268 | let float4 = resultRow["float4"] 269 | XCTAssertNotNil(float4?.double) 270 | XCTAssertEqual(float4!.double!, Double(rows[i].0)) 271 | 272 | let float8 = resultRow["float8"] 273 | XCTAssertNotNil(float8?.double) 274 | XCTAssertEqual(float8!.double!, Double(rows[i].1)) 275 | } 276 | } 277 | 278 | func testNumeric() throws { 279 | let conn = try postgreSQL.makeConnection() 280 | 281 | let rows: [String] = [ 282 | "0", 283 | "0.1", 284 | "10.08", 285 | "-0.123", 286 | "123", 287 | "456.7891412341", 288 | "123143236449825.291401412", 289 | "-14982351014.1284121590511", 290 | "100000001000000000000.0000000001000000001", 291 | "NaN", 292 | ] 293 | 294 | try conn.execute("DROP TABLE IF EXISTS foo") 295 | try conn.execute("CREATE TABLE foo (id serial, numeric numeric)") 296 | for row in rows { 297 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 298 | } 299 | 300 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 301 | XCTAssertEqual(result.count, rows.count) 302 | for (i, resultRow) in result.enumerated() { 303 | let numeric = resultRow["numeric"] 304 | XCTAssertNotNil(numeric?.string) 305 | XCTAssertEqual(numeric!.string!, rows[i]) 306 | } 307 | } 308 | 309 | func testNumericAverage() throws { 310 | let conn = try postgreSQL.makeConnection() 311 | 312 | try conn.execute("DROP TABLE IF EXISTS jobs") 313 | try conn.execute("CREATE TABLE jobs (id SERIAL PRIMARY KEY NOT NULL, title VARCHAR(255) NOT NULL, pay INT8 NOT NULL)") 314 | 315 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 100]) 316 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 200]) 317 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 300]) 318 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 400]) 319 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["A", 500]) 320 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["B", 100]) 321 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["B", 200]) 322 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["B", 900]) 323 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["C", 100]) 324 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["C", 1100]) 325 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["D", 100]) 326 | try conn.execute("INSERT INTO jobs (title, pay) VALUES ($1, $2)", ["E", 500]) 327 | 328 | defer { 329 | _ = try? conn.execute("DROP TABLE IF EXISTS jobs") 330 | } 331 | 332 | let result = try conn.execute("select title, avg(pay) as average, count(*) as cnt from jobs group by title order by average desc") 333 | 334 | guard let array = result.array else { 335 | XCTFail("Result was not an array") 336 | return 337 | } 338 | 339 | XCTAssertEqual(5, array.count) 340 | XCTAssertEqual("C", array[0]["title"]?.string) 341 | XCTAssertEqual("E", array[1]["title"]?.string) 342 | XCTAssertEqual("B", array[2]["title"]?.string) 343 | XCTAssertEqual("A", array[3]["title"]?.string) 344 | XCTAssertEqual("D", array[4]["title"]?.string) 345 | XCTAssertEqual(2, array[0]["cnt"]?.int) 346 | XCTAssertEqual(1, array[1]["cnt"]?.int) 347 | XCTAssertEqual(3, array[2]["cnt"]?.int) 348 | XCTAssertEqual(5, array[3]["cnt"]?.int) 349 | XCTAssertEqual(1, array[4]["cnt"]?.int) 350 | XCTAssertEqual(600, array[0]["average"]?.double) 351 | XCTAssertEqual(500, array[1]["average"]?.double) 352 | XCTAssertEqual(400, array[2]["average"]?.double) 353 | XCTAssertEqual(300, array[3]["average"]?.double) 354 | XCTAssertEqual(100, array[4]["average"]?.double) 355 | } 356 | 357 | func testJSON() throws { 358 | let conn = try postgreSQL.makeConnection() 359 | 360 | let rows: [String] = [ 361 | "{}", 362 | "[]", 363 | "true", 364 | "123", 365 | "456.7891412341", 366 | "[1, 2, 3, 4, 5, 6]", 367 | "{\"foo\": \"bar\"}", 368 | ] 369 | 370 | try conn.execute("DROP TABLE IF EXISTS foo") 371 | try conn.execute("CREATE TABLE foo (id serial, json json, jsonb jsonb)") 372 | for row in rows { 373 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1, $2)", [row.makeNode(in: nil), row.makeNode(in: nil)]) 374 | } 375 | 376 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 377 | XCTAssertEqual(result.count, rows.count) 378 | for (i, resultRow) in result.enumerated() { 379 | let json = resultRow["json"] 380 | XCTAssertNotNil(json?.string) 381 | XCTAssertEqual(json!.string!, rows[i]) 382 | 383 | let jsonb = resultRow["jsonb"] 384 | XCTAssertNotNil(jsonb?.string) 385 | XCTAssertEqual(jsonb!.string!, rows[i]) 386 | } 387 | } 388 | 389 | func testIntervals() throws { 390 | let conn = try postgreSQL.makeConnection() 391 | 392 | let rows: [[String]] = [ 393 | ["00:00:01","0:0:1"], 394 | ["00:00:00","0:0:0"], 395 | ["3 years 9 mons 2 days"], 396 | ["1 year 5 mons 1 day 00:00:12.134", "1 year 5 mons 1 day 0:0:12.134"], 397 | ["1 year"], 398 | ["2 years"], 399 | ["1 day"], 400 | ["2 days"], 401 | ["1 mon"], 402 | ["2 mons"], 403 | ["-00:00:01", "-0:0:1"], 404 | ["-1 days"], 405 | ["-11 mons +1 day"], 406 | ] 407 | 408 | try conn.execute("DROP TABLE IF EXISTS foo") 409 | try conn.execute("CREATE TABLE foo (id serial, interval interval)") 410 | for row in rows { 411 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row[0].makeNode(in: nil)]) 412 | } 413 | 414 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 415 | XCTAssertEqual(result.count, rows.count) 416 | for (i, resultRow) in result.enumerated() { 417 | let interval = resultRow["interval"] 418 | XCTAssertNotNil(interval?.string) 419 | XCTAssertTrue(rows[i].contains(interval!.string!)) 420 | } 421 | } 422 | 423 | func testPoints() throws { 424 | let conn = try postgreSQL.makeConnection() 425 | 426 | let rows = [ 427 | "(1.2,3.4)", 428 | "(-1.2,-3.4)", 429 | "(123.456,-298.135)", 430 | ] 431 | 432 | try conn.execute("DROP TABLE IF EXISTS foo") 433 | try conn.execute("CREATE TABLE foo (id serial, point point)") 434 | for row in rows { 435 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 436 | } 437 | 438 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 439 | XCTAssertEqual(result.count, rows.count) 440 | for (i, resultRow) in result.enumerated() { 441 | let point = resultRow["point"] 442 | XCTAssertNotNil(point?.string) 443 | XCTAssertEqual(point!.string!, rows[i]) 444 | } 445 | } 446 | 447 | func testLineSegments() throws { 448 | let conn = try postgreSQL.makeConnection() 449 | 450 | let rows = [ 451 | "[(1.2,3.4),(-1.2,-3.4)]", 452 | "[(-1.2,-3.4),(123.467,-298.135)]", 453 | "[(123.47,-238.123),(1.2,3.4)]", 454 | ] 455 | 456 | try conn.execute("DROP TABLE IF EXISTS foo") 457 | try conn.execute("CREATE TABLE foo (id serial, lseg lseg)") 458 | for row in rows { 459 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 460 | } 461 | 462 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 463 | XCTAssertEqual(result.count, rows.count) 464 | for (i, resultRow) in result.enumerated() { 465 | let lseg = resultRow["lseg"] 466 | XCTAssertNotNil(lseg?.string) 467 | XCTAssertEqual(lseg!.string!, rows[i]) 468 | } 469 | } 470 | 471 | func testPaths() throws { 472 | let conn = try postgreSQL.makeConnection() 473 | 474 | let rows = [ 475 | "[(1.2,3.4),(-1.2,-3.4),(123.67,-598.35)]", 476 | "((-1.2,-3.4),(12.4567,-298.35))", 477 | "((123.47,-235.35),(1.2,3.4))", 478 | "[(1.2,3.4)]", 479 | ] 480 | 481 | try conn.execute("DROP TABLE IF EXISTS foo") 482 | try conn.execute("CREATE TABLE foo (id serial, path path)") 483 | for row in rows { 484 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 485 | } 486 | 487 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 488 | XCTAssertEqual(result.count, rows.count) 489 | for (i, resultRow) in result.enumerated() { 490 | let path = resultRow["path"] 491 | XCTAssertNotNil(path?.string) 492 | XCTAssertEqual(path!.string!, rows[i]) 493 | } 494 | } 495 | 496 | func testBoxes() throws { 497 | let conn = try postgreSQL.makeConnection() 498 | 499 | let rows = [ 500 | "(1.2,3.4),(-1.2,-3.4)", 501 | "(13.467,-3.4),(-1.2,-598.35)", 502 | "(12.467,3.4),(1.2,-358.15)", 503 | ] 504 | 505 | try conn.execute("DROP TABLE IF EXISTS foo") 506 | try conn.execute("CREATE TABLE foo (id serial, box box)") 507 | for row in rows { 508 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 509 | } 510 | 511 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 512 | XCTAssertEqual(result.count, rows.count) 513 | for (i, resultRow) in result.enumerated() { 514 | let box = resultRow["box"] 515 | XCTAssertNotNil(box?.string) 516 | XCTAssertEqual(box!.string!, rows[i]) 517 | } 518 | } 519 | 520 | func testPolygons() throws { 521 | let conn = try postgreSQL.makeConnection() 522 | 523 | let rows = [ 524 | "((1.2,3.4),(-1.2,-3.4),(123.46,-358.25))", 525 | "((-1.2,-3.4),(3.4567,-28.235))", 526 | "((123.467,-98.123),(1.2,3.4))", 527 | "((1.2,3.4))", 528 | ] 529 | 530 | try conn.execute("DROP TABLE IF EXISTS foo") 531 | try conn.execute("CREATE TABLE foo (id serial, polygon polygon)") 532 | for row in rows { 533 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 534 | } 535 | 536 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 537 | XCTAssertEqual(result.count, rows.count) 538 | for (i, resultRow) in result.enumerated() { 539 | let polygon = resultRow["polygon"] 540 | XCTAssertNotNil(polygon?.string) 541 | XCTAssertEqual(polygon!.string!, rows[i]) 542 | } 543 | } 544 | 545 | func testCircles() throws { 546 | let conn = try postgreSQL.makeConnection() 547 | 548 | let rows = [ 549 | "<(1.2,3.4),456.7>", 550 | "<(-1.2,-3.4),98>", 551 | "<(123.67,-598.15),0.123>", 552 | ] 553 | 554 | try conn.execute("DROP TABLE IF EXISTS foo") 555 | try conn.execute("CREATE TABLE foo (id serial, circle circle)") 556 | for row in rows { 557 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 558 | } 559 | 560 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 561 | XCTAssertEqual(result.count, rows.count) 562 | for (i, resultRow) in result.enumerated() { 563 | let circle = resultRow["circle"] 564 | XCTAssertNotNil(circle?.string) 565 | XCTAssertEqual(circle!.string!, rows[i]) 566 | } 567 | } 568 | 569 | func testInets() throws { 570 | let conn = try postgreSQL.makeConnection() 571 | 572 | let rows = [ 573 | "192.168.100.128", 574 | "192.168.100.128/25", 575 | "2001:4f8:3:ba::/64", 576 | "2001:4f8:3:ba:2e0:81ff:fe22:d1f1", 577 | "80.60.123.255", 578 | "0.0.0.0", 579 | "127.0.0.1", 580 | ] 581 | 582 | try conn.execute("DROP TABLE IF EXISTS foo") 583 | try conn.execute("CREATE TABLE foo (id serial, inet inet)") 584 | for row in rows { 585 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 586 | } 587 | 588 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 589 | XCTAssertEqual(result.count, rows.count) 590 | for (i, resultRow) in result.enumerated() { 591 | let inet = resultRow["inet"] 592 | XCTAssertNotNil(inet?.string) 593 | XCTAssertEqual(inet!.string!, rows[i]) 594 | } 595 | } 596 | 597 | func testCidrs() throws { 598 | let conn = try postgreSQL.makeConnection() 599 | 600 | let rows = [ 601 | "192.168.100.128/32", 602 | "192.168.100.128/25", 603 | "2001:4f8:3:ba::/64", 604 | "2001:4f8:3:ba:2e0:81ff:fe22:d1f1/128", 605 | "80.60.123.255/32", 606 | "0.0.0.0/32", 607 | "127.0.0.1/32", 608 | ] 609 | 610 | try conn.execute("DROP TABLE IF EXISTS foo") 611 | try conn.execute("CREATE TABLE foo (id serial, cidr cidr)") 612 | for row in rows { 613 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 614 | } 615 | 616 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 617 | XCTAssertEqual(result.count, rows.count) 618 | for (i, resultRow) in result.enumerated() { 619 | let cidr = resultRow["cidr"] 620 | XCTAssertNotNil(cidr?.string) 621 | XCTAssertEqual(cidr!.string!, rows[i]) 622 | } 623 | } 624 | 625 | func testMacAddresses() throws { 626 | let conn = try postgreSQL.makeConnection() 627 | 628 | let rows = [ 629 | "5a:92:79:a1:ce:1a", 630 | "74:da:91:28:6a:a6", 631 | "c6:50:8d:dd:c9:dd", 632 | "fd:b8:e7:23:a4:56", 633 | "bb:ee:7f:8e:1e:39", 634 | "5d:0b:f4:f5:c9:24", 635 | "9e:b4:0c:b4:95:20", 636 | "b5:43:4c:f4:05:dd", 637 | "d8:39:78:9e:f6:fe", 638 | "58:ff:b8:e9:85:30", 639 | ] 640 | 641 | try conn.execute("DROP TABLE IF EXISTS foo") 642 | try conn.execute("CREATE TABLE foo (id serial, macaddr macaddr)") 643 | for row in rows { 644 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 645 | } 646 | 647 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 648 | XCTAssertEqual(result.count, rows.count) 649 | for (i, resultRow) in result.enumerated() { 650 | let macaddr = resultRow["macaddr"] 651 | XCTAssertNotNil(macaddr?.string) 652 | XCTAssertEqual(macaddr!.string!, rows[i]) 653 | } 654 | } 655 | 656 | func testBitStrings() throws { 657 | let conn = try postgreSQL.makeConnection() 658 | 659 | let rows = [ 660 | "01010", 661 | "00000", 662 | "11111", 663 | "10101", 664 | "11000", 665 | "00111", 666 | "00011", 667 | "00001", 668 | "10000", 669 | ] 670 | 671 | try conn.execute("DROP TABLE IF EXISTS foo") 672 | try conn.execute("CREATE TABLE foo (id serial, bits bit(5))") 673 | for row in rows { 674 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 675 | } 676 | 677 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 678 | XCTAssertEqual(result.count, rows.count) 679 | for (i, resultRow) in result.enumerated() { 680 | let bits = resultRow["bits"] 681 | XCTAssertNotNil(bits?.string) 682 | XCTAssertEqual(bits!.string!, rows[i]) 683 | } 684 | } 685 | 686 | func testVarBitStrings() throws { 687 | let conn = try postgreSQL.makeConnection() 688 | 689 | let rows = [ 690 | "0", 691 | "1", 692 | "01", 693 | "1011", 694 | "0011", 695 | "01100101", 696 | "11010010001110001010110010001101100010011110", 697 | "00000000", 698 | "11111111", 699 | "00000000000", 700 | "1111111111", 701 | ] 702 | 703 | try conn.execute("DROP TABLE IF EXISTS foo") 704 | try conn.execute("CREATE TABLE foo (id serial, bits bit varying)") 705 | for row in rows { 706 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row.makeNode(in: nil)]) 707 | } 708 | 709 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 710 | XCTAssertEqual(result.count, rows.count) 711 | for (i, resultRow) in result.enumerated() { 712 | let bits = resultRow["bits"] 713 | XCTAssertNotNil(bits?.string) 714 | XCTAssertEqual(bits!.string!, rows[i]) 715 | } 716 | } 717 | 718 | func testUnsupportedObject() throws { 719 | let conn = try postgreSQL.makeConnection() 720 | 721 | let rows: [Node] = [ 722 | .object(["1":1, "2":2]), 723 | .object(["1":1, "2":2, "3":3]), 724 | .object([:]), 725 | .object(["1":1]), 726 | ] 727 | 728 | try conn.execute("DROP TABLE IF EXISTS foo") 729 | try conn.execute("CREATE TABLE foo (id serial, text text)") 730 | for row in rows { 731 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, $1)", [row]) 732 | } 733 | 734 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 735 | XCTAssertEqual(result.count, rows.count) 736 | for resultRow in result { 737 | let value = resultRow["text"] 738 | XCTAssertNotNil(value) 739 | XCTAssertEqual(value, Node.null) 740 | } 741 | } 742 | 743 | func testUnsupportedOID() throws { 744 | let conn = try postgreSQL.makeConnection() 745 | 746 | try conn.execute("DROP TABLE IF EXISTS foo") 747 | try conn.execute("CREATE TABLE foo (id serial, oid oid)") 748 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, 1)") 749 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, 2)") 750 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, 123)") 751 | try conn.execute("INSERT INTO foo VALUES (DEFAULT, 456)") 752 | 753 | let result = try conn.execute("SELECT * FROM foo ORDER BY id ASC").array ?? [] 754 | XCTAssertEqual(result.count, 4) 755 | for resultRow in result { 756 | let value = resultRow["oid"] 757 | XCTAssertNotNil(value) 758 | } 759 | } 760 | 761 | func testNotification() throws { 762 | let conn1 = try postgreSQL.makeConnection() 763 | let conn2 = try postgreSQL.makeConnection() 764 | 765 | let testExpectation = expectation(description: "Receive notification") 766 | 767 | conn1.listen(toChannel: "test_channel1") { (notification, error, stop) in 768 | XCTAssertEqual(notification?.channel, "test_channel1") 769 | XCTAssertNil(notification?.payload) 770 | XCTAssertNil(error) 771 | 772 | testExpectation.fulfill() 773 | stop = true 774 | } 775 | 776 | sleep(1) 777 | 778 | try conn2.notify(channel: "test_channel1", payload: nil) 779 | 780 | waitForExpectations(timeout: 5) 781 | } 782 | 783 | func testNotificationWithPayload() throws { 784 | let conn1 = try postgreSQL.makeConnection() 785 | let conn2 = try postgreSQL.makeConnection() 786 | 787 | let testExpectation = expectation(description: "Receive notification with payload") 788 | 789 | conn1.listen(toChannel: "test_channel2") { (notification, error, stop) in 790 | XCTAssertEqual(notification?.channel, "test_channel2") 791 | XCTAssertEqual(notification?.payload, "test_payload") 792 | XCTAssertNil(error) 793 | 794 | testExpectation.fulfill() 795 | stop = true 796 | } 797 | 798 | sleep(1) 799 | 800 | try conn2.notify(channel: "test_channel2", payload: "test_payload") 801 | 802 | waitForExpectations(timeout: 5) 803 | } 804 | 805 | func testQueryToNode() throws { 806 | let conn = try postgreSQL.makeConnection() 807 | 808 | let results = try conn.execute("SELECT version()") 809 | XCTAssertNotNil(results.array?[0].object?["version"]?.string) 810 | } 811 | 812 | func testEmptyQuery() throws { 813 | let conn = try postgreSQL.makeConnection() 814 | 815 | do { 816 | try conn.execute("") 817 | XCTFail("This query should not succeed") 818 | } 819 | catch PostgresSQLStatusError.emptyQuery { 820 | // Should end up here 821 | } 822 | catch { 823 | throw error 824 | } 825 | } 826 | 827 | func testInvalidQuery() throws { 828 | let conn = try postgreSQL.makeConnection() 829 | 830 | do { 831 | try conn.execute("SELECT * FROM nothing") 832 | XCTFail("This query should not succeed") 833 | } 834 | catch let error as PostgreSQLError { 835 | XCTAssertEqual(error.code, PostgreSQLError.Code.undefinedTable) 836 | } 837 | catch { 838 | throw error 839 | } 840 | } 841 | 842 | func testTransactionSuccess() throws { 843 | let conn = try postgreSQL.makeConnection() 844 | 845 | let isolationLevels: [Connection.TransactionIsolationLevel] = [ 846 | .readCommitted, 847 | .repeatableRead, 848 | .serializable, 849 | ] 850 | 851 | for isolationLevel in isolationLevels { 852 | try conn.execute("DROP TABLE IF EXISTS foo") 853 | try conn.execute("CREATE TABLE foo (bar INT, baz VARCHAR(16), bla BOOLEAN)") 854 | 855 | try conn.transaction(isolationLevel: isolationLevel) { 856 | try conn.execute("INSERT INTO foo VALUES (42, 'Life', true)") 857 | try conn.execute("INSERT INTO foo VALUES (1337, 'Elite', false)") 858 | try conn.execute("INSERT INTO foo VALUES (9, NULL, true)") 859 | } 860 | 861 | let resuls = try conn.execute("SELECT * FROM foo").array ?? [] 862 | XCTAssertEqual(resuls.count, 3) 863 | } 864 | } 865 | 866 | func testTransactionFailure() throws { 867 | let conn = try postgreSQL.makeConnection() 868 | 869 | enum TestError : Error { 870 | case failure 871 | } 872 | 873 | let isolationLevels: [Connection.TransactionIsolationLevel] = [ 874 | .readCommitted, 875 | .repeatableRead, 876 | .serializable, 877 | ] 878 | 879 | for isolationLevel in isolationLevels { 880 | try conn.execute("DROP TABLE IF EXISTS foo") 881 | try conn.execute("CREATE TABLE foo (bar INT, baz VARCHAR(16), bla BOOLEAN)") 882 | 883 | do { 884 | try conn.transaction(isolationLevel: isolationLevel) { 885 | try conn.execute("INSERT INTO foo VALUES (42, 'Life', true)") 886 | try conn.execute("INSERT INTO foo VALUES (1337, 'Elite', false)") 887 | try conn.execute("INSERT INTO foo VALUES (9, NULL, true)") 888 | 889 | throw TestError.failure 890 | } 891 | 892 | XCTFail("transaction should throw error") 893 | } 894 | catch TestError.failure { 895 | 896 | } 897 | catch { 898 | XCTFail("Should not fail with unknown error") 899 | } 900 | 901 | let resuls = try conn.execute("SELECT * FROM foo").array ?? [] 902 | XCTAssertEqual(resuls.count, 0) 903 | } 904 | } 905 | } 906 | -------------------------------------------------------------------------------- /Tests/PostgreSQLTests/Utilities.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import PostgreSQL 3 | import Foundation 4 | 5 | extension PostgreSQL.Database { 6 | static func makeTest() -> PostgreSQL.Database { 7 | do { 8 | let postgreSQL = try PostgreSQL.Database( 9 | hostname: "127.0.0.1", 10 | port: 5432, 11 | database: "test", 12 | user: "postgres", 13 | password: "" 14 | ) 15 | 16 | let connection = try postgreSQL.makeConnection() 17 | try connection.execute("SELECT version()") 18 | 19 | return postgreSQL 20 | } catch { 21 | print() 22 | print() 23 | print("⚠️ PostgreSQL Not Configured ⚠️") 24 | print() 25 | print("Error: \(error)") 26 | print() 27 | print("You must configure PostgreSQL to run with the following configuration: ") 28 | print(" user: 'postgres'") 29 | print(" password: '' // (empty)") 30 | print(" hostname: '127.0.0.1'") 31 | print(" database: 'test'") 32 | print() 33 | print() 34 | 35 | XCTFail("Configure PostgreSQL") 36 | fatalError("Configure PostgreSQL") 37 | } 38 | } 39 | } 40 | 41 | extension String { 42 | var hexStringBytes: [Int8] { 43 | guard let characters = cString(using: .utf8) else { 44 | return [] 45 | } 46 | 47 | var data: [Int8] = [] 48 | data.reserveCapacity(characters.count / 2) 49 | 50 | var byteChars: [CChar] = [0, 0, 0] 51 | for i in stride(from: 0, to: characters.count - 1, by: 2) { 52 | byteChars[0] = characters[i] 53 | byteChars[1] = characters[i+1] 54 | let byteValue = UInt8(strtol(byteChars, nil, 16)) 55 | 56 | guard byteValue != 0 || (byteChars[0] == 48 && byteChars[1] == 48) else { 57 | return [] 58 | } 59 | 60 | data.append(Int8(bitPattern: byteValue)) 61 | } 62 | 63 | return data 64 | } 65 | 66 | var postgreSQLParsedDate: Date { 67 | struct Formatter { 68 | static let `static`: DateFormatter = { 69 | let formatter = DateFormatter() 70 | formatter.timeZone = TimeZone(abbreviation: "UTC") 71 | formatter.dateFormat = "yyyy-MM-dd HH:mm:ss.SSS" 72 | return formatter 73 | }() 74 | } 75 | return Formatter.static.date(from: self)! 76 | } 77 | } 78 | 79 | extension Float32 { 80 | static let min = Float32(bitPattern: 0x00800000) 81 | static let max = Float32(bitPattern: 0x7f7fffff) 82 | } 83 | 84 | extension Float64 { 85 | static let min = Float64(bitPattern: 0x0010000000000000) 86 | static let max = Float64(bitPattern: 0x7fefffffffffffff) 87 | } 88 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | ignore: 3 | - "Tests" 4 | --------------------------------------------------------------------------------