diff --git a/CHANGELOG.md b/CHANGELOG.md index af61552..48d6da9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.5.11 + +- Adding `JsonbListView` with `isSqlNull(int index)` method to check if a JSONB array has SQL or JSON null value. + ## 3.5.10 - Supporting `BYTEA[]` built-in type. diff --git a/lib/src/types.dart b/lib/src/types.dart index 316bb47..8811b79 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -3,6 +3,7 @@ import 'dart:core' as core; import 'dart:core'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; import 'types/generic_type.dart'; @@ -469,3 +470,29 @@ class TypedValue { return 'TypedValue($type, $value)'; } } + +/// A decoded PostgreSQL `jsonb[]` array. +/// +/// Wraps the list of decoded JSON values and tracks which elements were SQL +/// `NULL` (as opposed to the JSON `null` literal, which also decodes to Dart +/// `null` but is a non-null value at the SQL level). +/// +/// Use [isSqlNull] to distinguish between the two: +/// ```dart +/// final list = row[0] as JsonbListView; +/// list[0]; // null — but is it SQL NULL or JSON null? +/// list.isSqlNull(0); // true → SQL NULL (IS NULL in SQL) +/// list.isSqlNull(1); // false → JSON null ('null'::jsonb, IS NOT NULL in SQL) +/// ``` +class JsonbListView extends UnmodifiableListView { + final List _sqlNulls; + + JsonbListView(super.items, List sqlNulls) : _sqlNulls = sqlNulls; + + /// Returns `true` if the element at [index] was a SQL `NULL` in the array + /// (i.e. the PostgreSQL wire protocol sent a -1 length sentinel). + /// + /// Returns `false` for elements that carry an actual value, including the + /// JSON `null` literal (`'null'::jsonb`), which also decodes to Dart `null`. + bool isSqlNull(int index) => _sqlNulls[index]; +} diff --git a/lib/src/types/binary_codec.dart b/lib/src/types/binary_codec.dart index e626647..5b562fb 100644 --- a/lib/src/types/binary_codec.dart +++ b/lib/src/types/binary_codec.dart @@ -583,9 +583,11 @@ class PostgresBinaryEncoder { case TypeOid.jsonbArray: { if (input is List) { - final objectsArray = input - .map(_jsonFusedEncoding(encoding).encode) - .toList(); + final encoder = _jsonFusedEncoding(encoding); + final objectsArray = input.map((item) { + if (item is TypedValue && item.isSqlNull) return null; + return encoder.encode(item); + }).toList(); return _writeListBytes>( objectsArray, 3802, @@ -914,7 +916,7 @@ class PostgresBinaryDecoder { return readListBytes( input, (reader, length) => reader.read(length), - ); + ).items; case TypeOid.uuid: return _decodeUuid(input); @@ -922,7 +924,7 @@ class PostgresBinaryDecoder { return readListBytes( input, (reader, _) => _decodeUuid(reader.read(16)), - ); + ).items; case TypeOid.regtype: final data = input.buffer.asByteData(input.offsetInBytes, input.length); @@ -984,26 +986,35 @@ class PostgresBinaryDecoder { return readListBytes( input, (reader, _) => reader.readUint8() != 0, - ); + ).items; case TypeOid.smallIntegerArray: - return readListBytes(input, (reader, _) => reader.readInt16()); + return readListBytes( + input, + (reader, _) => reader.readInt16(), + ).items; case TypeOid.integerArray: - return readListBytes(input, (reader, _) => reader.readInt32()); + return readListBytes( + input, + (reader, _) => reader.readInt32(), + ).items; case TypeOid.bigIntegerArray: - return readListBytes(input, (reader, _) => reader.readInt64()); + return readListBytes( + input, + (reader, _) => reader.readInt64(), + ).items; case TypeOid.dateArray: return readListBytes( input, (reader, _) => DateTime.utc(2000).add(Duration(days: reader.readInt32())), - ); + ).items; case TypeOid.timeArray: return readListBytes