diff --git a/CHANGELOG.md b/CHANGELOG.md index a708a72b..f4c84997 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Decoder for type `numeric` / `decimal`, [#7](https://github.com/isoos/postgresql-dart/pull/7). + ## 2.3.2 - Expose `ColumnDescription.typeId`. diff --git a/lib/src/binary_codec.dart b/lib/src/binary_codec.dart index ebca69d1..a5b3912f 100644 --- a/lib/src/binary_codec.dart +++ b/lib/src/binary_codec.dart @@ -329,6 +329,9 @@ class PostgresBinaryDecoder extends Converter { return DateTime.utc(2000) .add(Duration(microseconds: buffer.getInt64(0))); + case PostgreSQLDataType.numeric: + return _decodeNumeric(value); + case PostgreSQLDataType.date: return DateTime.utc(2000).add(Duration(days: buffer.getInt32(0))); @@ -425,6 +428,7 @@ class PostgresBinaryDecoder extends Converter { return decoded; } + /// See: https://github.com/postgres/postgres/blob/master/src/include/catalog/pg_type.dat static final Map typeMap = { 16: PostgreSQLDataType.boolean, 17: PostgreSQLDataType.byteArray, @@ -444,8 +448,33 @@ class PostgresBinaryDecoder extends Converter { 1082: PostgreSQLDataType.date, 1114: PostgreSQLDataType.timestampWithoutTimezone, 1184: PostgreSQLDataType.timestampWithTimezone, + 1700: PostgreSQLDataType.numeric, 2950: PostgreSQLDataType.uuid, 3802: PostgreSQLDataType.jsonb, 3807: PostgreSQLDataType.jsonbArray, }; + + /// Decode numeric / decimal to String without loosing precision. + /// See encoding: https://github.com/postgres/postgres/blob/0e39a608ed5545cc6b9d538ac937c3c1ee8cdc36/src/backend/utils/adt/numeric.c#L305 + /// See implementation: https://github.com/charmander/pg-numeric/blob/0c310eeb11dc680dffb7747821e61d542831108b/index.js#L13 + static String _decodeNumeric(Uint8List value) { + final reader = ByteDataReader()..add(value); + final nDigits = reader.readInt16(); // non-zero digits, data buffer length = 2 * nDigits + var weight = reader.readInt16(); // weight of first digit + final signByte = reader.readInt16(); // NUMERIC_POS, NEG, NAN, PINF, or NINF + final dScale = reader.readInt16(); // display scale + if (signByte == 0xc000) return 'NaN'; + final sign = signByte == 0x4000 ? '-' : ''; + var intPart = ''; + var fractPart = ''; + for (var i = 0; i < nDigits; i++) { + if (weight >= 0) { + intPart += reader.readInt16().toString().padLeft(4, '0'); + } else { + fractPart += reader.readInt16().toString().padLeft(4, '0'); + } + weight--; + } + return '$sign${intPart.replaceAll(RegExp(r'^0+'), '')}.${fractPart.padRight(dScale, '0').substring(0, dScale)}'; + } } diff --git a/lib/src/query.dart b/lib/src/query.dart index 162687d8..8336d3c6 100644 --- a/lib/src/query.dart +++ b/lib/src/query.dart @@ -321,6 +321,7 @@ class PostgreSQLFormatIdentifier { 'date': PostgreSQLDataType.date, 'timestamp': PostgreSQLDataType.timestampWithoutTimezone, 'timestamptz': PostgreSQLDataType.timestampWithTimezone, + 'numeric': PostgreSQLDataType.numeric, 'jsonb': PostgreSQLDataType.jsonb, 'bytea': PostgreSQLDataType.byteArray, 'name': PostgreSQLDataType.name, diff --git a/lib/src/substituter.dart b/lib/src/substituter.dart index fc22db48..69c3f429 100644 --- a/lib/src/substituter.dart +++ b/lib/src/substituter.dart @@ -37,6 +37,8 @@ class PostgreSQLFormat { return 'timestamp'; case PostgreSQLDataType.timestampWithTimezone: return 'timestamptz'; + case PostgreSQLDataType.numeric: + return 'numeric'; case PostgreSQLDataType.date: return 'date'; case PostgreSQLDataType.jsonb: diff --git a/lib/src/types.dart b/lib/src/types.dart index a53ab6bd..cd0ec833 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -42,6 +42,9 @@ enum PostgreSQLDataType { /// Must be a [DateTime] (microsecond date and time precision) timestampWithTimezone, + /// Must be a [List] + numeric, + /// Must be a [DateTime] (contains year, month and day only) date, diff --git a/test/decode_test.dart b/test/decode_test.dart index 478502e7..93c2c950 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -1,4 +1,7 @@ +import 'dart:typed_data'; + import 'package:postgres/postgres.dart'; +import 'package:postgres/src/binary_codec.dart'; import 'package:test/test.dart'; void main() { @@ -167,4 +170,20 @@ void main() { [null] ]); }); + + test('Decode Numeric to String', () { + // -123400000.2 + final binary1 = [0, 4, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 0, 7, 208]; + + // -123400001.01234 + final binary2 = [0, 5, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 1, 0, 0, 7, 208]; + + final decoder = PostgresBinaryDecoder(1700); + final uint8List1 = Uint8List.fromList(binary1); + final uint8List2 = Uint8List.fromList(binary2); + final res1 = decoder.convert(uint8List1); + final res2 = decoder.convert(uint8List2); + expect(res1, '-123400000.20000'); + expect(res2, '-123400001.00002'); + }); }