Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## 3.5.10

- Supporting `BYTEA[]` built-in type.
- Fix TypedValue parameter propagation.

## 3.5.9

Expand Down
39 changes: 36 additions & 3 deletions lib/src/v3/connection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ abstract class _PgSessionBase implements Session {
} else {
// The simple query protocol does not support variables. So when we have
// parameters, we need an explicit prepare.
final prepared = await _prepare(description);
final prepared = await _prepare(description, variables);
try {
return await prepared.run(variables, timeout: timeout);
} finally {
Expand All @@ -195,7 +195,10 @@ abstract class _PgSessionBase implements Session {
return await _prepare(query);
}

Future<_PreparedStatement> _prepare(Object query) async {
Future<_PreparedStatement> _prepare(
Object query, [
List<TypedValue>? fallbackTypes,
]) async {
final stackTrace = StackTrace.current;
final trace = Trace.from(stackTrace);
final conn = _connection;
Expand All @@ -209,7 +212,7 @@ abstract class _PgSessionBase implements Session {
ParseMessage(
description.transformedSql,
statementName: name,
typeOids: description.parameterTypes?.map((e) => e?.oid).toList(),
typeOids: _mergeTypeOids(description.parameterTypes, fallbackTypes),
),
stackTrace: stackTrace,
);
Expand Down Expand Up @@ -1391,6 +1394,36 @@ class _AuthenticationProcedure extends _PendingOperation {
}
}

/// Merges inline SQL type annotations with runtime [TypedValue] types for use
/// in a [ParseMessage].
///
/// Inline annotations (from `:type` syntax) take precedence. For positions
/// without an annotation, the [TypedValue.type] is used as a hint so that
/// PostgreSQL can resolve polymorphic operators (e.g. `@>`, `&&`, `<@`).
List<int?>? _mergeTypeOids(
List<Type?>? paramTypes,
List<TypedValue>? fallbackTypes,
) {
if (fallbackTypes == null || fallbackTypes.isEmpty) {
return paramTypes?.map((e) => e?.oid).toList();
}
final length = paramTypes?.length ?? fallbackTypes.length;
final result = <int?>[];
for (var i = 0; i < length; i++) {
final fromAnnotation =
(paramTypes != null && i < paramTypes.length) ? paramTypes[i]?.oid : null;
if (fromAnnotation != null) {
result.add(fromAnnotation);
} else {
final type = i < fallbackTypes.length ? fallbackTypes[i].type : null;
result.add(
(type != null && type != Type.unspecified) ? type.oid : null,
);
}
}
return result;
}

extension on PgException {
bool get willAbortConnection {
return severity == Severity.fatal || severity == Severity.panic;
Expand Down
164 changes: 164 additions & 0 deletions test/byte_array_array_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import 'dart:typed_data';

import 'package:postgres/postgres.dart';
import 'package:test/test.dart';

import 'docker.dart';

void main() {
withPostgresServer('byteArrayArray (_bytea)', (server) {
late Connection conn;

setUp(() async {
conn = await server.newConnection();
});

tearDown(() async {
await conn.close();
});

test('round-trips via SELECT', () async {
Future<void> check(List<List<int>?> value) async {
final result = await conn.execute(
Sql(r'SELECT $1', types: [Type.byteArrayArray]),
parameters: [value],
);
final returned = result.single.single as List;
expect(returned.length, value.length);
for (var i = 0; i < value.length; i++) {
if (value[i] == null) {
expect(returned[i], isNull);
} else {
expect(returned[i], value[i]);
}
}
}

await check([]);
await check([
[0],
]);
await check([
[1, 2, 3],
]);
await check([
[255, 254, 253],
]);
await check([
[0],
[1, 2, 3],
[255, 254, 253],
]);
await check([null]);
await check([
null,
[1, 2, 3],
null,
]);
});

test('round-trips via named parameter', () async {
Future<void> check(List<List<int>?> value) async {
final result = await conn.execute(
Sql.named('SELECT @v:_bytea'),
parameters: {'v': value},
);
final returned = result.single.single as List;
expect(returned.length, value.length);
for (var i = 0; i < value.length; i++) {
if (value[i] == null) {
expect(returned[i], isNull);
} else {
expect(returned[i], value[i]);
}
}
}

await check([]);
await check([
[42],
]);
await check([
null,
[1, 2],
[3, 4, 5],
]);
});

test('round-trips through a table column', () async {
await conn.execute('CREATE TEMPORARY TABLE t (v bytea[])');

final values = [
<List<int>?>[],
[
[0],
],
[
[1, 2, 3],
[255, 254, 253],
],
[
null,
[10, 20],
null,
],
];

for (final value in values) {
await conn.execute(
Sql.named('INSERT INTO t (v) VALUES (@v:_bytea)'),
parameters: {'v': value},
);
}

final result = await conn.execute('SELECT v FROM t ORDER BY ctid');
expect(result.length, values.length);

for (var i = 0; i < values.length; i++) {
final returned = result[i][0] as List;
final expected = values[i];
expect(returned.length, expected.length);
for (var j = 0; j < expected.length; j++) {
if (expected[j] == null) {
expect(returned[j], isNull);
} else {
expect(returned[j], expected[j]);
}
}
}
});

test('SQL NULL round-trips as null', () async {
final result = await conn.execute(
Sql.named('SELECT @v:_bytea'),
parameters: {'v': null},
);
expect(result.single.single, isNull);
});

test('decoded elements are Uint8List', () async {
final result = await conn.execute(
Sql(r'SELECT $1', types: [Type.byteArrayArray]),
parameters: [
[
[1, 2, 3],
],
],
);
final list = result.single.single as List;
expect(list.single, isA<Uint8List>());
});

test('rejects wrong element type', () async {
await expectLater(
() => conn.execute(
Sql.named('SELECT @v:_bytea'),
parameters: {
'v': ['not-a-list'],
},
),
throwsA(isA<FormatException>()),
);
});
});
}
79 changes: 79 additions & 0 deletions test/typed_value_parameter_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:postgres/postgres.dart';
import 'package:test/test.dart';

import 'docker.dart';

void main() {
withPostgresServer('TypedValue parameter type propagation', (server) {
late Connection conn;

setUp(() async {
conn = await server.newConnection();
});

tearDown(() async {
await conn.close();
});

test('daterange @> TypedValue(Type.date) without inline annotation',
() async {
final result = await conn.execute(
Sql.named(
"SELECT daterange('2026-01-01','2026-01-10','[)') @> @d",
),
parameters: {'d': TypedValue(Type.date, DateTime.utc(2026, 1, 5))},
);
expect(result.single.single, isTrue);
});

test('TypedValue date outside range returns false', () async {
final result = await conn.execute(
Sql.named(
"SELECT daterange('2026-01-01','2026-01-10','[)') @> @d",
),
parameters: {'d': TypedValue(Type.date, DateTime.utc(2026, 1, 20))},
);
expect(result.single.single, isFalse);
});

test('integerArray && TypedValue(_int4) without inline annotation',
() async {
final result = await conn.execute(
Sql.named("SELECT ARRAY[1,2,3] && @arr"),
parameters: {
'arr': TypedValue(Type.integerArray, [2, 5]),
},
);
expect(result.single.single, isTrue);
});

test('inline annotation takes precedence over TypedValue type', () async {
// :date annotation wins even though we pass TypedValue(Type.date, ...)
final result = await conn.execute(
Sql.named(
"SELECT daterange('2026-01-01','2026-01-10','[)') @> @d:date",
),
parameters: {'d': TypedValue(Type.date, DateTime.utc(2026, 1, 5))},
);
expect(result.single.single, isTrue);
});

test('positional TypedValue without explicit types list', () async {
final result = await conn.execute(
Sql(r"SELECT daterange('2026-01-01','2026-01-10','[)') @> $1"),
parameters: [TypedValue(Type.date, DateTime.utc(2026, 1, 5))],
);
expect(result.single.single, isTrue);
});

test('unspecified TypedValue still infers type from value', () async {
// Type.unspecified means the driver should fall back to text encoding,
// which PostgreSQL can handle for simple equality checks.
final result = await conn.execute(
Sql.named('SELECT @v::int = 42'),
parameters: {'v': TypedValue(Type.unspecified, 42)},
);
expect(result.single.single, isTrue);
});
});
}
Loading