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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 2.3.1

- Added support for types varchar, point, integerArray, doubleArray, textArray and jsonArray.

## 2.3.0

- Finalized null-safe release.
Expand Down
1 change: 1 addition & 0 deletions lib/postgres.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ library postgres;

export 'src/connection.dart';
export 'src/execution_context.dart';
export 'src/models.dart';
export 'src/substituter.dart';
export 'src/types.dart';
146 changes: 143 additions & 3 deletions lib/src/binary_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,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 +145,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 +154,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,10 +203,90 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
}
return outBuffer;
}

case PostgreSQLDataType.point:
{
if (value is PgPoint) {
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: PgPoint Got: ${value.runtimeType}');
}

case PostgreSQLDataType.integerArray:
{
if (value is List<int>) {
return writeListBytes<int>(
value, 23, (_) => 4, (writer, item) => writer.writeInt32(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) => utf8.encode(v));
return writeListBytes<List<int>>(bytesArray, 25,
(item) => item.length, (writer, item) => writer.write(item));
}
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,
(writer, item) => writer.writeFloat64(item));
}
throw FormatException(
'Invalid type for parameter value. Expected: List<double> Got: ${value.runtimeType}');
}

case PostgreSQLDataType.jsonbArray:
{
if (value is List<Object>) {
final objectsArray = value.map((v) => utf8.encode(json.encode(v)));
return writeListBytes<List<int>>(
objectsArray, 3802, (item) => item.length + 1, (writer, item) {
writer.writeUint8(1);
writer.write(item);
});
}
throw FormatException(
'Invalid type for parameter value. Expected: List<Object> Got: ${value.runtimeType}');
}

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

Uint8List writeListBytes<T>(
Iterable<T> value,
int type,
int Function(T item) lengthEncoder,
void Function(ByteDataWriter writer, T item) valueEncoder) {
final writer = ByteDataWriter();

writer.writeInt32(1); // dimension
writer.writeInt32(0); // ign
writer.writeInt32(type); // type
writer.writeInt32(value.length); // size
writer.writeInt32(1); // index

for (var i in value) {
final len = lengthEncoder(i);
writer.writeInt32(len);
valueEncoder(writer, i);
}

return writer.toBytes();
}
}

class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
Expand All @@ -224,6 +308,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 +332,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 +365,29 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {

return buf.toString();
}

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

case PostgreSQLDataType.integerArray:
return readListBytes<int>(value, (reader, _) => reader.readInt32());

case PostgreSQLDataType.textArray:
return readListBytes<String>(value, (reader, length) {
return utf8.decode(length > 0 ? reader.read(length) : []);
});

case PostgreSQLDataType.doubleArray:
return readListBytes<double>(
value, (reader, _) => reader.readFloat64());

case PostgreSQLDataType.jsonbArray:
return readListBytes<dynamic>(value, (reader, length) {
reader.read(1);
final bytes = reader.read(length - 1);
return json.decode(utf8.decode(bytes));
});

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

List<T> readListBytes<T>(Uint8List data,
T Function(ByteDataReader reader, int length) valueDecoder) {
if (data.length < 16) {
return [];
}

final reader = ByteDataReader()..add(data);
reader.read(12); // header

final decoded = [].cast<T>();
final size = reader.readInt32();

reader.read(4); // index

for (var i = 0; i < size; i++) {
final len = reader.readInt32();
decoded.add(valueDecoder(reader, len));
}

return decoded;
}

static final Map<int, PostgreSQLDataType> typeMap = {
16: PostgreSQLDataType.boolean,
17: PostgreSQLDataType.byteArray,
Expand All @@ -300,12 +433,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,
};
}
16 changes: 16 additions & 0 deletions lib/src/models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class PgPoint {
final double latitude;
final double longitude;
const PgPoint(this.latitude, this.longitude);

@override
bool operator ==(Object other) =>
identical(this, other) ||
other is PgPoint &&
runtimeType == other.runtimeType &&
latitude == other.latitude &&
longitude == other.longitude;

@override
int get hashCode => latitude.hashCode ^ longitude.hashCode;
}
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
50 changes: 50 additions & 0 deletions lib/src/text_codec.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ class PostgresTextEncoder {
return _encodeJSON(value);
}

if (value is PgPoint) {
return _encodePoint(value);
}

if (value is List) {
return _encodeList(value);
}

// TODO: use custom type encoders

throw PostgreSQLException("Could not infer type of value '$value'.");
Expand Down Expand Up @@ -155,4 +163,46 @@ class PostgresTextEncoder {

return json.encode(value);
}

String _encodePoint(PgPoint value) {
return '(${_encodeDouble(value.latitude)}, ${_encodeDouble(value.longitude)})';
}

String _encodeList(List value) {
if (value.isEmpty) {
return '{}';
}

final type = value.fold(value.first.runtimeType, (type, item) {
if (type == item.runtimeType) {
return type;
} else if ((type == int || type == double) && item is num) {
return double;
} else {
return Map;
}
});

if (type == int || type == double) {
return '{${value.cast<num>().map((s) => s is double ? _encodeDouble(s) : _encodeNumber(s)).join(',')}}';
}

if (type == String) {
return '{${value.cast<String>().map((s) {
final escaped = s.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
return '"$escaped"';
}).join(',')}}';
}

if (type == Map) {
return '{${value.map((s) {
final escaped =
json.encode(s).replaceAll(r'\', r'\\').replaceAll('"', r'\"');

return '"$escaped"';
}).join(',')}}';
}

throw PostgreSQLException("Could not infer array type of value '$value'.");
}
}
Loading