diff --git a/.swift-version b/.swift-version index f2c6cb6..819e07a 100644 --- a/.swift-version +++ b/.swift-version @@ -1 +1 @@ -4.2.3 +5.0 diff --git a/.travis.yml b/.travis.yml index 7618094..2f82262 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,12 +27,17 @@ matrix: dist: xenial sudo: required services: docker - env: DOCKER_IMAGE=swift:4.2.3 DOCKER_PACKAGES="libpq-dev postgresql postgresql-contrib locales locales-all" CUSTOM_BUILD_SCRIPT=.build-docker + env: DOCKER_IMAGE=swift:4.2.4 DOCKER_PACKAGES="libpq-dev postgresql postgresql-contrib locales locales-all" CUSTOM_BUILD_SCRIPT=.build-docker SWIFT_SNAPSHOT=4.2.4 - os: linux dist: xenial sudo: required services: docker - env: DOCKER_IMAGE=swift:4.2.3 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT DOCKER_PACKAGES="libpq-dev postgresql postgresql-contrib locales locales-all" CUSTOM_BUILD_SCRIPT=.build-docker + env: DOCKER_IMAGE=swift:5.0-xenial DOCKER_PACKAGES="libpq-dev postgresql postgresql-contrib locales locales-all" CUSTOM_BUILD_SCRIPT=.build-docker + - os: linux + dist: xenial + sudo: required + services: docker + env: DOCKER_IMAGE=swift:5.0 SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT DOCKER_PACKAGES="libpq-dev postgresql postgresql-contrib locales locales-all" CUSTOM_BUILD_SCRIPT=.build-docker - os: osx osx_image: xcode9.2 sudo: required @@ -44,8 +49,12 @@ matrix: - os: osx osx_image: xcode10.1 sudo: required + env: SWIFT_SNAPSHOT=4.2.1 - os: osx - osx_image: xcode10.1 + osx_image: xcode10.2 + sudo: required + - os: osx + osx_image: xcode10.2 sudo: required env: SWIFT_SNAPSHOT=$SWIFT_DEVELOPMENT_SNAPSHOT diff --git a/Package.swift b/Package.swift index fac410f..6cc1c9c 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,8 @@ -// swift-tools-version:4.2 +// swift-tools-version:5.0 // The swift-tools-version declares the minimum version of Swift required to build this package. /** - * Copyright IBM Corporation 2016, 2018 + * Copyright IBM Corporation 2016, 2018, 2019 * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Package@swift-4.2.swift b/Package@swift-4.2.swift new file mode 100644 index 0000000..f330d0e --- /dev/null +++ b/Package@swift-4.2.swift @@ -0,0 +1,53 @@ +// swift-tools-version:4.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +/** + * Copyright IBM Corporation 2016, 2018, 2019 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + **/ + +import PackageDescription + +let package = Package( + name: "SwiftKueryPostgreSQL", + products: [ + .library( + name: "SwiftKueryPostgreSQL", + targets: ["SwiftKueryPostgreSQL"] + ) + ], + dependencies: [ + .package(url: "https://github.com/IBM-Swift/Swift-Kuery.git", from: "3.0.0"), + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .systemLibrary( + name: "CLibpq", + pkgConfig: "libpq", + providers: [ + .brew(["postgresql"]), + .apt(["libpq-dev"]) + ] + ), + .target( + name: "SwiftKueryPostgreSQL", + dependencies: ["SwiftKuery", "CLibpq"] + ), + .testTarget( + name: "SwiftKueryPostgreSQLTests", + dependencies: ["SwiftKueryPostgreSQL"] + ) + ] +) diff --git a/Sources/SwiftKueryPostgreSQL/PostgreSQLConnection.swift b/Sources/SwiftKueryPostgreSQL/PostgreSQLConnection.swift index b6891ca..cf651fe 100644 --- a/Sources/SwiftKueryPostgreSQL/PostgreSQLConnection.swift +++ b/Sources/SwiftKueryPostgreSQL/PostgreSQLConnection.swift @@ -482,7 +482,15 @@ public class PostgreSQLConnection: Connection { runCompletionHandler(.successNoData, onCompletion: onCompletion) } else if status == PGRES_SINGLE_TUPLE { - PostgreSQLResultFetcher.create(queryResult: result, connection: self) { resultFetcher in + PostgreSQLResultFetcher.create(queryResult: result, connection: self) { resultFetcher, error in + guard let resultFetcher = resultFetcher else { + guard let error = error else { + clearResult(result, connection: self) + return runCompletionHandler(.error(QueryError.databaseError("Unknown error creating ResultFetcher")), onCompletion: onCompletion) + } + clearResult(result, connection: self) + return runCompletionHandler(.error(error), onCompletion: onCompletion) + } self.setState(.fetchingResultSet) self.currentResultFetcher = resultFetcher runCompletionHandler(.resultSet(ResultSet(resultFetcher, connection: self)), onCompletion: onCompletion) diff --git a/Sources/SwiftKueryPostgreSQL/PostgreSQLResultFetcher.swift b/Sources/SwiftKueryPostgreSQL/PostgreSQLResultFetcher.swift index 17a3269..42ccb28 100644 --- a/Sources/SwiftKueryPostgreSQL/PostgreSQLResultFetcher.swift +++ b/Sources/SwiftKueryPostgreSQL/PostgreSQLResultFetcher.swift @@ -57,7 +57,11 @@ public class PostgreSQLResultFetcher: ResultFetcher { clearResult(queryResult, connection: self.connection) return callback((nil, QueryError.noResult("Error retrieving result from database"))) } - return callback((self.buildRow(queryResult: queryResult), nil)) + let builtRow = self.buildRow(queryResult: queryResult) + guard let row = builtRow.row else { + return callback((nil, builtRow.error)) + } + return callback((row, nil)) } } @@ -68,19 +72,26 @@ public class PostgreSQLResultFetcher: ResultFetcher { callback((self.titles, nil)) } - private func buildRow(queryResult: OpaquePointer) -> [Any?] { + private func buildRow(queryResult: OpaquePointer) -> (row: [Any?]?, error: Error?) { let columns = PQnfields(queryResult) + defer { + PQclear(queryResult) + } var row = [Any?]() for column in 0 ..< columns { if PQgetisnull(queryResult, 0, column) == 1 { row.append(nil) } else { - row.append(PostgreSQLResultFetcher.convert(queryResult, row: 0, column: column)) + let convertedValue = PostgreSQLResultFetcher.convert(queryResult, row: 0, column: column) + guard let value = convertedValue.value else { + return (nil, convertedValue.error) + } + + row.append(value) } } - PQclear(queryResult) - return row + return (row, nil) } /// Indicate no further calls will be made to this ResultFetcher allowing the connection in use to be released. @@ -89,7 +100,7 @@ public class PostgreSQLResultFetcher: ResultFetcher { clearResult(nil, connection: connection) } - private static func convert(_ queryResult: OpaquePointer, row: Int32, column: Int32) -> Any { + private static func convert(_ queryResult: OpaquePointer, row: Int32, column: Int32) -> (value: Any?, error: Error?) { let value = PQgetvalue(queryResult, row, column) let count = Int(PQgetlength(queryResult, row, column)) let data = Data(bytes: value!, count: count) @@ -97,11 +108,11 @@ public class PostgreSQLResultFetcher: ResultFetcher { let type = PostgreSQLType(rawValue: PQftype(queryResult, column)) if PQfformat(queryResult, column) == 0 { - return String(data: data, encoding: String.Encoding.utf8) ?? "" + return (String(data: data, encoding: String.Encoding.utf8) ?? "", nil) } else { guard let type = type, let value = value else { - return data + return (data, nil) } switch type { @@ -118,23 +129,40 @@ public class PostgreSQLResultFetcher: ResultFetcher { case .json: fallthrough case .xml: - return String(cString: value) + return (String(cString: value), nil) case .int2: - return PostgreSQLResultFetcher.int16NetworkToHost(from: value) + return (PostgreSQLResultFetcher.int16NetworkToHost(from: value), nil) case .int4: - return Int32(bigEndian: value.withMemoryRebound(to: Int32.self, capacity: 1) { $0.pointee }) + return (Int32(bigEndian: value.withMemoryRebound(to: Int32.self, capacity: 1) { $0.pointee }), nil) case .int8: - return Int64(bigEndian: value.withMemoryRebound(to: Int64.self, capacity: 1) { $0.pointee }) + return (Int64(bigEndian: value.withMemoryRebound(to: Int64.self, capacity: 1) { $0.pointee }), nil) case .float4: - return Float32(bitPattern: UInt32(bigEndian: data.withUnsafeBytes { $0.pointee } )) + #if swift(>=5) + let uintValue = data.withUnsafeBytes( { (address: UnsafeRawBufferPointer) in (address.baseAddress?.assumingMemoryBound(to: UInt32.self).pointee) } ) + guard let bits = uintValue else { + return(nil, QueryError.databaseError("Unable to convert value to Float32 while reading result")) + } + return (Float32(bitPattern: UInt32(bigEndian: bits)), nil) + #else + return (Float32(bitPattern: UInt32(bigEndian: data.withUnsafeBytes { $0.pointee } )), nil) + #endif case .float8: - return Float64(bitPattern: UInt64(bigEndian: data.withUnsafeBytes { $0.pointee } )) - + + #if swift(>=5) + let uintValue = data.withUnsafeBytes( { (address: UnsafeRawBufferPointer) in (address.baseAddress?.assumingMemoryBound(to: UInt64.self).pointee) } ) + guard let bits = uintValue else { + return(nil, QueryError.databaseError("Unable to convert value to Float64 while reading result")) + } + return (Float64(bitPattern: UInt64(bigEndian: bits)), nil) + #else + return (Float64(bitPattern: UInt64(bigEndian: data.withUnsafeBytes { $0.pointee } )), nil) + #endif + case .numeric: // Numeric is a sequence of Int16's: number of digits, weight, sign, display scale, numeric digits. // The numeric digits are stored in the form of a series of 16 bit base-10000 numbers each representing @@ -148,12 +176,12 @@ public class PostgreSQLResultFetcher: ResultFetcher { // https://www.postgresql.org/message-id/491DC5F3D279CD4EB4B157DDD62237F404E27FE9@zipwire.esri.com let sign = PostgreSQLResultFetcher.int16NetworkToHost(from: value.advanced(by: 4)) if sign == -16384 { // 0xC000 - return "NaN" + return ("NaN", nil) } let numberOfDigits = PostgreSQLResultFetcher.int16NetworkToHost(from: value) if numberOfDigits <= 0 { - return "0" + return ("0", nil) } var result: String = "" @@ -213,12 +241,12 @@ public class PostgreSQLResultFetcher: ResultFetcher { result = "-" + result } - return result + return (result, nil) case .date: let days = Int32(bigEndian: value.withMemoryRebound(to: Int32.self, capacity: 1) { $0.pointee }) let timeInterval = TimeInterval(days * secondsInDay) - return Date(timeIntervalSince1970: timeInterval + timeIntervalBetween1970AndPostgresReferenceDate) + return (Date(timeIntervalSince1970: timeInterval + timeIntervalBetween1970AndPostgresReferenceDate), nil) case .time: fallthrough @@ -229,14 +257,14 @@ public class PostgreSQLResultFetcher: ResultFetcher { case .timestamptz: let microseconds = Int64(bigEndian: value.withMemoryRebound(to: Int64.self, capacity: 1) { $0.pointee }) let timeInterval = TimeInterval(microseconds / 1000000) - return Date(timeIntervalSince1970: timeInterval + timeIntervalBetween1970AndPostgresReferenceDate) + return (Date(timeIntervalSince1970: timeInterval + timeIntervalBetween1970AndPostgresReferenceDate), nil) case .bool: - return Bool(value.withMemoryRebound(to: Bool.self, capacity: 1) { $0.pointee }) - + return (Bool(value.withMemoryRebound(to: Bool.self, capacity: 1) { $0.pointee }), nil) + case .uuid: let uuid = UUID(uuid: uuid_t(value.withMemoryRebound(to: uuid_t.self, capacity: 1) { $0.pointee })) - return uuid.uuidString + return (uuid.uuidString, nil) } } } @@ -251,7 +279,7 @@ public class PostgreSQLResultFetcher: ResultFetcher { // Static factory method to create an instance of PostgreSQLQueryBuilder // This exists to allow the column names from the returned result to be retrieved while we have access to a result set as part of an asynchronous callback chain. - internal static func create(queryResult: OpaquePointer, connection: PostgreSQLConnection, callback: (PostgreSQLResultFetcher) ->()) { + internal static func create(queryResult: OpaquePointer, connection: PostgreSQLConnection, callback: ((PostgreSQLResultFetcher?, Error?)) ->()) { let resultFetcher = PostgreSQLResultFetcher(connection: connection) let columns = PQnfields(queryResult) var columnNames = [String]() @@ -259,8 +287,12 @@ public class PostgreSQLResultFetcher: ResultFetcher { columnNames.append(String(validatingUTF8: PQfname(queryResult, column))!) } resultFetcher.titles = columnNames - resultFetcher.row = resultFetcher.buildRow(queryResult: queryResult) - callback(resultFetcher) + let rowResult = resultFetcher.buildRow(queryResult: queryResult) + guard let row = rowResult.row else { + return callback((nil, rowResult.error)) + } + resultFetcher.row = row + callback((resultFetcher, nil)) } }