Skip to content

Commit 7f0a24d

Browse files
authored
Merge pull request #3 from schultek/add-missing-types-pr
Support for additional types (varchar, point, integerArray, textArray, doubleArray, jsonArray)
2 parents f4c2e48 + 99b5bbe commit 7f0a24d

File tree

11 files changed

+499
-38
lines changed

11 files changed

+499
-38
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.3.1
4+
5+
- Added support for types varchar, point, integerArray, doubleArray, textArray and jsonArray.
6+
37
## 2.3.0
48

59
- Finalized null-safe release.

lib/postgres.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ library postgres;
22

33
export 'src/connection.dart';
44
export 'src/execution_context.dart';
5+
export 'src/models.dart';
56
export 'src/substituter.dart';
67
export 'src/types.dart';

lib/src/binary_codec.dart

Lines changed: 143 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
8282
}
8383
case PostgreSQLDataType.name:
8484
case PostgreSQLDataType.text:
85+
case PostgreSQLDataType.varChar:
8586
{
8687
if (value is String) {
8788
return castBytes(utf8.encode(value));
@@ -144,7 +145,7 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
144145
'Invalid type for parameter value. Expected: DateTime Got: ${value.runtimeType}');
145146
}
146147

147-
case PostgreSQLDataType.json:
148+
case PostgreSQLDataType.jsonb:
148149
{
149150
final jsonBytes = utf8.encode(json.encode(value));
150151
final writer = ByteDataWriter(bufferLength: jsonBytes.length + 1);
@@ -153,6 +154,9 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
153154
return writer.toBytes();
154155
}
155156

157+
case PostgreSQLDataType.json:
158+
return castBytes(utf8.encode(json.encode(value)));
159+
156160
case PostgreSQLDataType.byteArray:
157161
{
158162
if (value is List<int>) {
@@ -199,10 +203,90 @@ class PostgresBinaryEncoder extends Converter<dynamic, Uint8List?> {
199203
}
200204
return outBuffer;
201205
}
206+
207+
case PostgreSQLDataType.point:
208+
{
209+
if (value is PgPoint) {
210+
final bd = ByteData(16);
211+
bd.setFloat64(0, value.latitude);
212+
bd.setFloat64(8, value.longitude);
213+
return bd.buffer.asUint8List();
214+
}
215+
throw FormatException(
216+
'Invalid type for parameter value. Expected: PgPoint Got: ${value.runtimeType}');
217+
}
218+
219+
case PostgreSQLDataType.integerArray:
220+
{
221+
if (value is List<int>) {
222+
return writeListBytes<int>(
223+
value, 23, (_) => 4, (writer, item) => writer.writeInt32(item));
224+
}
225+
throw FormatException(
226+
'Invalid type for parameter value. Expected: List<int> Got: ${value.runtimeType}');
227+
}
228+
229+
case PostgreSQLDataType.textArray:
230+
{
231+
if (value is List<String>) {
232+
final bytesArray = value.map((v) => utf8.encode(v));
233+
return writeListBytes<List<int>>(bytesArray, 25,
234+
(item) => item.length, (writer, item) => writer.write(item));
235+
}
236+
throw FormatException(
237+
'Invalid type for parameter value. Expected: List<String> Got: ${value.runtimeType}');
238+
}
239+
240+
case PostgreSQLDataType.doubleArray:
241+
{
242+
if (value is List<double>) {
243+
return writeListBytes<double>(value, 701, (_) => 8,
244+
(writer, item) => writer.writeFloat64(item));
245+
}
246+
throw FormatException(
247+
'Invalid type for parameter value. Expected: List<double> Got: ${value.runtimeType}');
248+
}
249+
250+
case PostgreSQLDataType.jsonbArray:
251+
{
252+
if (value is List<Object>) {
253+
final objectsArray = value.map((v) => utf8.encode(json.encode(v)));
254+
return writeListBytes<List<int>>(
255+
objectsArray, 3802, (item) => item.length + 1, (writer, item) {
256+
writer.writeUint8(1);
257+
writer.write(item);
258+
});
259+
}
260+
throw FormatException(
261+
'Invalid type for parameter value. Expected: List<Object> Got: ${value.runtimeType}');
262+
}
263+
202264
default:
203265
throw PostgreSQLException('Unsupported datatype');
204266
}
205267
}
268+
269+
Uint8List writeListBytes<T>(
270+
Iterable<T> value,
271+
int type,
272+
int Function(T item) lengthEncoder,
273+
void Function(ByteDataWriter writer, T item) valueEncoder) {
274+
final writer = ByteDataWriter();
275+
276+
writer.writeInt32(1); // dimension
277+
writer.writeInt32(0); // ign
278+
writer.writeInt32(type); // type
279+
writer.writeInt32(value.length); // size
280+
writer.writeInt32(1); // index
281+
282+
for (var i in value) {
283+
final len = lengthEncoder(i);
284+
writer.writeInt32(len);
285+
valueEncoder(writer, i);
286+
}
287+
288+
return writer.toBytes();
289+
}
206290
}
207291

208292
class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
@@ -224,6 +308,7 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
224308
switch (dataType) {
225309
case PostgreSQLDataType.name:
226310
case PostgreSQLDataType.text:
311+
case PostgreSQLDataType.varChar:
227312
return utf8.decode(value);
228313
case PostgreSQLDataType.boolean:
229314
return buffer.getInt8(0) != 0;
@@ -247,14 +332,17 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
247332
case PostgreSQLDataType.date:
248333
return DateTime.utc(2000).add(Duration(days: buffer.getInt32(0)));
249334

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

343+
case PostgreSQLDataType.json:
344+
return json.decode(utf8.decode(value));
345+
258346
case PostgreSQLDataType.byteArray:
259347
return value;
260348

@@ -277,6 +365,29 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
277365

278366
return buf.toString();
279367
}
368+
369+
case PostgreSQLDataType.point:
370+
return PgPoint(buffer.getFloat64(0), buffer.getFloat64(8));
371+
372+
case PostgreSQLDataType.integerArray:
373+
return readListBytes<int>(value, (reader, _) => reader.readInt32());
374+
375+
case PostgreSQLDataType.textArray:
376+
return readListBytes<String>(value, (reader, length) {
377+
return utf8.decode(length > 0 ? reader.read(length) : []);
378+
});
379+
380+
case PostgreSQLDataType.doubleArray:
381+
return readListBytes<double>(
382+
value, (reader, _) => reader.readFloat64());
383+
384+
case PostgreSQLDataType.jsonbArray:
385+
return readListBytes<dynamic>(value, (reader, length) {
386+
reader.read(1);
387+
final bytes = reader.read(length - 1);
388+
return json.decode(utf8.decode(bytes));
389+
});
390+
280391
default:
281392
{
282393
// We'll try and decode this as a utf8 string and return that
@@ -292,6 +403,28 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
292403
}
293404
}
294405

406+
List<T> readListBytes<T>(Uint8List data,
407+
T Function(ByteDataReader reader, int length) valueDecoder) {
408+
if (data.length < 16) {
409+
return [];
410+
}
411+
412+
final reader = ByteDataReader()..add(data);
413+
reader.read(12); // header
414+
415+
final decoded = [].cast<T>();
416+
final size = reader.readInt32();
417+
418+
reader.read(4); // index
419+
420+
for (var i = 0; i < size; i++) {
421+
final len = reader.readInt32();
422+
decoded.add(valueDecoder(reader, len));
423+
}
424+
425+
return decoded;
426+
}
427+
295428
static final Map<int, PostgreSQLDataType> typeMap = {
296429
16: PostgreSQLDataType.boolean,
297430
17: PostgreSQLDataType.byteArray,
@@ -300,12 +433,19 @@ class PostgresBinaryDecoder extends Converter<Uint8List, dynamic> {
300433
21: PostgreSQLDataType.smallInteger,
301434
23: PostgreSQLDataType.integer,
302435
25: PostgreSQLDataType.text,
436+
114: PostgreSQLDataType.json,
437+
600: PostgreSQLDataType.point,
303438
700: PostgreSQLDataType.real,
304439
701: PostgreSQLDataType.double,
440+
1007: PostgreSQLDataType.integerArray,
441+
1009: PostgreSQLDataType.textArray,
442+
1043: PostgreSQLDataType.varChar,
443+
1022: PostgreSQLDataType.doubleArray,
305444
1082: PostgreSQLDataType.date,
306445
1114: PostgreSQLDataType.timestampWithoutTimezone,
307446
1184: PostgreSQLDataType.timestampWithTimezone,
308447
2950: PostgreSQLDataType.uuid,
309-
3802: PostgreSQLDataType.json,
448+
3802: PostgreSQLDataType.jsonb,
449+
3807: PostgreSQLDataType.jsonbArray,
310450
};
311451
}

lib/src/models.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class PgPoint {
2+
final double latitude;
3+
final double longitude;
4+
const PgPoint(this.latitude, this.longitude);
5+
6+
@override
7+
bool operator ==(Object other) =>
8+
identical(this, other) ||
9+
other is PgPoint &&
10+
runtimeType == other.runtimeType &&
11+
latitude == other.latitude &&
12+
longitude == other.longitude;
13+
14+
@override
15+
int get hashCode => latitude.hashCode ^ longitude.hashCode;
16+
}

lib/src/query.dart

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,17 @@ class PostgreSQLFormatIdentifier {
320320
'date': PostgreSQLDataType.date,
321321
'timestamp': PostgreSQLDataType.timestampWithoutTimezone,
322322
'timestamptz': PostgreSQLDataType.timestampWithTimezone,
323-
'jsonb': PostgreSQLDataType.json,
323+
'jsonb': PostgreSQLDataType.jsonb,
324324
'bytea': PostgreSQLDataType.byteArray,
325325
'name': PostgreSQLDataType.name,
326-
'uuid': PostgreSQLDataType.uuid
326+
'uuid': PostgreSQLDataType.uuid,
327+
'json': PostgreSQLDataType.json,
328+
'point': PostgreSQLDataType.point,
329+
'_int4': PostgreSQLDataType.integerArray,
330+
'_text': PostgreSQLDataType.textArray,
331+
'_float8': PostgreSQLDataType.doubleArray,
332+
'varchar': PostgreSQLDataType.varChar,
333+
'_jsonb': PostgreSQLDataType.jsonbArray,
327334
};
328335

329336
factory PostgreSQLFormatIdentifier(String t) {

lib/src/substituter.dart

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,28 @@ class PostgreSQLFormat {
3939
return 'timestamptz';
4040
case PostgreSQLDataType.date:
4141
return 'date';
42-
case PostgreSQLDataType.json:
42+
case PostgreSQLDataType.jsonb:
4343
return 'jsonb';
4444
case PostgreSQLDataType.byteArray:
4545
return 'bytea';
4646
case PostgreSQLDataType.name:
4747
return 'name';
4848
case PostgreSQLDataType.uuid:
4949
return 'uuid';
50+
case PostgreSQLDataType.point:
51+
return 'point';
52+
case PostgreSQLDataType.json:
53+
return 'json';
54+
case PostgreSQLDataType.integerArray:
55+
return '_int4';
56+
case PostgreSQLDataType.textArray:
57+
return '_text';
58+
case PostgreSQLDataType.doubleArray:
59+
return '_float8';
60+
case PostgreSQLDataType.varChar:
61+
return 'varchar';
62+
case PostgreSQLDataType.jsonbArray:
63+
return '_jsonb';
5064
default:
5165
return null;
5266
}

lib/src/text_codec.dart

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ class PostgresTextEncoder {
3232
return _encodeJSON(value);
3333
}
3434

35+
if (value is PgPoint) {
36+
return _encodePoint(value);
37+
}
38+
39+
if (value is List) {
40+
return _encodeList(value);
41+
}
42+
3543
// TODO: use custom type encoders
3644

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

156164
return json.encode(value);
157165
}
166+
167+
String _encodePoint(PgPoint value) {
168+
return '(${_encodeDouble(value.latitude)}, ${_encodeDouble(value.longitude)})';
169+
}
170+
171+
String _encodeList(List value) {
172+
if (value.isEmpty) {
173+
return '{}';
174+
}
175+
176+
final type = value.fold(value.first.runtimeType, (type, item) {
177+
if (type == item.runtimeType) {
178+
return type;
179+
} else if ((type == int || type == double) && item is num) {
180+
return double;
181+
} else {
182+
return Map;
183+
}
184+
});
185+
186+
if (type == int || type == double) {
187+
return '{${value.cast<num>().map((s) => s is double ? _encodeDouble(s) : _encodeNumber(s)).join(',')}}';
188+
}
189+
190+
if (type == String) {
191+
return '{${value.cast<String>().map((s) {
192+
final escaped = s.replaceAll(r'\', r'\\').replaceAll('"', r'\"');
193+
return '"$escaped"';
194+
}).join(',')}}';
195+
}
196+
197+
if (type == Map) {
198+
return '{${value.map((s) {
199+
final escaped =
200+
json.encode(s).replaceAll(r'\', r'\\').replaceAll('"', r'\"');
201+
202+
return '"$escaped"';
203+
}).join(',')}}';
204+
}
205+
206+
throw PostgreSQLException("Could not infer array type of value '$value'.");
207+
}
158208
}

0 commit comments

Comments
 (0)