Skip to content

Commit 73dd0d7

Browse files
authored
feat: allow explicit use of Simple Query Protocol (v3) (#118)
1 parent cb091e0 commit 73dd0d7

File tree

5 files changed

+96
-20
lines changed

5 files changed

+96
-20
lines changed

lib/postgres_v3_experimental.dart

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,11 +129,17 @@ abstract class PgSession {
129129
/// optimization can be applied also depends on the parameters chosen, so
130130
/// there is no guarantee that the [PgResult] from a [ignoreRows] excution has
131131
/// no rows.
132+
///
133+
/// [queryMode] is optional to override the default query execution mode that
134+
/// is defined in [PgSessionSettings]. Unless necessary, always prefer using
135+
/// [QueryMode.extended] which is the default value. For more information,
136+
/// see [PgSessionSettings.queryMode]
132137
Future<PgResult> execute(
133138
Object /* String | PgSql */ query, {
134139
Object? /* List<Object?|PgTypedParameter> | Map<String, Object?|PgTypedParameter> */
135140
parameters,
136141
bool ignoreRows = false,
142+
QueryMode? queryMode,
137143
});
138144

139145
/// Closes this session, cleaning up resources and forbiding further calls to
@@ -376,13 +382,23 @@ final class PgSessionSettings {
376382
/// [Streaming Replication Protocol]: https://www.postgresql.org/docs/current/protocol-replication.html
377383
final ReplicationMode replicationMode;
378384

385+
/// The Query Execution Mode
386+
///
387+
/// The default value is [QueryMode.extended] which uses the Extended Query
388+
/// Protocol. In certain cases, the Extended protocol cannot be used
389+
/// (e.g. in replication mode or with proxies such as PGBouncer), hence the
390+
/// the Simple one would be the only viable option. Unless necessary, always
391+
/// prefer using [QueryMode.extended].
392+
final QueryMode queryMode;
393+
379394
PgSessionSettings({
380395
this.connectTimeout,
381396
this.timeZone,
382397
this.encoding,
383398
this.onBadSslCertificate,
384399
this.transformer,
385400
this.replicationMode = ReplicationMode.none,
401+
this.queryMode = QueryMode.extended,
386402
});
387403
}
388404

@@ -393,3 +409,11 @@ final class PgPoolSettings {
393409
this.maxConnectionCount,
394410
});
395411
}
412+
413+
/// Options for the Query Execution Mode
414+
enum QueryMode {
415+
/// Extended Query Protocol
416+
extended,
417+
/// Simple Query Protocol
418+
simple,
419+
}

lib/src/text_codec.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,12 @@ class PostgresTextDecoder<T extends Object> {
242242
case PgDataType.double:
243243
return num.parse(asText) as T;
244244
case PgDataType.boolean:
245-
return (asText == 'true') as T;
245+
// In text data format when using simple query protocol, "true" & "false"
246+
// are represented as `t` and `f`, respectively.
247+
// we will check for both just in case
248+
// TODO: should we check for other representations (e.g. `1`, `on`, `y`,
249+
// and `yes`)?
250+
return (asText == 't' || asText == 'true') as T;
246251

247252
// We could list out all cases, but it's about 20 lines of code.
248253
// ignore: no_default_cases

lib/src/v3/connection.dart

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class _ResolvedSettings {
4242
final Encoding encoding;
4343

4444
final ReplicationMode replicationMode;
45+
final QueryMode queryMode;
4546

4647
final StreamChannelTransformer<BaseMessage, BaseMessage>? transformer;
4748

@@ -56,7 +57,8 @@ class _ResolvedSettings {
5657
timeZone = settings?.timeZone ?? 'UTC',
5758
encoding = settings?.encoding ?? utf8,
5859
transformer = settings?.transformer,
59-
replicationMode = settings?.replicationMode ?? ReplicationMode.none;
60+
replicationMode = settings?.replicationMode ?? ReplicationMode.none,
61+
queryMode = settings?.queryMode ?? QueryMode.extended;
6062

6163
bool onBadSslCertificate(X509Certificate certificate) {
6264
return settings?.onBadSslCertificate?.call(certificate) ?? false;
@@ -120,36 +122,49 @@ abstract class _PgSessionBase implements PgSession {
120122
Object query, {
121123
Object? parameters,
122124
bool ignoreRows = false,
125+
QueryMode? queryMode,
123126
}) async {
124127
final description = InternalQueryDescription.wrap(query);
125128
final variables = description.bindParameters(parameters);
126129

127-
if (!ignoreRows || variables.isNotEmpty) {
128-
// The simple query protocol does not support variables and returns rows
129-
// as text. So when we need rows or parameters, we need an explicit prepare.
130-
final prepared = await prepare(description);
131-
try {
132-
return await prepared.run(variables);
133-
} finally {
134-
await prepared.dispose();
135-
}
130+
late final bool isSimple;
131+
if (queryMode != null) {
132+
isSimple = queryMode == QueryMode.simple;
136133
} else {
134+
isSimple = _connection._settings.queryMode == QueryMode.simple;
135+
}
136+
137+
if (isSimple && variables.isNotEmpty) {
138+
throw PostgreSQLException('Parameterized queries are not supported when '
139+
'using the Simple Query Protocol');
140+
}
141+
142+
if (isSimple || (ignoreRows && variables.isEmpty)) {
137143
// Great, we can just run a simple query.
138144
final controller = StreamController<PgResultRow>();
139145
final items = <PgResultRow>[];
140146

141-
final querySubscription =
142-
_PgResultStreamSubscription.simpleQueryAndIgnoreRows(
147+
final querySubscription = _PgResultStreamSubscription.simpleQueryProtocol(
143148
description.transformedSql,
144149
this,
145150
controller,
146151
controller.stream.listen(items.add),
152+
ignoreRows,
147153
);
148154
await querySubscription.asFuture();
149155
await querySubscription.cancel();
150156

151157
return PgResult(items, await querySubscription.affectedRows,
152158
await querySubscription.schema);
159+
} else {
160+
// The simple query protocol does not support variables. So when we have
161+
// parameters, we need an explicit prepare.
162+
final prepared = await prepare(description);
163+
try {
164+
return await prepared.run(variables);
165+
} finally {
166+
await prepared.dispose();
167+
}
153168
}
154169
}
155170

@@ -496,9 +511,8 @@ class _PgResultStreamSubscription
496511
});
497512
}
498513

499-
_PgResultStreamSubscription.simpleQueryAndIgnoreRows(
500-
String sql, this.session, this._controller, this._source)
501-
: ignoreRows = true {
514+
_PgResultStreamSubscription.simpleQueryProtocol(String sql, this.session,
515+
this._controller, this._source, this.ignoreRows) {
502516
session._withResource(() async {
503517
connection._pending = this;
504518

lib/src/v3/pool.dart

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,17 @@ class PoolImplementation implements PgPool {
4242
}
4343

4444
@override
45-
Future<PgResult> execute(Object query,
46-
{Object? parameters, bool ignoreRows = false}) {
45+
Future<PgResult> execute(
46+
Object query, {
47+
Object? parameters,
48+
bool ignoreRows = false,
49+
QueryMode? queryMode,
50+
}) {
4751
return withConnection((connection) => connection.execute(
4852
query,
4953
parameters: parameters,
5054
ignoreRows: ignoreRows,
55+
queryMode: queryMode,
5156
));
5257
}
5358

@@ -161,12 +166,17 @@ class _PoolConnection implements PgConnection {
161166
}
162167

163168
@override
164-
Future<PgResult> execute(Object query,
165-
{Object? parameters, bool ignoreRows = false}) {
169+
Future<PgResult> execute(
170+
Object query, {
171+
Object? parameters,
172+
bool ignoreRows = false,
173+
QueryMode? queryMode,
174+
}) {
166175
return _connection.execute(
167176
query,
168177
parameters: parameters,
169178
ignoreRows: ignoreRows,
179+
queryMode: queryMode,
170180
);
171181
}
172182

test/v3_test.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,29 @@ void main() {
402402
expect(await connection.execute('SELECT id FROM t'), isEmpty);
403403
});
404404
});
405+
406+
group('Simple Query Protocol', () {
407+
test('single simple query', () async {
408+
final res = await connection.execute(
409+
"SELECT 'dart', 42, true, false, NULL",
410+
queryMode: QueryMode.simple,
411+
);
412+
expect(res, [
413+
['dart', 42, true, false, null]
414+
]);
415+
});
416+
417+
test('parameterized query throws', () async {
418+
await expectLater(
419+
() => connection.execute(
420+
r'SELECT 1',
421+
parameters: [PgTypedParameter(PgDataType.integer, 1)],
422+
queryMode: QueryMode.simple
423+
),
424+
_throwsPostgresException,
425+
);
426+
});
427+
});
405428
});
406429

407430
test('can inject transformer into connection', () async {

0 commit comments

Comments
 (0)