Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for additional types (varchar, point, integerArray, textArray, doubleArray, jsonArray) #3

Merged
merged 18 commits into from
Mar 25, 2021
152 changes: 149 additions & 3 deletions lib/src/binary_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:convert';
import 'dart:typed_data';

import 'package:buffer/buffer.dart';
import 'package:latlng/latlng.dart';
schultek marked this conversation as resolved.
Show resolved Hide resolved

import '../postgres.dart';
import 'types.dart';
Expand Down Expand Up @@ -82,6 +83,7 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
}
case PostgreSQLDataType.name:
case PostgreSQLDataType.text:
case PostgreSQLDataType.varChar:
{
if (value is String) {
return castBytes(utf8.encode(value));
Expand Down Expand Up @@ -144,7 +146,7 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
'Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}');
}

case PostgreSQLDataType.json:
case PostgreSQLDataType.jsonb:
{
final jsonBytes = utf8.encode(json.encode(value));
final writer = ByteDataWriter(bufferLength: jsonBytes.length + 1);
Expand All @@ -153,6 +155,9 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
return writer.toBytes();
}

case PostgreSQLDataType.json:
return castBytes(utf8.encode(json.encode(value)));

case PostgreSQLDataType.byteArray:
{
if (value is List<int>) {
Expand Down Expand Up @@ -199,12 +204,99 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
}
return outBuffer;
}

case PostgreSQLDataType.point:
{
if (value is LatLng) {
final bd = ByteData(16);
bd.setFloat64(0, value.latitude);
bd.setFloat64(8, value.longitude);
return bd.buffer.asUint8List();
}
throw FormatException(
'Invalid type for parameter value. Expected: LatLng Got: ${value.runtimeType}');
}

case PostgreSQLDataType.integerArray:
{
if (value is List<int>) {
return writeListBytes<int>(value, 23, (_) => 4,
(bd, offset, item) => bd.setInt32(offset, item));
}
throw FormatException(
'Invalid type for parameter value. Expected: List<int> Got: ${value.runtimeType}');
}

case PostgreSQLDataType.textArray:
{
if (value is List<String>) {
final bytesArray = value.map((v) => castBytes(utf8.encode(v)));
return writeListBytes<Uint8List>(
bytesArray, 25, (item) => item.length, (bd, offset, item) {
item.forEach((i) => bd.setUint8(offset++, i));
});
}
throw FormatException(
'Invalid type for parameter value. Expected: List<String> Got: ${value.runtimeType}');
}

case PostgreSQLDataType.doubleArray:
{
if (value is List<double>) {
return writeListBytes<double>(value, 701, (_) => 8,
(bd, offset, item) => bd.setFloat64(offset, item));
}
throw FormatException(
'Invalid type for parameter value. Expected: List<double> Got: ${value.runtimeType}');
}

case PostgreSQLDataType.jsonbArray:
{
if (value is List<Object>) {
final bytesArray = value.map((v) {
final jsonBytes = utf8.encode(json.encode(v));
final writer = ByteDataWriter(bufferLength: jsonBytes.length + 1);
writer.writeUint8(1);
writer.write(jsonBytes);
return writer.toBytes();
});
return writeListBytes<Uint8List>(
bytesArray, 3802, (item) => item.length, (bd, offset, item) {
item.forEach((i) => bd.setUint8(offset++, i));
});
}
throw FormatException(
'Invalid type for parameter value. Expected: List<Object> Got: ${value.runtimeType}');
}

default:
throw PostgreSQLException('Unsupported datatype');
}
}
}

Uint8List writeListBytes<T>(
schultek marked this conversation as resolved.
Show resolved Hide resolved
Iterable<T> value,
int type,
int Function(T item) lengthEncoder,
void Function(ByteData bd, int offset, T item) valueEncoder) {
final bd =
ByteData(20 + value.fold<int>(0, (sum, item) => 4 + lengthEncoder(item)));
bd.setInt32(0, 1); // dimension
bd.setInt32(4, 0); // ign
bd.setInt32(8, type); // type
bd.setInt32(12, value.length); // size
bd.setInt32(16, 1); // index
var offset = 20;
for (var i in value) {
final len = lengthEncoder(i);
bd.setInt32(offset, len); // value length
valueEncoder(bd, offset + 4, i); // value
offset += 4 + len;
}
return bd.buffer.asUint8List();
}

class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
const PostgresBinaryDecoder(this.typeCode);

Expand All @@ -224,6 +316,7 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
switch (dataType) {
case PostgreSQLDataType.name:
case PostgreSQLDataType.text:
case PostgreSQLDataType.varChar:
return utf8.decode(value);
case PostgreSQLDataType.boolean:
return buffer.getInt8(0) != 0;
Expand All @@ -247,14 +340,17 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
case PostgreSQLDataType.date:
return DateTime.utc(2000).add(Duration(days: buffer.getInt32(0)));

case PostgreSQLDataType.json:
case PostgreSQLDataType.jsonb:
{
// Removes version which is first character and currently always '1'
final bytes = value.buffer
.asUint8List(value.offsetInBytes + 1, value.lengthInBytes - 1);
return json.decode(utf8.decode(bytes));
}

case PostgreSQLDataType.json:
return json.decode(utf8.decode(value));

case PostgreSQLDataType.byteArray:
return value;

Expand All @@ -277,6 +373,29 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {

return buf.toString();
}

case PostgreSQLDataType.point:
return LatLng(buffer.getFloat64(0), buffer.getFloat64(8));

case PostgreSQLDataType.integerArray:
return readListBytes<int>(
buffer, (offset, _) => buffer.getInt32(offset));

case PostgreSQLDataType.textArray:
return readListBytes<String>(buffer, (offset, length) {
return utf8.decode(value.sublist(offset, offset + length));
});

case PostgreSQLDataType.doubleArray:
return readListBytes<double>(
buffer, (offset, _) => buffer.getFloat64(offset));

case PostgreSQLDataType.jsonbArray:
return readListBytes<dynamic>(buffer, (offset, length) {
final bytes = value.sublist(offset + 1, offset + length);
return json.decode(utf8.decode(bytes));
});

default:
{
// We'll try and decode this as a utf8 string and return that
Expand All @@ -292,6 +411,26 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
}
}

List<T> readListBytes<T>(
schultek marked this conversation as resolved.
Show resolved Hide resolved
ByteData buffer, T Function(int offset, int length) valueDecoder) {
final decoded = [].cast<T>();

try {
final size = buffer.getInt32(12);

var offset = 20;
for (var i = 0; i < size; i++) {
final len = buffer.getInt32(offset);
decoded.add(valueDecoder(offset + 4, len));
offset += 4 + len;
}

return decoded;
} on RangeError catch (_) {
return decoded;
schultek marked this conversation as resolved.
Show resolved Hide resolved
}
}

static final Map<int, PostgreSQLDataType> typeMap = {
16: PostgreSQLDataType.boolean,
17: PostgreSQLDataType.byteArray,
Expand All @@ -300,12 +439,19 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
21: PostgreSQLDataType.smallInteger,
23: PostgreSQLDataType.integer,
25: PostgreSQLDataType.text,
114: PostgreSQLDataType.json,
600: PostgreSQLDataType.point,
700: PostgreSQLDataType.real,
701: PostgreSQLDataType.double,
1007: PostgreSQLDataType.integerArray,
1009: PostgreSQLDataType.textArray,
1043: PostgreSQLDataType.varChar,
1022: PostgreSQLDataType.doubleArray,
1082: PostgreSQLDataType.date,
1114: PostgreSQLDataType.timestampWithoutTimezone,
1184: PostgreSQLDataType.timestampWithTimezone,
2950: PostgreSQLDataType.uuid,
3802: PostgreSQLDataType.json,
3802: PostgreSQLDataType.jsonb,
3807: PostgreSQLDataType.jsonbArray,
};
}
11 changes: 9 additions & 2 deletions lib/src/query.dart
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,17 @@ class PostgreSQLFormatIdentifier {
'date': PostgreSQLDataType.date,
'timestamp': PostgreSQLDataType.timestampWithoutTimezone,
'timestamptz': PostgreSQLDataType.timestampWithTimezone,
'jsonb': PostgreSQLDataType.json,
'jsonb': PostgreSQLDataType.jsonb,
'bytea': PostgreSQLDataType.byteArray,
'name': PostgreSQLDataType.name,
'uuid': PostgreSQLDataType.uuid
'uuid': PostgreSQLDataType.uuid,
'json': PostgreSQLDataType.json,
'point': PostgreSQLDataType.point,
'_int4': PostgreSQLDataType.integerArray,
'_text': PostgreSQLDataType.textArray,
'_float8': PostgreSQLDataType.doubleArray,
'varchar': PostgreSQLDataType.varChar,
'_jsonb': PostgreSQLDataType.jsonbArray,
};

factory PostgreSQLFormatIdentifier(String t) {
Expand Down
16 changes: 15 additions & 1 deletion lib/src/substituter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,28 @@ class PostgreSQLFormat {
return 'timestamptz';
case PostgreSQLDataType.date:
return 'date';
case PostgreSQLDataType.json:
case PostgreSQLDataType.jsonb:
return 'jsonb';
case PostgreSQLDataType.byteArray:
return 'bytea';
case PostgreSQLDataType.name:
return 'name';
case PostgreSQLDataType.uuid:
return 'uuid';
case PostgreSQLDataType.point:
return 'point';
case PostgreSQLDataType.json:
return 'json';
case PostgreSQLDataType.integerArray:
return '_int4';
case PostgreSQLDataType.textArray:
return '_text';
case PostgreSQLDataType.doubleArray:
return '_float8';
case PostgreSQLDataType.varChar:
return 'varchar';
case PostgreSQLDataType.jsonbArray:
return '_jsonb';
default:
return null;
}
Expand Down
25 changes: 24 additions & 1 deletion lib/src/types.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ enum PostgreSQLDataType {
/// Must be a [DateTime] (contains year, month and day only)
date,

/// Must be encodable via [json.encode].
///
/// Values will be encoded via [json.encode] before being sent to the database.
jsonb,

/// Must be encodable via [json.encode].
///
/// Values will be encoded via [json.encode] before being sent to the database.
Expand All @@ -64,5 +69,23 @@ enum PostgreSQLDataType {
///
/// Must contain 32 hexadecimal characters. May contain any number of '-' characters.
/// When returned from database, format will be xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.
uuid
uuid,

/// Must be a [LatLng]
point,

/// Must be a [List<int>]
integerArray,

/// Must be a [List<String>]
textArray,

/// Must be a [List<double>]
doubleArray,

/// Must be a [String]
varChar,

/// Must be a [List] of encodable objects
jsonbArray,
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ environment:
dependencies:
buffer: ^1.1.0
crypto: ^3.0.0
latlng: ^0.1.0
collection: ^1.15.0

dev_dependencies:
Expand Down
8 changes: 4 additions & 4 deletions test/encoding_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -184,14 +184,14 @@ void main() {
});

test('jsonb', () async {
await expectInverse('string', PostgreSQLDataType.json);
await expectInverse(2, PostgreSQLDataType.json);
await expectInverse(['foo'], PostgreSQLDataType.json);
await expectInverse('string', PostgreSQLDataType.jsonb);
await expectInverse(2, PostgreSQLDataType.jsonb);
await expectInverse(['foo'], PostgreSQLDataType.jsonb);
await expectInverse({
'key': 'val',
'key1': 1,
'array': ['foo']
}, PostgreSQLDataType.json);
}, PostgreSQLDataType.jsonb);

try {
await conn.query('INSERT INTO t (v) VALUES (@v:jsonb)',
Expand Down
2 changes: 1 addition & 1 deletion test/query_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ void main() {
'${PostgreSQLFormat.id('dt', type: PostgreSQLDataType.date)},'
'${PostgreSQLFormat.id('ts', type: PostgreSQLDataType.timestampWithoutTimezone)},'
'${PostgreSQLFormat.id('tsz', type: PostgreSQLDataType.timestampWithTimezone)},'
'${PostgreSQLFormat.id('j', type: PostgreSQLDataType.json)},'
'${PostgreSQLFormat.id('j', type: PostgreSQLDataType.jsonb)},'
'${PostgreSQLFormat.id('u', type: PostgreSQLDataType.uuid)})'
' returning i,s, bi, bs, bl, si, t, f, d, dt, ts, tsz, j, u',
substitutionValues: {
Expand Down