Skip to content

Commit 983df6e

Browse files
authored
Merge pull request isoos#9 from Gustl22/164-numeric-datatype
Binary encoder for numeric datatype
2 parents 81ab97f + 98d8659 commit 983df6e

File tree

4 files changed

+193
-40
lines changed

4 files changed

+193
-40
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22

33
## Unreleased
44

5+
- Support for type `numeric` / `decimal` ([#7](https://github.com/isoos/postgresql-dart/pull/7), [#9](https://github.com/isoos/postgresql-dart/pull/9)).
56
- Support SASL / SCRAM-SHA-256 Authentication, [#6](https://github.com/isoos/postgresql-dart/pull/6).
6-
- Decoder for type `numeric` / `decimal`, [#7](https://github.com/isoos/postgresql-dart/pull/7).
77

88
## 2.3.2
99

lib/src/binary_codec.dart

Lines changed: 106 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ final _hex = <String>[
2727
'e',
2828
'f',
2929
];
30+
final _numericRegExp = RegExp(r'^(\d*)(\.\d*)?$');
31+
final _leadingZerosRegExp = RegExp(r'^0+');
32+
final _trailingZerosRegExp = RegExp(r'0+$');
3033

3134
class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
3235
final PostgreSQLDataType _dataType;
@@ -141,8 +144,19 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
141144
0, value.toUtc().difference(DateTime.utc(2000)).inMicroseconds);
142145
return bd.buffer.asUint8List();
143146
}
147+
throw FormatException('Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}');
148+
}
149+
150+
case PostgreSQLDataType.numeric:
151+
{
152+
if (value is double || value is int) {
153+
value = value.toString();
154+
}
155+
if (value is String) {
156+
return _encodeNumeric(value);
157+
}
144158
throw FormatException(
145-
'Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}');
159+
'Invalid type for parameter value. Expected: String|double|int Got: ${value.runtimeType}');
146160
}
147161

148162
case PostgreSQLDataType.jsonb:
@@ -287,6 +301,76 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
287301

288302
return writer.toBytes();
289303
}
304+
305+
/// Encode String / double / int to numeric / decimal without loosing precision.
306+
/// Compare implementation: https://github.com/frohoff/jdk8u-dev-jdk/blob/da0da73ab82ed714dc5be94acd2f0d00fbdfe2e9/src/share/classes/java/math/BigDecimal.java#L409
307+
Uint8List _encodeNumeric(String value) {
308+
value = value.trim();
309+
var signByte = 0x0000;
310+
if (value.toLowerCase() == 'nan') {
311+
signByte = 0xc000;
312+
value = '';
313+
} else if (value.startsWith('-')) {
314+
value = value.substring(1);
315+
signByte = 0x4000;
316+
} else if (value.startsWith('+')) {
317+
value = value.substring(1);
318+
}
319+
if (!_numericRegExp.hasMatch(value)) {
320+
throw FormatException('Invalid format for parameter value. Expected: String which matches "/^(\\d*)(\\.\\d*)?\$/" Got: ${value}');
321+
}
322+
final parts = value.split('.');
323+
324+
var intPart = parts[0].replaceAll(_leadingZerosRegExp, '');
325+
var intWeight = intPart.isEmpty ? -1 : (intPart.length - 1) ~/ 4;
326+
intPart = intPart.padLeft((intWeight + 1) * 4, '0');
327+
328+
var fractPart = parts.length > 1 ? parts[1] : '';
329+
final dScale = fractPart.length;
330+
fractPart = fractPart.replaceAll(_trailingZerosRegExp, '');
331+
var fractWeight = fractPart.isEmpty ? -1 : (fractPart.length - 1) ~/ 4;
332+
fractPart = fractPart.padRight((fractWeight + 1) * 4, '0');
333+
334+
var weight = intWeight;
335+
if (intWeight < 0) {
336+
// If int part has no weight, handle leading zeros in fractional part.
337+
if (fractPart.isEmpty) {
338+
// Weight of value 0 or '' is 0;
339+
weight = 0;
340+
} else {
341+
final leadingZeros = _leadingZerosRegExp.firstMatch(fractPart)?.group(0);
342+
if (leadingZeros != null) {
343+
final leadingZerosWeight = leadingZeros.length ~/ 4; // Get count of leading zeros '0000'
344+
fractPart = fractPart.substring(leadingZerosWeight * 4); // Remove leading zeros '0000'
345+
fractWeight -= leadingZerosWeight;
346+
weight = -(leadingZerosWeight + 1); // Ignore leading zeros in weight
347+
}
348+
}
349+
} else if (fractWeight < 0) {
350+
// If int fract has no weight, handle trailing zeros in int part.
351+
final trailingZeros = _trailingZerosRegExp.firstMatch(intPart)?.group(0);
352+
if (trailingZeros != null) {
353+
final trailingZerosWeight = trailingZeros.length ~/ 4; // Get count of trailing zeros '0000'
354+
intPart = intPart.substring(0, intPart.length - trailingZerosWeight * 4); // Remove leading zeros '0000'
355+
intWeight -= trailingZerosWeight;
356+
}
357+
}
358+
359+
final nDigits = intWeight + fractWeight + 2;
360+
361+
final writer = ByteDataWriter();
362+
writer.writeInt16(nDigits);
363+
writer.writeInt16(weight);
364+
writer.writeUint16(signByte);
365+
writer.writeInt16(dScale);
366+
for (var i = 0; i <= intWeight * 4; i += 4) {
367+
writer.writeInt16(int.parse(intPart.substring(i, i + 4)));
368+
}
369+
for (var i = 0; i <= fractWeight * 4; i += 4) {
370+
writer.writeInt16(int.parse(fractPart.substring(i, i + 4)));
371+
}
372+
return writer.toBytes();
373+
}
290374
}
291375

292376
class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
@@ -461,12 +545,19 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
461545
final reader = ByteDataReader()..add(value);
462546
final nDigits = reader.readInt16(); // non-zero digits, data buffer length = 2 * nDigits
463547
var weight = reader.readInt16(); // weight of first digit
464-
final signByte = reader.readInt16(); // NUMERIC_POS, NEG, NAN, PINF, or NINF
548+
final signByte = reader.readUint16(); // NUMERIC_POS, NEG, NAN, PINF, or NINF
465549
final dScale = reader.readInt16(); // display scale
466550
if (signByte == 0xc000) return 'NaN';
467551
final sign = signByte == 0x4000 ? '-' : '';
468552
var intPart = '';
469553
var fractPart = '';
554+
555+
final fractOmitted = -(weight + 1);
556+
if (fractOmitted > 0) {
557+
// If value < 0, the leading zeros in fractional part were omitted.
558+
fractPart += '0000' * fractOmitted;
559+
}
560+
470561
for (var i = 0; i < nDigits; i++) {
471562
if (weight >= 0) {
472563
intPart += reader.readInt16().toString().padLeft(4, '0');
@@ -475,6 +566,18 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
475566
}
476567
weight--;
477568
}
478-
return '$sign${intPart.replaceAll(RegExp(r'^0+'), '')}.${fractPart.padRight(dScale, '0').substring(0, dScale)}';
569+
570+
if (weight >= 0) {
571+
// Trailing zeros were omitted
572+
intPart += '0000' * (weight + 1);
573+
}
574+
575+
var result = '$sign${intPart.replaceAll(_leadingZerosRegExp, '')}';
576+
if (result.isEmpty) result = '0'; // Show at least 0, if no int value is given.
577+
if (dScale > 0) {
578+
// Only add fractional digits, if dScale allows
579+
result += '.${fractPart.padRight(dScale, '0').substring(0, dScale)}';
580+
}
581+
return result;
479582
}
480583
}

test/decode_test.dart

Lines changed: 44 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,33 @@ void main() {
1414
await connection.execute('''
1515
CREATE TEMPORARY TABLE t (
1616
i int, s serial, bi bigint, bs bigserial, bl boolean, si smallint,
17-
t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, j jsonb, ba bytea,
17+
t text, f real, d double precision, dt date, ts timestamp, tsz timestamptz, n numeric, j jsonb, ba bytea,
1818
u uuid, v varchar, p point, jj json, ia _int4, ta _text, da _float8, ja _jsonb)
1919
''');
2020

2121
await connection.execute(
22-
'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u, v, p, jj, ia, ta, da, ja) '
22+
'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, n, j, ba, u, v, p, jj, ia, ta, da, ja) '
2323
'VALUES (-2147483648, -9223372036854775808, TRUE, -32768, '
2424
"'string', 10.0, 10.0, '1983-11-06', "
2525
"'1983-11-06 06:00:00.000000', '1983-11-06 06:00:00.000000', "
26+
"'-1234567890.0987654321', "
2627
"'{\"key\":\"value\"}', E'\\\\000', '00000000-0000-0000-0000-000000000000', "
2728
"'abcdef', '(0.01, 12.34)', '{\"key\": \"value\"}', '{}', '{}', '{}', '{}')");
2829
await connection.execute(
29-
'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u, v, p, jj, ia, ta, da, ja) '
30+
'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, n, j, ba, u, v, p, jj, ia, ta, da, ja) '
3031
'VALUES (2147483647, 9223372036854775807, FALSE, 32767, '
3132
"'a significantly longer string to the point where i doubt this actually matters', "
3233
"10.25, 10.125, '2183-11-06', '2183-11-06 00:00:00.111111', "
3334
"'2183-11-06 00:00:00.999999', "
35+
"'1000000000000000000000000000.0000000000000000000000000001', "
3436
"'[{\"key\":1}]', E'\\\\377', 'FFFFFFFF-ffff-ffff-ffff-ffffffffffff', "
3537
"'01234', '(0.2, 100)', '{}', '{-123, 999}', '{\"a\", \"lorem ipsum\", \"\"}', "
3638
"'{1, 2, 4.5, 1234.5}', '{1, \"\\\"test\\\"\", \"{\\\"a\\\": \\\"b\\\"}\"}')");
3739

3840
await connection.execute(
39-
'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, j, ba, u, v, p, jj, ia, ta, da, ja) '
41+
'INSERT INTO t (i, bi, bl, si, t, f, d, dt, ts, tsz, n, j, ba, u, v, p, jj, ia, ta, da, ja) '
4042
'VALUES (null, null, null, null, null, null, null, null, null, null, null, null, null, '
41-
'null, null, null, null, null, null, null )');
43+
'null, null, null, null, null, null, null, null )');
4244
});
4345
tearDown(() async {
4446
await connection.close();
@@ -66,16 +68,17 @@ void main() {
6668
expect(row1[9], equals(DateTime.utc(1983, 11, 6)));
6769
expect(row1[10], equals(DateTime.utc(1983, 11, 6, 6)));
6870
expect(row1[11], equals(DateTime.utc(1983, 11, 6, 6)));
69-
expect(row1[12], equals({'key': 'value'}));
70-
expect(row1[13], equals([0]));
71-
expect(row1[14], equals('00000000-0000-0000-0000-000000000000'));
72-
expect(row1[15], equals('abcdef'));
73-
expect(row1[16], equals(PgPoint(0.01, 12.34)));
74-
expect(row1[17], equals({'key': 'value'}));
75-
expect(row1[18], equals(<int>[]));
76-
expect(row1[19], equals(<String>[]));
77-
expect(row1[20], equals(<double>[]));
78-
expect(row1[21], equals([]));
71+
expect(row1[12], equals('-1234567890.0987654321'));
72+
expect(row1[13], equals({'key': 'value'}));
73+
expect(row1[14], equals([0]));
74+
expect(row1[15], equals('00000000-0000-0000-0000-000000000000'));
75+
expect(row1[16], equals('abcdef'));
76+
expect(row1[17], equals(PgPoint(0.01, 12.34)));
77+
expect(row1[18], equals({'key': 'value'}));
78+
expect(row1[19], equals(<int>[]));
79+
expect(row1[20], equals(<String>[]));
80+
expect(row1[21], equals(<double>[]));
81+
expect(row1[22], equals([]));
7982

8083
// upper bound row
8184
expect(row2[0], equals(2147483647));
@@ -95,21 +98,22 @@ void main() {
9598
expect(row2[9], equals(DateTime.utc(2183, 11, 6)));
9699
expect(row2[10], equals(DateTime.utc(2183, 11, 6, 0, 0, 0, 111, 111)));
97100
expect(row2[11], equals(DateTime.utc(2183, 11, 6, 0, 0, 0, 999, 999)));
101+
expect(row2[12], equals('1000000000000000000000000000.0000000000000000000000000001'));
98102
expect(
99-
row2[12],
103+
row2[13],
100104
equals([
101105
{'key': 1}
102106
]));
103-
expect(row2[13], equals([255]));
104-
expect(row2[14], equals('ffffffff-ffff-ffff-ffff-ffffffffffff'));
105-
expect(row2[15], equals('01234'));
106-
expect(row2[16], equals(PgPoint(0.2, 100)));
107-
expect(row2[17], equals({}));
108-
expect(row2[18], equals(<int>[-123, 999]));
109-
expect(row2[19], equals(<String>['a', 'lorem ipsum', '']));
110-
expect(row2[20], equals(<double>[1, 2, 4.5, 1234.5]));
107+
expect(row2[14], equals([255]));
108+
expect(row2[15], equals('ffffffff-ffff-ffff-ffff-ffffffffffff'));
109+
expect(row2[16], equals('01234'));
110+
expect(row2[17], equals(PgPoint(0.2, 100)));
111+
expect(row2[18], equals({}));
112+
expect(row2[19], equals(<int>[-123, 999]));
113+
expect(row2[20], equals(<String>['a', 'lorem ipsum', '']));
114+
expect(row2[21], equals(<double>[1, 2, 4.5, 1234.5]));
111115
expect(
112-
row2[21],
116+
row2[22],
113117
equals([
114118
1,
115119
'test',
@@ -139,6 +143,7 @@ void main() {
139143
expect(row3[19], isNull);
140144
expect(row3[20], isNull);
141145
expect(row3[21], isNull);
146+
expect(row3[22], isNull);
142147
});
143148

144149
test('Fetch/insert empty string', () async {
@@ -172,18 +177,21 @@ void main() {
172177
});
173178

174179
test('Decode Numeric to String', () {
175-
// -123400000.2
176-
final binary1 = [0, 4, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 0, 7, 208];
177-
178-
// -123400001.01234
179-
final binary2 = [0, 5, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 1, 0, 0, 7, 208];
180+
final binaries = {
181+
'-123400000.20000': [0, 4, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 0, 7, 208],
182+
'-123400001.00002': [0, 5, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 1, 0, 0, 7, 208],
183+
'0.00001': [0, 1, 255, 254, 0, 0, 0, 5, 3, 232],
184+
'10000.000000000': [0, 1, 0, 1, 0, 0, 0, 9, 0, 1],
185+
'NaN': [0, 0, 0, 0, 192, 0, 0, 0],
186+
'0': [0, 0, 0, 0, 0, 0, 0, 0], // 0 or 0.
187+
'0.0': [0, 0, 0, 0, 0, 0, 0, 1], // .0 or 0.0
188+
};
180189

181190
final decoder = PostgresBinaryDecoder(1700);
182-
final uint8List1 = Uint8List.fromList(binary1);
183-
final uint8List2 = Uint8List.fromList(binary2);
184-
final res1 = decoder.convert(uint8List1);
185-
final res2 = decoder.convert(uint8List2);
186-
expect(res1, '-123400000.20000');
187-
expect(res2, '-123400001.00002');
191+
binaries.forEach((key, value) {
192+
final uint8List = Uint8List.fromList(value);
193+
final res = decoder.convert(uint8List);
194+
expect(res, key);
195+
});
188196
});
189197
}

test/encoding_test.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22
import 'dart:convert';
3+
import 'dart:typed_data';
34

45
import 'package:postgres/postgres.dart';
56
import 'package:postgres/src/binary_codec.dart';
@@ -182,6 +183,47 @@ void main() {
182183
}
183184
});
184185

186+
test('numeric', () async {
187+
final binaries = {
188+
'-123400000.20000': [0, 4, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 0, 7, 208],
189+
'-123400001.00002': [0, 5, 0, 2, 64, 0, 0, 5, 0, 1, 9, 36, 0, 1, 0, 0, 7, 208],
190+
'0.00001': [0, 1, 255, 254, 0, 0, 0, 5, 3, 232],
191+
'10000.000000000': [0, 1, 0, 1, 0, 0, 0, 9, 0, 1],
192+
'NaN': [0, 0, 0, 0, 192, 0, 0, 0],
193+
'0': [0, 0, 0, 0, 0, 0, 0, 0], // 0 or 0.
194+
'0.0': [0, 0, 0, 0, 0, 0, 0, 1], // .0 or 0.0
195+
};
196+
197+
final encoder = PostgresBinaryEncoder(PostgreSQLDataType.numeric);
198+
binaries.forEach((key, value) {
199+
final uint8List = Uint8List.fromList(value);
200+
final res = encoder.convert(key);
201+
expect(res, uint8List);
202+
});
203+
204+
await expectInverse('1000000000000000000000000000.0000000000000000000000000001', PostgreSQLDataType.numeric);
205+
await expectInverse('3141592653589793238462643383279502.1618033988749894848204586834365638', PostgreSQLDataType.numeric);
206+
await expectInverse('-3141592653589793238462643383279502.1618033988749894848204586834365638', PostgreSQLDataType.numeric);
207+
await expectInverse('0.0', PostgreSQLDataType.numeric);
208+
await expectInverse('0.1', PostgreSQLDataType.numeric);
209+
await expectInverse('0.0001', PostgreSQLDataType.numeric);
210+
await expectInverse('0.00001', PostgreSQLDataType.numeric);
211+
await expectInverse('0.000001', PostgreSQLDataType.numeric);
212+
await expectInverse('0.000000001', PostgreSQLDataType.numeric);
213+
await expectInverse('1.000000000', PostgreSQLDataType.numeric);
214+
await expectInverse('1000.000000000', PostgreSQLDataType.numeric);
215+
await expectInverse('10000.000000000', PostgreSQLDataType.numeric);
216+
await expectInverse('100000000.00000000', PostgreSQLDataType.numeric);
217+
await expectInverse('NaN', PostgreSQLDataType.numeric);
218+
try {
219+
await conn.query('INSERT INTO t (v) VALUES (@v:numeric)',
220+
substitutionValues: {'v': 'not-numeric'});
221+
fail('unreachable');
222+
} on FormatException catch (e) {
223+
expect(e.toString(), contains('Expected: String'));
224+
}
225+
});
226+
185227
test('jsonb', () async {
186228
await expectInverse('string', PostgreSQLDataType.jsonb);
187229
await expectInverse(2, PostgreSQLDataType.jsonb);

0 commit comments

Comments
 (0)