diff --git a/README.md b/README.md index 412399e6..a9f16691 100644 --- a/README.md +++ b/README.md @@ -198,17 +198,15 @@ let data: PostgresData= ... print(data.string) // String? +// Postgres only supports signed Ints. print(data.int) // Int? -print(data.int8) // Int8? print(data.int16) // Int16? print(data.int32) // Int32? print(data.int64) // Int64? -print(data.uint) // UInt? +// 'char' can be interpreted as a UInt8. +// It will show in db as a character though. print(data.uint8) // UInt8? -print(data.uint16) // UInt16? -print(data.uint32) // UInt32? -print(data.uint64) // UInt64? print(data.bool) // Bool? diff --git a/Sources/PostgresNIO/Data/PostgresData+Int.swift b/Sources/PostgresNIO/Data/PostgresData+Int.swift index a1e95c3e..ce77dd43 100644 --- a/Sources/PostgresNIO/Data/PostgresData+Int.swift +++ b/Sources/PostgresNIO/Data/PostgresData+Int.swift @@ -1,117 +1,157 @@ extension PostgresData { public init(int value: Int) { - self.init(fwi: value) + assert(Int.bitWidth == 64) + self.init(type: .int8, value: .init(integer: value)) } - - public init(int8 value: Int8) { - self.init(fwi: value) + + public init(uint8 value: UInt8) { + self.init(type: .char, value: .init(integer: value)) } public init(int16 value: Int16) { - self.init(fwi: value) + self.init(type: .int2, value: .init(integer: value)) } public init(int32 value: Int32) { - self.init(fwi: value) + self.init(type: .int4, value: .init(integer: value)) } public init(int64 value: Int64) { - self.init(fwi: value) - } - - public init(uint value: UInt) { - self.init(fwi: value) - } - - public init(uint8 value: UInt8) { - self.init(fwi: value) - } - - public init(uint16 value: UInt16) { - self.init(fwi: value) - } - - public init(uint32 value: UInt32) { - self.init(fwi: value) - } - - public init(uint64 value: UInt64) { - self.init(fwi: value) + self.init(type: .int8, value: .init(integer: value)) } public var int: Int? { - return fwi() + guard var value = self.value else { + return nil + } + + switch self.formatCode { + case .binary: + switch self.type { + case .char, .bpchar: + guard value.readableBytes == 1 else { + return nil + } + return value.readInteger(as: UInt8.self) + .flatMap(Int.init) + case .int2: + assert(value.readableBytes == 2) + return value.readInteger(as: Int16.self) + .flatMap(Int.init) + case .int4, .regproc: + assert(value.readableBytes == 4) + return value.readInteger(as: Int32.self) + .flatMap(Int.init) + case .oid: + assert(value.readableBytes == 4) + assert(Int.bitWidth == 64) // or else overflow is possible + return value.readInteger(as: UInt32.self) + .flatMap(Int.init) + case .int8: + assert(value.readableBytes == 8) + assert(Int.bitWidth == 64) + return value.readInteger(as: Int.self) + default: + return nil + } + case .text: + guard let string = self.string else { + return nil + } + return Int(string) + } } - - public var int8: Int8? { - return fwi() + + public var uint8: UInt8? { + guard var value = self.value else { + return nil + } + + switch self.formatCode { + case .binary: + switch self.type { + case .char, .bpchar: + guard value.readableBytes == 1 else { + return nil + } + return value.readInteger(as: UInt8.self) + default: + return nil + } + case .text: + guard let string = self.string else { + return nil + } + return UInt8(string) + } } public var int16: Int16? { - return fwi() + guard var value = self.value else { + return nil + } + + switch self.formatCode { + case .binary: + switch self.type { + case .char, .bpchar: + guard value.readableBytes == 1 else { + return nil + } + return value.readInteger(as: UInt8.self) + .flatMap(Int16.init) + case .int2: + assert(value.readableBytes == 2) + return value.readInteger(as: Int16.self) + default: + return nil + } + case .text: + guard let string = self.string else { + return nil + } + return Int16(string) + } } public var int32: Int32? { - return fwi() - } - - public var int64: Int64? { - return fwi() - } - - public var uint: UInt? { - return fwi() - } - - public var uint8: UInt8? { - return fwi() - } - - public var uint16: UInt16? { - return fwi() - } - - public var uint32: UInt32? { - return fwi() - } - - public var uint64: UInt64? { - return fwi() - } -} + guard var value = self.value else { + return nil + } -private extension PostgresData { - init(fwi: I) where I: FixedWidthInteger { - let capacity: Int - let type: PostgresDataType - switch I.bitWidth { - case 8: - capacity = 1 - type = .char - case 16: - capacity = 2 - type = .int2 - case 32: - capacity = 3 - type = .int4 - case 64: - capacity = 4 - type = .int8 - default: - fatalError("Cannot encode \(I.self) to PostgresData") + switch self.formatCode { + case .binary: + switch self.type { + case .char, .bpchar: + guard value.readableBytes == 1 else { + return nil + } + return value.readInteger(as: UInt8.self) + .flatMap(Int32.init) + case .int2: + assert(value.readableBytes == 2) + return value.readInteger(as: Int16.self) + .flatMap(Int32.init) + case .int4, .regproc: + assert(value.readableBytes == 4) + return value.readInteger(as: Int32.self) + .flatMap(Int32.init) + default: + return nil + } + case .text: + guard let string = self.string else { + return nil + } + return Int32(string) } - var buffer = ByteBufferAllocator().buffer(capacity: capacity) - buffer.writeInteger(fwi) - self.init(type: type, formatCode: .binary, value: buffer) } - func fwi(_ type: I.Type = I.self) -> I? - where I: FixedWidthInteger - { + public var int64: Int64? { guard var value = self.value else { return nil } - + switch self.formatCode { case .binary: switch self.type { @@ -119,34 +159,25 @@ private extension PostgresData { guard value.readableBytes == 1 else { return nil } - guard let uint8 = value.getInteger(at: value.readerIndex, as: UInt8.self) else { - return nil - } - return I(uint8) + return value.readInteger(as: UInt8.self) + .flatMap(Int64.init) case .int2: assert(value.readableBytes == 2) - guard let int16 = value.readInteger(as: Int16.self) else { - return nil - } - return I(int16) + return value.readInteger(as: Int16.self) + .flatMap(Int64.init) case .int4, .regproc: assert(value.readableBytes == 4) - guard let int32 = value.getInteger(at: value.readerIndex, as: Int32.self) else { - return nil - } - return I(int32) + return value.readInteger(as: Int32.self) + .flatMap(Int64.init) case .oid: assert(value.readableBytes == 4) - guard let uint32 = value.getInteger(at: value.readerIndex, as: UInt32.self) else { - return nil - } - return I(uint32) + assert(Int.bitWidth == 64) // or else overflow is possible + return value.readInteger(as: UInt32.self) + .flatMap(Int64.init) case .int8: assert(value.readableBytes == 8) - guard let int64 = value.getInteger(at: value.readerIndex, as: Int64.self) else { - return nil - } - return I(int64) + assert(Int.bitWidth == 64) + return value.readInteger(as: Int64.self) default: return nil } @@ -154,52 +185,82 @@ private extension PostgresData { guard let string = self.string else { return nil } - return I(string) + return Int64(string) + } + } +} + +extension Int: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int8 } + + public init?(postgresData: PostgresData) { + guard let int = postgresData.int else { + return nil + } + self = int + } + + public var postgresData: PostgresData? { + .init(int: self) + } +} + +extension UInt8: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .char } + + public init?(postgresData: PostgresData) { + guard let uint8 = postgresData.uint8 else { + return nil } + self = uint8 + } + + public var postgresData: PostgresData? { + .init(uint8: self) } } -extension FixedWidthInteger { - public static var postgresDataType: PostgresDataType { - switch self.bitWidth { - case 8: - return .char - case 16: - return .int2 - case 32: - return .int4 - case 64: - return .int8 - default: - fatalError("\(self.bitWidth) not supported") +extension Int16: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int2 } + + public init?(postgresData: PostgresData) { + guard let int16 = postgresData.int16 else { + return nil } + self = int16 } public var postgresData: PostgresData? { - return .init(fwi: self) + .init(int16: self) } +} + +extension Int32: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int4 } public init?(postgresData: PostgresData) { - guard let fwi = postgresData.fwi(Self.self) else { + guard let int32 = postgresData.int32 else { return nil } - self = fwi + self = int32 + } + + public var postgresData: PostgresData? { + .init(int32: self) } } -extension Int: PostgresDataConvertible { } -extension Int8: PostgresDataConvertible { } -extension Int16: PostgresDataConvertible { } -extension Int32: PostgresDataConvertible { } -extension Int64: PostgresDataConvertible { } -extension UInt: PostgresDataConvertible { } -extension UInt8: PostgresDataConvertible { } -extension UInt16: PostgresDataConvertible { } -extension UInt32: PostgresDataConvertible { } -extension UInt64: PostgresDataConvertible { } - -extension PostgresData: ExpressibleByIntegerLiteral { - public init(integerLiteral value: Int) { - self.init(int: value) +extension Int64: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int8 } + + public init?(postgresData: PostgresData) { + guard let int64 = postgresData.int64 else { + return nil + } + self = int64 + } + + public var postgresData: PostgresData? { + .init(int64: self) } } diff --git a/Sources/PostgresNIO/Deprecated/PostgresData+UInt.swift b/Sources/PostgresNIO/Deprecated/PostgresData+UInt.swift new file mode 100644 index 00000000..ab3e493f --- /dev/null +++ b/Sources/PostgresNIO/Deprecated/PostgresData+UInt.swift @@ -0,0 +1,162 @@ +private func warn( + _ old: Any.Type, mustBeConvertedTo new: Any.Type, + file: StaticString = #file, line: UInt = #line +) { + assertionFailure(""" + Integer conversion unsafe. + Postgres does not support storing \(old) natively. + + To bypass this assertion, compile in release mode. + + swift build -c release + + Unsigned integers were previously allowed by PostgresNIO + but may cause overflow. To avoid overflow errors, update + your code to use \(new) instead. + + See https://github.com/vapor/postgres-nio/pull/120 + + """, file: file, line: line) +} + +extension PostgresData { + @available(*, deprecated, renamed: "init(int:)") + public init(uint value: UInt) { + warn(UInt.self, mustBeConvertedTo: Int.self) + self.init(int: .init(bitPattern: value)) + } + + @available(*, deprecated, renamed: "init(uint8:)") + public init(int8 value: Int8) { + warn(Int8.self, mustBeConvertedTo: UInt8.self) + self.init(uint8: .init(bitPattern: value)) + } + + @available(*, deprecated, renamed: "init(int16:)") + public init(uint16 value: UInt16) { + warn(UInt16.self, mustBeConvertedTo: Int16.self) + self.init(int16: .init(bitPattern: value)) + } + + @available(*, deprecated, renamed: "init(int32:)") + public init(uint32 value: UInt32) { + warn(UInt32.self, mustBeConvertedTo: Int32.self) + self.init(int32: .init(bitPattern: value)) + } + + @available(*, deprecated, renamed: "init(int64:)") + public init(uint64 value: UInt64) { + warn(UInt64.self, mustBeConvertedTo: Int64.self) + self.init(int64: .init(bitPattern: value)) + } + + @available(*, deprecated, renamed: "int") + public var uint: UInt? { + warn(UInt.self, mustBeConvertedTo: Int.self) + return self.int.flatMap { .init(bitPattern: $0) } + } + + @available(*, deprecated, renamed: "uint8") + public var int8: Int8? { + warn(Int8.self, mustBeConvertedTo: UInt8.self) + return self.uint8.flatMap { .init(bitPattern: $0) } + } + + @available(*, deprecated, renamed: "int16") + public var uint16: UInt16? { + warn(UInt16.self, mustBeConvertedTo: Int16.self) + return self.int16.flatMap { .init(bitPattern: $0) } + } + + @available(*, deprecated, renamed: "int32") + public var uint32: UInt32? { + warn(UInt32.self, mustBeConvertedTo: Int32.self) + return self.int32.flatMap { .init(bitPattern: $0) } + } + + @available(*, deprecated, renamed: "int64") + public var uint64: UInt64? { + warn(UInt64.self, mustBeConvertedTo: Int64.self) + return self.int64.flatMap { .init(bitPattern: $0) } + } +} + +@available(*, deprecated, message: "Use 'Int' instead.") +extension UInt: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int8 } + + public init?(postgresData: PostgresData) { + guard let uint = postgresData.uint else { + return nil + } + self = uint + } + + public var postgresData: PostgresData? { + .init(uint: self) + } +} + +@available(*, deprecated, message: "Use 'UInt8' instead.") +extension Int8: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .char } + + public init?(postgresData: PostgresData) { + guard let int8 = postgresData.int8 else { + return nil + } + self = int8 + } + + public var postgresData: PostgresData? { + .init(int8: self) + } +} + +@available(*, deprecated, message: "Use 'Int16' instead.") +extension UInt16: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int2 } + + public init?(postgresData: PostgresData) { + guard let uint16 = postgresData.uint16 else { + return nil + } + self = uint16 + } + + public var postgresData: PostgresData? { + .init(uint16: self) + } +} + +@available(*, deprecated, message: "Use 'Int32' instead.") +extension UInt32: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int4 } + + public init?(postgresData: PostgresData) { + guard let uint32 = postgresData.uint32 else { + return nil + } + self = uint32 + } + + public var postgresData: PostgresData? { + .init(uint32: self) + } +} + +@available(*, deprecated, message: "Use 'Int64' instead.") +extension UInt64: PostgresDataConvertible { + public static var postgresDataType: PostgresDataType { .int8 } + + public init?(postgresData: PostgresData) { + guard let uint64 = postgresData.uint64 else { + return nil + } + self = uint64 + } + + public var postgresData: PostgresData? { + .init(uint64: self) + } +} diff --git a/Tests/PostgresNIOTests/PostgresNIOTests.swift b/Tests/PostgresNIOTests/PostgresNIOTests.swift index 424602f4..59601294 100644 --- a/Tests/PostgresNIOTests/PostgresNIOTests.swift +++ b/Tests/PostgresNIOTests/PostgresNIOTests.swift @@ -860,10 +860,10 @@ final class PostgresNIOTests: XCTestCase { '5'::char(2) as two """).wait() XCTAssertEqual(rows[0].column("one")?.uint8, 53) - XCTAssertEqual(rows[0].column("one")?.uint16, 53) + XCTAssertEqual(rows[0].column("one")?.int16, 53) XCTAssertEqual(rows[0].column("one")?.string, "5") XCTAssertEqual(rows[0].column("two")?.uint8, nil) - XCTAssertEqual(rows[0].column("two")?.uint16, nil) + XCTAssertEqual(rows[0].column("two")?.int16, nil) XCTAssertEqual(rows[0].column("two")?.string, "5 ") } @@ -937,6 +937,55 @@ final class PostgresNIOTests: XCTestCase { _ = try conn.simpleQuery("SET TIME ZONE INTERVAL '+5:45' HOUR TO MINUTE").wait() _ = try conn.query("SET TIME ZONE INTERVAL '+5:45' HOUR TO MINUTE").wait() } + + func testIntegerConversions() throws { + let conn = try PostgresConnection.test(on: eventLoop).wait() + defer { try! conn.close().wait() } + let rows = try conn.query(""" + select + 'a'::char as test8, + + '-32768'::smallint as min16, + '32767'::smallint as max16, + + '-2147483648'::integer as min32, + '2147483647'::integer as max32, + + '-9223372036854775808'::bigint as min64, + '9223372036854775807'::bigint as max64 + """).wait() + XCTAssertEqual(rows[0].column("test8")?.uint8, 97) + XCTAssertEqual(rows[0].column("test8")?.int16, 97) + XCTAssertEqual(rows[0].column("test8")?.int32, 97) + XCTAssertEqual(rows[0].column("test8")?.int64, 97) + + XCTAssertEqual(rows[0].column("min16")?.uint8, nil) + XCTAssertEqual(rows[0].column("max16")?.uint8, nil) + XCTAssertEqual(rows[0].column("min16")?.int16, .min) + XCTAssertEqual(rows[0].column("max16")?.int16, .max) + XCTAssertEqual(rows[0].column("min16")?.int32, -32768) + XCTAssertEqual(rows[0].column("max16")?.int32, 32767) + XCTAssertEqual(rows[0].column("min16")?.int64, -32768) + XCTAssertEqual(rows[0].column("max16")?.int64, 32767) + + XCTAssertEqual(rows[0].column("min32")?.uint8, nil) + XCTAssertEqual(rows[0].column("max32")?.uint8, nil) + XCTAssertEqual(rows[0].column("min32")?.int16, nil) + XCTAssertEqual(rows[0].column("max32")?.int16, nil) + XCTAssertEqual(rows[0].column("min32")?.int32, .min) + XCTAssertEqual(rows[0].column("max32")?.int32, .max) + XCTAssertEqual(rows[0].column("min32")?.int64, -2147483648) + XCTAssertEqual(rows[0].column("max32")?.int64, 2147483647) + + XCTAssertEqual(rows[0].column("min64")?.uint8, nil) + XCTAssertEqual(rows[0].column("max64")?.uint8, nil) + XCTAssertEqual(rows[0].column("min64")?.int16, nil) + XCTAssertEqual(rows[0].column("max64")?.int16, nil) + XCTAssertEqual(rows[0].column("min64")?.int32, nil) + XCTAssertEqual(rows[0].column("max64")?.int32, nil) + XCTAssertEqual(rows[0].column("min64")?.int64, .min) + XCTAssertEqual(rows[0].column("max64")?.int64, .max) + } } func env(_ name: String) -> String? {