Skip to content

Commit

Permalink
Add proper support for Decimal (#194)
Browse files Browse the repository at this point in the history
* Use `PostgresNumeric` for `Decimal` instead of String
* Make `Decimal` conform to `PSQLCodable`
* Fix support for text decimals
* Add integration test for decimal string serialization
* Test inserting decimal to text column

Co-authored-by: Gwynne Raskind <gwynne@darkrainfall.org>
  • Loading branch information
madsodgaard and gwynne authored Nov 24, 2021
1 parent f91f23d commit 2c49bee
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 8 deletions.
4 changes: 2 additions & 2 deletions Sources/PostgresNIO/Data/PostgresData+Decimal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ extension PostgresData {

extension Decimal: PostgresDataConvertible {
public static var postgresDataType: PostgresDataType {
return String.postgresDataType
return .numeric
}

public init?(postgresData: PostgresData) {
Expand All @@ -29,6 +29,6 @@ extension Decimal: PostgresDataConvertible {
}

public var postgresData: PostgresData? {
return .init(decimal: self)
return .init(numeric: PostgresNumeric(decimal: self))
}
}
39 changes: 39 additions & 0 deletions Sources/PostgresNIO/New/Data/Decimal+PSQLCodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import NIOCore
import struct Foundation.Decimal

extension Decimal: PSQLCodable {
var psqlType: PSQLDataType {
.numeric
}

var psqlFormat: PSQLFormat {
.binary
}

static func decode(from byteBuffer: inout ByteBuffer, type: PSQLDataType, format: PSQLFormat, context: PSQLDecodingContext) throws -> Decimal {
switch (format, type) {
case (.binary, .numeric):
guard let numeric = PostgresNumeric(buffer: &byteBuffer) else {
throw PSQLCastingError.failure(targetType: Self.self, type: type, postgresData: byteBuffer, context: context)
}
return numeric.decimal
case (.text, .numeric):
guard let string = byteBuffer.readString(length: byteBuffer.readableBytes), let value = Decimal(string: string) else {
throw PSQLCastingError.failure(targetType: Self.self, type: type, postgresData: byteBuffer, context: context)
}
return value
default:
throw PSQLCastingError.failure(targetType: Self.self, type: type, postgresData: byteBuffer, context: context)
}
}

func encode(into byteBuffer: inout ByteBuffer, context: PSQLEncodingContext) {
let numeric = PostgresNumeric(decimal: self)
byteBuffer.writeInteger(numeric.ndigits)
byteBuffer.writeInteger(numeric.weight)
byteBuffer.writeInteger(numeric.sign)
byteBuffer.writeInteger(numeric.dscale)
var value = numeric.value
byteBuffer.writeBuffer(&value)
}
}
25 changes: 25 additions & 0 deletions Tests/IntegrationTests/PSQLIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,31 @@ final class IntegrationTests: XCTestCase {
XCTAssertEqual(try row?.decode(column: "timestamptz", as: Date.self).description, "2016-01-18 00:20:03 +0000")
}

func testDecodeDecimals() {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
let eventLoop = eventLoopGroup.next()

var conn: PSQLConnection?
XCTAssertNoThrow(conn = try PSQLConnection.test(on: eventLoop).wait())
defer { XCTAssertNoThrow(try conn?.close().wait()) }

var stream: PSQLRowStream?
XCTAssertNoThrow(stream = try conn?.query("""
SELECT
$1::numeric as numeric,
$2::numeric as numeric_negative
""", [Decimal(string: "123456.789123")!, Decimal(string: "-123456.789123")!], logger: .psqlTest).wait())

var rows: [PSQLRow]?
XCTAssertNoThrow(rows = try stream?.all().wait())
XCTAssertEqual(rows?.count, 1)
let row = rows?.first

XCTAssertEqual(try row?.decode(column: "numeric", as: Decimal.self), Decimal(string: "123456.789123")!)
XCTAssertEqual(try row?.decode(column: "numeric_negative", as: Decimal.self), Decimal(string: "-123456.789123")!)
}

func testDecodeUUID() {
let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
defer { XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) }
Expand Down
36 changes: 30 additions & 6 deletions Tests/IntegrationTests/PostgresNIOTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -466,17 +466,41 @@ final class PostgresNIOTests: XCTestCase {
var rows: PostgresQueryResult?
XCTAssertNoThrow(rows = try conn?.query("""
select
$1::numeric::text as a,
$2::numeric::text as b,
$3::numeric::text as c
$1::numeric as a,
$2::numeric as b,
$3::numeric as c
""", [
.init(numeric: a),
.init(numeric: b),
.init(numeric: c)
]).wait())
XCTAssertEqual(rows?.first?.column("a")?.string, "123456.789123")
XCTAssertEqual(rows?.first?.column("b")?.string, "-123456.789123")
XCTAssertEqual(rows?.first?.column("c")?.string, "3.14159265358979")
XCTAssertEqual(rows?.first?.column("a")?.decimal, Decimal(string: "123456.789123")!)
XCTAssertEqual(rows?.first?.column("b")?.decimal, Decimal(string: "-123456.789123")!)
XCTAssertEqual(rows?.first?.column("c")?.decimal, Decimal(string: "3.14159265358979")!)
}

func testDecimalStringSerialization() {
var conn: PostgresConnection?
XCTAssertNoThrow(conn = try PostgresConnection.test(on: eventLoop).wait())
defer { XCTAssertNoThrow( try conn?.close().wait() ) }

XCTAssertNoThrow(_ = try conn?.simpleQuery("DROP TABLE IF EXISTS \"table1\"").wait())
XCTAssertNoThrow(_ = try conn?.simpleQuery("""
CREATE TABLE table1 (
"balance" text NOT NULL
);
""").wait())
defer { XCTAssertNoThrow(_ = try conn?.simpleQuery("DROP TABLE \"table1\"").wait()) }

XCTAssertNoThrow(_ = try conn?.query("INSERT INTO table1 VALUES ($1)", [.init(decimal: Decimal(string: "123456.789123")!)]).wait())

var rows: PostgresQueryResult?
XCTAssertNoThrow(rows = try conn?.query("""
SELECT
"balance"
FROM table1
""").wait())
XCTAssertEqual(rows?.first?.column("balance")?.decimal, Decimal(string: "123456.789123")!)
}

func testMoney() {
Expand Down
32 changes: 32 additions & 0 deletions Tests/PostgresNIOTests/New/Data/Decimal+PSQLCodableTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import XCTest
import NIOCore
@testable import PostgresNIO

class Decimal_PSQLCodableTests: XCTestCase {

func testRoundTrip() {
let values: [Decimal] = [1.1, .pi, -5e-12]

for value in values {
var buffer = ByteBuffer()
value.encode(into: &buffer, context: .forTests())
XCTAssertEqual(value.psqlType, .numeric)
let data = PSQLData(bytes: buffer, dataType: .numeric, format: .binary)

var result: Decimal?
XCTAssertNoThrow(result = try data.decode(as: Decimal.self, context: .forTests()))
XCTAssertEqual(value, result)
}
}

func testDecodeFailureInvalidType() {
var buffer = ByteBuffer()
buffer.writeInteger(Int64(0))
let data = PSQLData(bytes: buffer, dataType: .int8, format: .binary)

XCTAssertThrowsError(try data.decode(as: Decimal.self, context: .forTests())) { error in
XCTAssert(error is PSQLCastingError)
}
}

}

0 comments on commit 2c49bee

Please sign in to comment.