From e236d715fc253da432f4515f07c38b899f7514cb Mon Sep 17 00:00:00 2001 From: osaxma <46427323+osaxma@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:40:39 +0300 Subject: [PATCH 1/4] feat: allow explicit use of Simple Query Protocol --- lib/postgres_v3_experimental.dart | 6 +++++ lib/src/v3/connection.dart | 38 +++++++++++++++---------------- lib/src/v3/pool.dart | 18 +++++++++++---- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/lib/postgres_v3_experimental.dart b/lib/postgres_v3_experimental.dart index a4f0143f..5ff29a90 100644 --- a/lib/postgres_v3_experimental.dart +++ b/lib/postgres_v3_experimental.dart @@ -129,11 +129,17 @@ abstract class PgSession { /// optimization can be applied also depends on the parameters chosen, so /// there is no guarantee that the [PgResult] from a [ignoreRows] excution has /// no rows. + /// + /// When [useSimpleQueryProtocol] is set to true, the implementation will use + /// the Simple Query Protocol. Please note, a query with [parameters] cannot + /// be used with this protocol. + /// Future execute( Object /* String | PgSql */ query, { Object? /* List | Map */ parameters, bool ignoreRows = false, + bool useSimpleQueryProtocol = false, }); /// Closes this session, cleaning up resources and forbiding further calls to diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart index c11ca4ba..e1de3431 100644 --- a/lib/src/v3/connection.dart +++ b/lib/src/v3/connection.dart @@ -120,36 +120,37 @@ abstract class _PgSessionBase implements PgSession { Object query, { Object? parameters, bool ignoreRows = false, + bool useSimpleQueryProtocol = false, }) async { final description = InternalQueryDescription.wrap(query); final variables = description.bindParameters(parameters); - if (!ignoreRows || variables.isNotEmpty) { - // The simple query protocol does not support variables and returns rows - // as text. So when we need rows or parameters, we need an explicit prepare. - final prepared = await prepare(description); - try { - return await prepared.run(variables); - } finally { - await prepared.dispose(); - } - } else { + if (useSimpleQueryProtocol || (ignoreRows && !variables.isNotEmpty)) { // Great, we can just run a simple query. final controller = StreamController(); final items = []; final querySubscription = - _PgResultStreamSubscription.simpleQueryAndIgnoreRows( - description.transformedSql, - this, - controller, - controller.stream.listen(items.add), - ); + _PgResultStreamSubscription.simpleQueryProtocol( + description.transformedSql, + this, + controller, + controller.stream.listen(items.add), + ignoreRows); await querySubscription.asFuture(); await querySubscription.cancel(); return PgResult(items, await querySubscription.affectedRows, await querySubscription.schema); + } else { + // The simple query protocol does not support variables. So when we have + // parameters, we need an explicit prepare. + final prepared = await prepare(description); + try { + return await prepared.run(variables); + } finally { + await prepared.dispose(); + } } } @@ -496,9 +497,8 @@ class _PgResultStreamSubscription }); } - _PgResultStreamSubscription.simpleQueryAndIgnoreRows( - String sql, this.session, this._controller, this._source) - : ignoreRows = true { + _PgResultStreamSubscription.simpleQueryProtocol(String sql, this.session, + this._controller, this._source, this.ignoreRows) { session._withResource(() async { connection._pending = this; diff --git a/lib/src/v3/pool.dart b/lib/src/v3/pool.dart index f5657569..7743bd2f 100644 --- a/lib/src/v3/pool.dart +++ b/lib/src/v3/pool.dart @@ -42,12 +42,17 @@ class PoolImplementation implements PgPool { } @override - Future execute(Object query, - {Object? parameters, bool ignoreRows = false}) { + Future execute( + Object query, { + Object? parameters, + bool ignoreRows = false, + bool useSimpleQueryProtocol = false, + }) { return withConnection((connection) => connection.execute( query, parameters: parameters, ignoreRows: ignoreRows, + useSimpleQueryProtocol: useSimpleQueryProtocol, )); } @@ -161,12 +166,17 @@ class _PoolConnection implements PgConnection { } @override - Future execute(Object query, - {Object? parameters, bool ignoreRows = false}) { + Future execute( + Object query, { + Object? parameters, + bool ignoreRows = false, + bool useSimpleQueryProtocol = false, + }) { return _connection.execute( query, parameters: parameters, ignoreRows: ignoreRows, + useSimpleQueryProtocol: useSimpleQueryProtocol, ); } From f865bce69a1288f55958ebca9a893cc983926931 Mon Sep 17 00:00:00 2001 From: osaxma <46427323+osaxma@users.noreply.github.com> Date: Sat, 19 Aug 2023 21:52:56 +0300 Subject: [PATCH 2/4] format and use clearer logic --- lib/postgres_v3_experimental.dart | 1 - lib/src/v3/connection.dart | 16 ++++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/lib/postgres_v3_experimental.dart b/lib/postgres_v3_experimental.dart index 5ff29a90..d41eae01 100644 --- a/lib/postgres_v3_experimental.dart +++ b/lib/postgres_v3_experimental.dart @@ -133,7 +133,6 @@ abstract class PgSession { /// When [useSimpleQueryProtocol] is set to true, the implementation will use /// the Simple Query Protocol. Please note, a query with [parameters] cannot /// be used with this protocol. - /// Future execute( Object /* String | PgSql */ query, { Object? /* List | Map */ diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart index e1de3431..6dbebd02 100644 --- a/lib/src/v3/connection.dart +++ b/lib/src/v3/connection.dart @@ -125,18 +125,18 @@ abstract class _PgSessionBase implements PgSession { final description = InternalQueryDescription.wrap(query); final variables = description.bindParameters(parameters); - if (useSimpleQueryProtocol || (ignoreRows && !variables.isNotEmpty)) { + if (useSimpleQueryProtocol || (ignoreRows && variables.isEmpty)) { // Great, we can just run a simple query. final controller = StreamController(); final items = []; - final querySubscription = - _PgResultStreamSubscription.simpleQueryProtocol( - description.transformedSql, - this, - controller, - controller.stream.listen(items.add), - ignoreRows); + final querySubscription = _PgResultStreamSubscription.simpleQueryProtocol( + description.transformedSql, + this, + controller, + controller.stream.listen(items.add), + ignoreRows, + ); await querySubscription.asFuture(); await querySubscription.cancel(); From 05572af546249a3e421fd5b9a607373aa5601c70 Mon Sep 17 00:00:00 2001 From: osaxma <46427323+osaxma@users.noreply.github.com> Date: Sat, 26 Aug 2023 01:48:30 +0300 Subject: [PATCH 3/4] fix: text decoder for boolean parsing --- lib/src/text_codec.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/src/text_codec.dart b/lib/src/text_codec.dart index d99011ad..b2e58c1f 100644 --- a/lib/src/text_codec.dart +++ b/lib/src/text_codec.dart @@ -242,7 +242,12 @@ class PostgresTextDecoder { case PgDataType.double: return num.parse(asText) as T; case PgDataType.boolean: - return (asText == 'true') as T; + // In text data format when using simple query protocol, "true" & "false" + // are represented as `t` and `f`, respectively. + // we will check for both just in case + // TODO: should we check for other representations (e.g. `1`, `on`, `y`, + // and `yes`)? + return (asText == 't' || asText == 'true') as T; // We could list out all cases, but it's about 20 lines of code. // ignore: no_default_cases From 703c90917ba7351cd51fd653772d9bd6f63b024d Mon Sep 17 00:00:00 2001 From: osaxma <46427323+osaxma@users.noreply.github.com> Date: Sat, 26 Aug 2023 12:09:16 +0300 Subject: [PATCH 4/4] feat: Support Simple Query Protocol --- lib/postgres_v3_experimental.dart | 27 +++++++++++++++++++++++---- lib/src/v3/connection.dart | 20 +++++++++++++++++--- lib/src/v3/pool.dart | 8 ++++---- test/v3_test.dart | 23 +++++++++++++++++++++++ 4 files changed, 67 insertions(+), 11 deletions(-) diff --git a/lib/postgres_v3_experimental.dart b/lib/postgres_v3_experimental.dart index d41eae01..e34575e3 100644 --- a/lib/postgres_v3_experimental.dart +++ b/lib/postgres_v3_experimental.dart @@ -130,15 +130,16 @@ abstract class PgSession { /// there is no guarantee that the [PgResult] from a [ignoreRows] excution has /// no rows. /// - /// When [useSimpleQueryProtocol] is set to true, the implementation will use - /// the Simple Query Protocol. Please note, a query with [parameters] cannot - /// be used with this protocol. + /// [queryMode] is optional to override the default query execution mode that + /// is defined in [PgSessionSettings]. Unless necessary, always prefer using + /// [QueryMode.extended] which is the default value. For more information, + /// see [PgSessionSettings.queryMode] Future execute( Object /* String | PgSql */ query, { Object? /* List | Map */ parameters, bool ignoreRows = false, - bool useSimpleQueryProtocol = false, + QueryMode? queryMode, }); /// Closes this session, cleaning up resources and forbiding further calls to @@ -381,6 +382,15 @@ final class PgSessionSettings { /// [Streaming Replication Protocol]: https://www.postgresql.org/docs/current/protocol-replication.html final ReplicationMode replicationMode; + /// The Query Execution Mode + /// + /// The default value is [QueryMode.extended] which uses the Extended Query + /// Protocol. In certain cases, the Extended protocol cannot be used + /// (e.g. in replication mode or with proxies such as PGBouncer), hence the + /// the Simple one would be the only viable option. Unless necessary, always + /// prefer using [QueryMode.extended]. + final QueryMode queryMode; + PgSessionSettings({ this.connectTimeout, this.timeZone, @@ -388,6 +398,7 @@ final class PgSessionSettings { this.onBadSslCertificate, this.transformer, this.replicationMode = ReplicationMode.none, + this.queryMode = QueryMode.extended, }); } @@ -398,3 +409,11 @@ final class PgPoolSettings { this.maxConnectionCount, }); } + +/// Options for the Query Execution Mode +enum QueryMode { + /// Extended Query Protocol + extended, + /// Simple Query Protocol + simple, +} \ No newline at end of file diff --git a/lib/src/v3/connection.dart b/lib/src/v3/connection.dart index 6dbebd02..06f3a200 100644 --- a/lib/src/v3/connection.dart +++ b/lib/src/v3/connection.dart @@ -42,6 +42,7 @@ class _ResolvedSettings { final Encoding encoding; final ReplicationMode replicationMode; + final QueryMode queryMode; final StreamChannelTransformer? transformer; @@ -56,7 +57,8 @@ class _ResolvedSettings { timeZone = settings?.timeZone ?? 'UTC', encoding = settings?.encoding ?? utf8, transformer = settings?.transformer, - replicationMode = settings?.replicationMode ?? ReplicationMode.none; + replicationMode = settings?.replicationMode ?? ReplicationMode.none, + queryMode = settings?.queryMode ?? QueryMode.extended; bool onBadSslCertificate(X509Certificate certificate) { return settings?.onBadSslCertificate?.call(certificate) ?? false; @@ -120,12 +122,24 @@ abstract class _PgSessionBase implements PgSession { Object query, { Object? parameters, bool ignoreRows = false, - bool useSimpleQueryProtocol = false, + QueryMode? queryMode, }) async { final description = InternalQueryDescription.wrap(query); final variables = description.bindParameters(parameters); - if (useSimpleQueryProtocol || (ignoreRows && variables.isEmpty)) { + late final bool isSimple; + if (queryMode != null) { + isSimple = queryMode == QueryMode.simple; + } else { + isSimple = _connection._settings.queryMode == QueryMode.simple; + } + + if (isSimple && variables.isNotEmpty) { + throw PostgreSQLException('Parameterized queries are not supported when ' + 'using the Simple Query Protocol'); + } + + if (isSimple || (ignoreRows && variables.isEmpty)) { // Great, we can just run a simple query. final controller = StreamController(); final items = []; diff --git a/lib/src/v3/pool.dart b/lib/src/v3/pool.dart index 7743bd2f..f25f6cde 100644 --- a/lib/src/v3/pool.dart +++ b/lib/src/v3/pool.dart @@ -46,13 +46,13 @@ class PoolImplementation implements PgPool { Object query, { Object? parameters, bool ignoreRows = false, - bool useSimpleQueryProtocol = false, + QueryMode? queryMode, }) { return withConnection((connection) => connection.execute( query, parameters: parameters, ignoreRows: ignoreRows, - useSimpleQueryProtocol: useSimpleQueryProtocol, + queryMode: queryMode, )); } @@ -170,13 +170,13 @@ class _PoolConnection implements PgConnection { Object query, { Object? parameters, bool ignoreRows = false, - bool useSimpleQueryProtocol = false, + QueryMode? queryMode, }) { return _connection.execute( query, parameters: parameters, ignoreRows: ignoreRows, - useSimpleQueryProtocol: useSimpleQueryProtocol, + queryMode: queryMode, ); } diff --git a/test/v3_test.dart b/test/v3_test.dart index f00acc09..3c8bc944 100644 --- a/test/v3_test.dart +++ b/test/v3_test.dart @@ -402,6 +402,29 @@ void main() { expect(await connection.execute('SELECT id FROM t'), isEmpty); }); }); + + group('Simple Query Protocol', () { + test('single simple query', () async { + final res = await connection.execute( + "SELECT 'dart', 42, true, false, NULL", + queryMode: QueryMode.simple, + ); + expect(res, [ + ['dart', 42, true, false, null] + ]); + }); + + test('parameterized query throws', () async { + await expectLater( + () => connection.execute( + r'SELECT 1', + parameters: [PgTypedParameter(PgDataType.integer, 1)], + queryMode: QueryMode.simple + ), + _throwsPostgresException, + ); + }); + }); }); test('can inject transformer into connection', () async {