diff --git a/lib/postgres.dart b/lib/postgres.dart index 737a7b42..672f0853 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -16,7 +16,7 @@ export 'src/exceptions.dart'; export 'src/pool/pool_api.dart'; export 'src/replication.dart'; export 'src/types.dart'; -export 'src/types/type_registry.dart' show TypeOid, TypeRegistry; +export 'src/types/type_registry.dart' show TypeRegistry; /// A description of a SQL query as interpreted by this package. /// diff --git a/lib/src/messages/client_messages.dart b/lib/src/messages/client_messages.dart index 0575e245..fb241b66 100644 --- a/lib/src/messages/client_messages.dart +++ b/lib/src/messages/client_messages.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:charcode/ascii.dart'; +import 'package:postgres/src/types/generic_type.dart'; import '../buffer.dart'; import '../replication.dart'; diff --git a/lib/src/types.dart b/lib/src/types.dart index 219526fb..88ce9aa4 100644 --- a/lib/src/types.dart +++ b/lib/src/types.dart @@ -1,4 +1,3 @@ -import 'dart:convert'; import 'dart:core'; import 'dart:core' as core; import 'dart:typed_data'; @@ -139,206 +138,156 @@ class Point { /// Supported data types. abstract class Type { /// Used to represent value without any type representation. - static const unspecified = GenericType(null); + static final unspecified = unspecifiedType(); /// Must be a [String]. - static const text = - GenericType(TypeOid.text, nameForSubstitution: 'text'); + static final text = + _genericType(TypeOid.text, nameForSubstitution: 'text'); /// Must be an [int] (4-byte integer) - static const integer = - GenericType(TypeOid.integer, nameForSubstitution: 'int4'); + static final integer = + _genericType(TypeOid.integer, nameForSubstitution: 'int4'); /// Must be an [int] (2-byte integer) - static const smallInteger = - GenericType(TypeOid.smallInteger, nameForSubstitution: 'int2'); + static final smallInteger = + _genericType(TypeOid.smallInteger, nameForSubstitution: 'int2'); /// Must be an [int] (8-byte integer) - static const bigInteger = - GenericType(TypeOid.bigInteger, nameForSubstitution: 'int8'); + static final bigInteger = + _genericType(TypeOid.bigInteger, nameForSubstitution: 'int8'); /// Must be an [int] (autoincrementing 4-byte integer) - static const serial = GenericType(null, nameForSubstitution: 'int4'); + static final serial = _genericType(null, nameForSubstitution: 'int4'); /// Must be an [int] (autoincrementing 8-byte integer) - static const bigSerial = GenericType(null, nameForSubstitution: 'int8'); + static final bigSerial = _genericType(null, nameForSubstitution: 'int8'); /// Must be a [double] (32-bit floating point value) - static const real = - GenericType(TypeOid.real, nameForSubstitution: 'float4'); + static final real = + _genericType(TypeOid.real, nameForSubstitution: 'float4'); /// Must be a [double] (64-bit floating point value) - static const double = - GenericType(TypeOid.double, nameForSubstitution: 'float8'); + static final double = + _genericType(TypeOid.double, nameForSubstitution: 'float8'); /// Must be a [bool] - static const boolean = - GenericType(TypeOid.boolean, nameForSubstitution: 'boolean'); + static final boolean = + _genericType(TypeOid.boolean, nameForSubstitution: 'boolean'); /// Must be a [DateTime] (microsecond date and time precision) - static const timestampWithoutTimezone = GenericType( + static final timestampWithoutTimezone = _genericType( TypeOid.timestampWithoutTimezone, nameForSubstitution: 'timestamp'); /// Must be a [DateTime] (microsecond date and time precision) - static const timestampWithTimezone = GenericType( + static final timestampWithTimezone = _genericType( TypeOid.timestampWithTimezone, nameForSubstitution: 'timestamptz'); /// Must be a [Interval] - static const interval = - GenericType(TypeOid.interval, nameForSubstitution: 'interval'); + static final interval = + _genericType(TypeOid.interval, nameForSubstitution: 'interval'); /// An arbitrary-precision number. /// /// This library supports encoding numbers in a textual format, or when /// passed as [int] or [double]. When decoding values, numeric types are /// always returned as string. - static const numeric = - GenericType(TypeOid.numeric, nameForSubstitution: 'numeric'); + static final numeric = + _genericType(TypeOid.numeric, nameForSubstitution: 'numeric'); /// Must be a [DateTime] (contains year, month and day only) - static const date = - GenericType(TypeOid.date, nameForSubstitution: 'date'); + static final date = + _genericType(TypeOid.date, nameForSubstitution: 'date'); /// Must be encodable via [json.encode]. /// /// Values will be encoded via [json.encode] before being sent to the database. - static const jsonb = GenericType(TypeOid.jsonb, nameForSubstitution: 'jsonb'); + static final jsonb = GenericType(TypeOid.jsonb, nameForSubstitution: 'jsonb'); /// Must be encodable via [core.json.encode]. /// /// Values will be encoded via [core.json.encode] before being sent to the database. - static const json = GenericType(TypeOid.json, nameForSubstitution: 'json'); + static final json = GenericType(TypeOid.json, nameForSubstitution: 'json'); /// Must be a [List] of [int]. /// /// Each element of the list must fit into a byte (0-255). - static const byteArray = - GenericType>(TypeOid.byteArray, nameForSubstitution: 'bytea'); + static final byteArray = + _genericType>(TypeOid.byteArray, nameForSubstitution: 'bytea'); /// Must be a [String] /// /// Used for internal pg structure names - static const name = - GenericType(TypeOid.name, nameForSubstitution: 'name'); + static final name = + _genericType(TypeOid.name, nameForSubstitution: 'name'); /// Must be a [String]. /// /// Must contain 32 hexadecimal characters. May contain any number of '-' characters. /// When returned from database, format will be xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx. - static const uuid = - GenericType(TypeOid.uuid, nameForSubstitution: 'uuid'); + static final uuid = + _genericType(TypeOid.uuid, nameForSubstitution: 'uuid'); /// Must be a [Point] - static const point = - GenericType(TypeOid.point, nameForSubstitution: 'point'); + static final point = + _genericType(TypeOid.point, nameForSubstitution: 'point'); /// Must be a [List] - static const booleanArray = GenericType>(TypeOid.booleanArray, + static final booleanArray = _genericType>(TypeOid.booleanArray, nameForSubstitution: '_bool'); /// Must be a [List] - static const integerArray = GenericType>(TypeOid.integerArray, + static final integerArray = _genericType>(TypeOid.integerArray, nameForSubstitution: '_int4'); /// Must be a [List] - static const bigIntegerArray = GenericType>(TypeOid.bigIntegerArray, + static final bigIntegerArray = _genericType>( + TypeOid.bigIntegerArray, nameForSubstitution: '_int8'); /// Must be a [List] - static const textArray = GenericType>(TypeOid.textArray, + static final textArray = _genericType>(TypeOid.textArray, nameForSubstitution: '_text'); /// Must be a [List] - static const doubleArray = GenericType>(TypeOid.doubleArray, + static final doubleArray = _genericType>( + TypeOid.doubleArray, nameForSubstitution: '_float8'); /// Must be a [String] - static const varChar = - GenericType(TypeOid.varChar, nameForSubstitution: 'varchar'); + static final varChar = + _genericType(TypeOid.varChar, nameForSubstitution: 'varchar'); /// Must be a [List] - static const varCharArray = GenericType>(TypeOid.varCharArray, + static final varCharArray = _genericType>(TypeOid.varCharArray, nameForSubstitution: '_varchar'); /// Must be a [List] of encodable objects - static const jsonbArray = - GenericType(TypeOid.jsonbArray, nameForSubstitution: '_jsonb'); + static final jsonbArray = + _genericType(TypeOid.jsonbArray, nameForSubstitution: '_jsonb'); /// Must be a [Type]. - static const regtype = - GenericType(TypeOid.regtype, nameForSubstitution: 'regtype'); + static final regtype = + _genericType(TypeOid.regtype, nameForSubstitution: 'regtype'); /// Impossible to bind to, always null when read. - static const voidType = GenericType(TypeOid.voidType); + static final voidType = _genericType(TypeOid.voidType); /// The object ID of this data type. final int? oid; - /// The name of this type as considered by [Sql.named]. - /// - /// To declare an explicit type for a substituted parameter in a query, this - /// name can be used. - final String? nameForSubstitution; - - const Type( - this.oid, { - this.nameForSubstitution, - }); - - bool get hasOid => oid != null && oid! > 0; + const Type(this.oid); TypedValue value(T value) => TypedValue(this, value); - EncodeOutput encode(EncodeInput input); - - T? decode(DecodeInput input); - @override - String toString() => '$runtimeType(oid:$oid)'; + String toString() => 'Type(oid:$oid)'; } -class EncodeInput { - final T value; - final Encoding encoding; - - EncodeInput({ - required this.value, - required this.encoding, - }); -} - -class EncodeOutput { - final Uint8List? bytes; - final String? text; - - EncodeOutput.bytes(Uint8List value) - : bytes = value, - text = null; - - EncodeOutput.text(String value) - : bytes = null, - text = value; - - bool get isBinary => bytes != null; -} - -class DecodeInput { - final Uint8List bytes; - final bool isBinary; - final Encoding encoding; - final TypeRegistry typeRegistry; - - DecodeInput({ - required this.bytes, - required this.isBinary, - required this.encoding, - required this.typeRegistry, - }); - - late final asText = encoding.decode(bytes); -} +Type _genericType(int? typeOid, + {String? nameForSubstitution}) => + GenericType(typeOid, nameForSubstitution: nameForSubstitution); class TypedValue { final Type type; diff --git a/lib/src/types/binary_codec.dart b/lib/src/types/binary_codec.dart index f2343b04..9c19f950 100644 --- a/lib/src/types/binary_codec.dart +++ b/lib/src/types/binary_codec.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:buffer/buffer.dart'; +import 'package:postgres/src/types/generic_type.dart'; import '../buffer.dart'; import '../types.dart'; diff --git a/lib/src/types/generic_type.dart b/lib/src/types/generic_type.dart index f0325fb3..37c27d49 100644 --- a/lib/src/types/generic_type.dart +++ b/lib/src/types/generic_type.dart @@ -1,13 +1,55 @@ +import 'dart:convert'; import 'dart:typed_data'; import '../types.dart'; import 'binary_codec.dart'; import 'text_codec.dart'; +import 'type_registry.dart'; + +class EncodeOutput { + final Uint8List? bytes; + final String? text; + + EncodeOutput.bytes(Uint8List value) + : bytes = value, + text = null; + + EncodeOutput.text(String value) + : bytes = null, + text = value; + + bool get isBinary => bytes != null; +} + +class EncodeInput { + final T value; + final Encoding encoding; + + EncodeInput({ + required this.value, + required this.encoding, + }); +} + +class DecodeInput { + final Uint8List bytes; + final bool isBinary; + final Encoding encoding; + final TypeRegistry typeRegistry; + + DecodeInput({ + required this.bytes, + required this.isBinary, + required this.encoding, + required this.typeRegistry, + }); + + late final asText = encoding.decode(bytes); +} class UnknownType extends Type { UnknownType(super.oid); - @override EncodeOutput encode(EncodeInput input) { final v = input.value; if (v is Uint8List) { @@ -19,22 +61,32 @@ class UnknownType extends Type { 'Encoding ${v.runtimeType} for oid:$oid is not supported.'); } - @override Object? decode(DecodeInput input) { return TypedBytes(typeOid: oid ?? 0, bytes: input.bytes); } } +class UnspecifiedType extends Type { + const UnspecifiedType() : super(null); +} + +Type unspecifiedType() => const UnspecifiedType(); + /// NOTE: do not use this type in client code. class GenericType extends Type { + /// The name of this type as considered by [Sql.named]. + /// + /// To declare an explicit type for a substituted parameter in a query, this + /// name can be used. + final String? nameForSubstitution; + const GenericType( super.oid, { - super.nameForSubstitution, + this.nameForSubstitution, }); - @override EncodeOutput encode(EncodeInput input) { - if (hasOid) { + if (oid != null && oid! > 0) { final encoder = PostgresBinaryEncoder(oid!); final bytes = encoder.convert(input.value, input.encoding); return EncodeOutput.bytes(bytes); @@ -45,7 +97,6 @@ class GenericType extends Type { } } - @override T? decode(DecodeInput input) { if (input.isBinary) { return PostgresBinaryDecoder(oid!).convert(input) as T?; diff --git a/lib/src/types/text_codec.dart b/lib/src/types/text_codec.dart index ca1c4733..81890c95 100644 --- a/lib/src/types/text_codec.dart +++ b/lib/src/types/text_codec.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:postgres/src/types/generic_type.dart'; + import '../exceptions.dart'; import '../types.dart'; import 'type_registry.dart'; diff --git a/lib/src/types/type_registry.dart b/lib/src/types/type_registry.dart index d0619e15..c62360d6 100644 --- a/lib/src/types/type_registry.dart +++ b/lib/src/types/type_registry.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'dart:typed_data'; import 'package:meta/meta.dart'; +import 'package:postgres/src/exceptions.dart'; +import 'package:postgres/src/types/text_codec.dart'; import '../types.dart'; @@ -88,7 +90,7 @@ class TypeRegistry { /// Registers a type. void _register(Type type) { - if (type.hasOid) { + if (type.oid != null && type.oid! > 0) { _byTypeOid[type.oid!] = type; } // We don't index serial and bigSerial types here because they're using @@ -97,6 +99,7 @@ class TypeRegistry { // should always resolve to integer and bigInteger. if (type != Type.serial && type != Type.bigSerial && + type is GenericType && type.nameForSubstitution != null) { _bySubstitutionName[type.nameForSubstitution!] = type; } @@ -117,14 +120,26 @@ class TypeRegistry { Iterable get registered => _byTypeOid.values; } +final _textEncoder = const PostgresTextEncoder(); + extension TypeRegistryExt on TypeRegistry { EncodeOutput? encodeValue( Object? value, { - Type? type, + required Type type, required Encoding encoding, }) { if (value == null) return null; - return type?.encode(EncodeInput(value: value, encoding: encoding)); + switch (type) { + case GenericType(): + return type.encode(EncodeInput(value: value, encoding: encoding)); + case UnspecifiedType(): + final encoded = _textEncoder.tryConvert(value); + if (encoded != null) { + return EncodeOutput.text(encoded); + } + break; + } + throw PgException("Could not infer type of value '$value'."); } Object? decodeBytes( @@ -137,11 +152,22 @@ extension TypeRegistryExt on TypeRegistry { return null; } final type = resolveOid(typeOid); - return type.decode(DecodeInput( - bytes: bytes, - isBinary: isBinary, - encoding: encoding, - typeRegistry: this, - )); + switch (type) { + case GenericType(): + return type.decode(DecodeInput( + bytes: bytes, + isBinary: isBinary, + encoding: encoding, + typeRegistry: this, + )); + case UnknownType(): + return type.decode(DecodeInput( + bytes: bytes, + isBinary: isBinary, + encoding: encoding, + typeRegistry: this, + )); + } + return TypedBytes(typeOid: typeOid, bytes: bytes); } } diff --git a/lib/src/v2/query.dart b/lib/src/v2/query.dart index e2aa024a..3ac0e79a 100644 --- a/lib/src/v2/query.dart +++ b/lib/src/v2/query.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:postgres/src/buffer.dart'; +import 'package:postgres/src/types/generic_type.dart'; import 'package:postgres/src/types/type_registry.dart'; import '../exceptions.dart'; diff --git a/lib/src/v2/substituter.dart b/lib/src/v2/substituter.dart index 7a601793..d52b0bc8 100644 --- a/lib/src/v2/substituter.dart +++ b/lib/src/v2/substituter.dart @@ -1,3 +1,5 @@ +import 'package:postgres/src/types/generic_type.dart'; + import '../types.dart'; import '../types/text_codec.dart'; import 'query.dart'; @@ -14,7 +16,8 @@ class PostgreSQLFormat { } static String? dataTypeStringForDataType(Type? dt) { - return dt?.nameForSubstitution; + final gt = dt is GenericType ? dt : null; + return gt?.nameForSubstitution; } static String substitute(String fmtString, Map? values, diff --git a/test/decode_test.dart b/test/decode_test.dart index 48374b50..229a87fc 100644 --- a/test/decode_test.dart +++ b/test/decode_test.dart @@ -4,6 +4,7 @@ import 'package:buffer/buffer.dart'; import 'package:postgres/legacy.dart'; import 'package:postgres/postgres.dart'; import 'package:postgres/src/types/binary_codec.dart'; +import 'package:postgres/src/types/generic_type.dart'; import 'package:test/test.dart'; import 'docker.dart'; diff --git a/test/encoding_test.dart b/test/encoding_test.dart index e605589a..806c5bbe 100644 --- a/test/encoding_test.dart +++ b/test/encoding_test.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:postgres/legacy.dart'; import 'package:postgres/postgres.dart'; import 'package:postgres/src/types/binary_codec.dart'; +import 'package:postgres/src/types/generic_type.dart'; import 'package:postgres/src/types/text_codec.dart'; import 'package:test/test.dart'; diff --git a/test/interpolation_test.dart b/test/interpolation_test.dart index 9a7c559e..bbfdd3b6 100644 --- a/test/interpolation_test.dart +++ b/test/interpolation_test.dart @@ -7,7 +7,7 @@ import 'package:test/test.dart'; void main() { test('Ensure all types/format type mappings are available and accurate', () { - const withoutMapping = { + final withoutMapping = { Type.unspecified, // Can't bind into unspecified type Type.voidType, // Can't assign to void Type.bigSerial, // Can only be created from a table sequence diff --git a/test/v3_test.dart b/test/v3_test.dart index 403d81c5..7688a071 100644 --- a/test/v3_test.dart +++ b/test/v3_test.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:async/async.dart'; import 'package:postgres/messages.dart'; import 'package:postgres/postgres.dart'; +import 'package:postgres/src/types/generic_type.dart'; import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; @@ -92,9 +93,10 @@ void main() { [matcher ?? value] ]); - if (type.nameForSubstitution != null) { + final gt = type is GenericType ? type as GenericType : null; + if (gt?.nameForSubstitution != null) { final rowFromInferredType = await connection.execute( - Sql.named('SELECT @var:${type.nameForSubstitution}'), + Sql.named('SELECT @var:${gt?.nameForSubstitution}'), parameters: [value], ); expect(rowFromInferredType, [