Skip to content

Commit

Permalink
Merge pull request #3 from schultek/add-missing-types-pr
Browse files Browse the repository at this point in the history
Support for additional types (varchar, point, integerArray, textArray, doubleArray, jsonArray)
  • Loading branch information
isoos authored Mar 25, 2021
2 parents f4c2e48 + 99b5bbe commit 7f0a24d
Show file tree
Hide file tree
Showing 11 changed files with 499 additions and 38 deletions.
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

0 comments on commit 7f0a24d

Please sign in to comment.