From 78134638e001747d747b0b00e97cda9dc20b72c8 Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 2 Oct 2025 14:35:40 +0200 Subject: [PATCH 1/2] Supporting more URL-based connection-string parameters (mostly for pool). --- CHANGELOG.md | 1 + lib/postgres.dart | 1 + lib/src/connection_string.dart | 93 ++++++++++++++++++++-- lib/src/pool/pool_api.dart | 10 ++- test/connection_string_test.dart | 129 +++++++++++++++++++++++++++++++ 5 files changed, 227 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26925dc..2c53f2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 3.5.8 - Upgraded SDK constraints and lints. +- Supporting more URL-based connection-string parameters (mostly for pool). ## 3.5.7. diff --git a/lib/postgres.dart b/lib/postgres.dart index 0c27bab..6c2d358 100644 --- a/lib/postgres.dart +++ b/lib/postgres.dart @@ -247,6 +247,7 @@ abstract class Connection implements Session, SessionExecutor { connectTimeout: parsed.connectTimeout, encoding: parsed.encoding, replicationMode: parsed.replicationMode, + queryTimeout: parsed.queryTimeout, securityContext: parsed.securityContext, sslMode: parsed.sslMode, ), diff --git a/lib/src/connection_string.dart b/lib/src/connection_string.dart index 8ea8952..e212695 100644 --- a/lib/src/connection_string.dart +++ b/lib/src/connection_string.dart @@ -5,14 +5,25 @@ import '../postgres.dart'; ({ Endpoint endpoint, + // standard parameters String? applicationName, Duration? connectTimeout, Encoding? encoding, ReplicationMode? replicationMode, SecurityContext? securityContext, SslMode? sslMode, + // non-standard parameters + Duration? queryTimeout, + // pool parameters + Duration? maxConnectionAge, + int? maxConnectionCount, + Duration? maxSessionUse, + int? maxQueryCount, }) -parseConnectionString(String connectionString) { +parseConnectionString( + String connectionString, { + bool enablePoolSettings = false, +}) { final uri = Uri.parse(connectionString); if (uri.scheme != 'postgresql' && uri.scheme != 'postgres') { @@ -28,14 +39,24 @@ parseConnectionString(String connectionString) { final password = uri.userInfo.isEmpty ? null : _parsePassword(uri.userInfo); final validParams = { - 'sslmode', - 'sslcert', - 'sslkey', - 'sslrootcert', - 'connect_timeout', + // Note: parameters here should be matched to https://www.postgresql.org/docs/current/libpq-connect.html 'application_name', 'client_encoding', + 'connect_timeout', 'replication', + 'sslcert', + 'sslkey', + 'sslmode', + 'sslrootcert', + // Note: some parameters are not part of the libpq-connect above + 'query_timeout', + // Note: parameters here are only for pool-settings + if (enablePoolSettings) ...[ + 'max_connection_age', + 'max_connection_count', + 'max_session_use', + 'max_query_count', + ], }; final params = uri.queryParameters; @@ -133,6 +154,61 @@ parseConnectionString(String connectionString) { } } + Duration? queryTimeout; + if (params.containsKey('query_timeout')) { + final timeoutSeconds = int.tryParse(params['query_timeout']!); + if (timeoutSeconds == null || timeoutSeconds <= 0) { + throw ArgumentError( + 'Invalid query_timeout value: ${params['query_timeout']}. Expected positive integer.', + ); + } + queryTimeout = Duration(seconds: timeoutSeconds); + } + + Duration? maxConnectionAge; + if (enablePoolSettings && params.containsKey('max_connection_age')) { + final ageSeconds = int.tryParse(params['max_connection_age']!); + if (ageSeconds == null || ageSeconds <= 0) { + throw ArgumentError( + 'Invalid max_connection_age value: ${params['max_connection_age']}. Expected positive integer.', + ); + } + maxConnectionAge = Duration(seconds: ageSeconds); + } + + int? maxConnectionCount; + if (enablePoolSettings && params.containsKey('max_connection_count')) { + final count = int.tryParse(params['max_connection_count']!); + if (count == null || count <= 0) { + throw ArgumentError( + 'Invalid max_connection_count value: ${params['max_connection_count']}. Expected positive integer.', + ); + } + maxConnectionCount = count; + } + + Duration? maxSessionUse; + if (enablePoolSettings && params.containsKey('max_session_use')) { + final sessionSeconds = int.tryParse(params['max_session_use']!); + if (sessionSeconds == null || sessionSeconds <= 0) { + throw ArgumentError( + 'Invalid max_session_use value: ${params['max_session_use']}. Expected positive integer.', + ); + } + maxSessionUse = Duration(seconds: sessionSeconds); + } + + int? maxQueryCount; + if (enablePoolSettings && params.containsKey('max_query_count')) { + final count = int.tryParse(params['max_query_count']!); + if (count == null || count <= 0) { + throw ArgumentError( + 'Invalid max_query_count value: ${params['max_query_count']}. Expected positive integer.', + ); + } + maxQueryCount = count; + } + final endpoint = Endpoint( host: host, port: port, @@ -149,6 +225,11 @@ parseConnectionString(String connectionString) { applicationName: applicationName, encoding: encoding, replicationMode: replicationMode, + queryTimeout: queryTimeout, + maxConnectionAge: maxConnectionAge, + maxConnectionCount: maxConnectionCount, + maxSessionUse: maxSessionUse, + maxQueryCount: maxQueryCount, ); } diff --git a/lib/src/pool/pool_api.dart b/lib/src/pool/pool_api.dart index 67a0a06..640b8a7 100644 --- a/lib/src/pool/pool_api.dart +++ b/lib/src/pool/pool_api.dart @@ -75,16 +75,24 @@ abstract class Pool implements Session, SessionExecutor { /// Note: Only a single endpoint is supported for now. /// Note: Only a subset of settings can be set with parameters. factory Pool.withUrl(String connectionString) { - final parsed = parseConnectionString(connectionString); + final parsed = parseConnectionString( + connectionString, + enablePoolSettings: true, + ); return PoolImplementation( roundRobinSelector([parsed.endpoint]), PoolSettings( applicationName: parsed.applicationName, connectTimeout: parsed.connectTimeout, encoding: parsed.encoding, + queryTimeout: parsed.queryTimeout, replicationMode: parsed.replicationMode, securityContext: parsed.securityContext, sslMode: parsed.sslMode, + maxConnectionAge: parsed.maxConnectionAge, + maxConnectionCount: parsed.maxConnectionCount, + maxSessionUse: parsed.maxSessionUse, + maxQueryCount: parsed.maxQueryCount, ), ); } diff --git a/test/connection_string_test.dart b/test/connection_string_test.dart index a20cd42..ef6a7d4 100644 --- a/test/connection_string_test.dart +++ b/test/connection_string_test.dart @@ -387,5 +387,134 @@ void main() { expect(result.applicationName, equals('')); }); }); + + group('Query timeout and pool parameters', () { + test('query_timeout parameter', () { + final result = parseConnectionString( + 'postgresql://localhost/test?query_timeout=45', + ); + expect(result.queryTimeout, equals(Duration(seconds: 45))); + }); + + test('query_timeout validation', () { + expect( + () => parseConnectionString( + 'postgresql://localhost/test?query_timeout=invalid', + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid query_timeout'), + ), + ), + ); + expect( + () => parseConnectionString( + 'postgresql://localhost/test?query_timeout=0', + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid query_timeout'), + ), + ), + ); + }); + + test('pool parameters with enablePoolSettings', () { + final result = parseConnectionString( + 'postgresql://localhost/test?max_connection_age=3600&max_connection_count=10&max_session_use=7200&max_query_count=1000', + enablePoolSettings: true, + ); + expect(result.maxConnectionAge, equals(Duration(seconds: 3600))); + expect(result.maxConnectionCount, equals(10)); + expect(result.maxSessionUse, equals(Duration(seconds: 7200))); + expect(result.maxQueryCount, equals(1000)); + }); + + test('pool parameters rejected without enablePoolSettings', () { + expect( + () => parseConnectionString( + 'postgresql://localhost/test?max_connection_age=3600', + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Unrecognized connection parameter'), + ), + ), + ); + }); + + test('pool parameter validation', () { + expect( + () => parseConnectionString( + 'postgresql://localhost/test?max_connection_age=0', + enablePoolSettings: true, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid max_connection_age'), + ), + ), + ); + expect( + () => parseConnectionString( + 'postgresql://localhost/test?max_connection_count=invalid', + enablePoolSettings: true, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid max_connection_count'), + ), + ), + ); + expect( + () => parseConnectionString( + 'postgresql://localhost/test?max_session_use=-5', + enablePoolSettings: true, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid max_session_use'), + ), + ), + ); + expect( + () => parseConnectionString( + 'postgresql://localhost/test?max_query_count=0', + enablePoolSettings: true, + ), + throwsA( + isA().having( + (e) => e.message, + 'message', + contains('Invalid max_query_count'), + ), + ), + ); + }); + + test('all timeout and pool parameters combined', () { + final result = parseConnectionString( + 'postgresql://localhost/test?query_timeout=30&max_connection_age=3600&max_connection_count=20&max_session_use=7200&max_query_count=500', + enablePoolSettings: true, + ); + expect(result.queryTimeout, equals(Duration(seconds: 30))); + expect(result.maxConnectionAge, equals(Duration(seconds: 3600))); + expect(result.maxConnectionCount, equals(20)); + expect(result.maxSessionUse, equals(Duration(seconds: 7200))); + expect(result.maxQueryCount, equals(500)); + }); + }); }); } From e0b03ae12760a5e794c2ef2df6a58b3b2d4637aa Mon Sep 17 00:00:00 2001 From: Istvan Soos Date: Thu, 2 Oct 2025 15:33:25 +0200 Subject: [PATCH 2/2] updated readme with connection parameters --- README.md | 55 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/README.md b/README.md index 4e89022..9aa941b 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,61 @@ Execute queries in a transaction: See the API documentation: https://pub.dev/documentation/postgres/latest/ +## Connection string URLs + +The package supports connection strings for both single connections and connection pools: + +```dart +await Connection.openFromUrl('postgresql://localhost/mydb'); +await Connection.openFromUrl( + 'postgresql://user:pass@db.example.com:5432/production?sslmode=verify-full' +); +await Connection.openFromUrl( + 'postgresql://localhost/mydb?connect_timeout=10&query_timeout=60' +); +Pool.withUrl( + 'postgresql://localhost/mydb?max_connection_count=10&max_connection_age=3600' +); +``` + +### URL Format + +`postgresql://[userspec@][hostspec][:port][/dbname][?paramspec]` + +- **Scheme**: `postgresql://` or `postgres://` +- **User**: `username` or `username:password` +- **Host**: hostname or IP address (defaults to `localhost`) +- **Port**: port number (defaults to `5432`) +- **Database**: database name (defaults to `postgres`) +- **Parameters**: query parameters (see below) + +### Standard Parameters + +These parameters are supported by `Connection.openFromUrl()`: + +| Parameter | Type | Description | Example Values | +|-----------|------|-------------|----------------| +| `application_name` | String | Sets the application name | `application_name=myapp` | +| `client_encoding` | String | Character encoding | `UTF8`, `LATIN1` | +| `connect_timeout` | Integer | Connection timeout in seconds | `connect_timeout=30` | +| `sslmode` | String | SSL mode | `disable`, `require`, `verify-ca`, `verify-full` | +| `sslcert` | String | Path to client certificate | `sslcert=/path/to/cert.pem` | +| `sslkey` | String | Path to client private key | `sslkey=/path/to/key.pem` | +| `sslrootcert` | String | Path to root certificate | `sslrootcert=/path/to/ca.pem` | +| `replication` | String | Replication mode | `database` (logical), `true`/`physical`, `false`/`no_select` (none) | +| `query_timeout` | Integer | Query timeout in seconds | `query_timeout=300` | + +### Pool-Specific Parameters + +These additional parameters are supported by `Pool.withUrl()`: + +| Parameter | Type | Description | Example Values | +|-----------|------|-------------|----------------| +| `max_connection_count` | Integer | Maximum number of concurrent connections | `max_connection_count=20` | +| `max_connection_age` | Integer | Maximum connection lifetime in seconds | `max_connection_age=3600` | +| `max_session_use` | Integer | Maximum session duration in seconds | `max_session_use=600` | +| `max_query_count` | Integer | Maximum queries per connection | `max_query_count=1000` | + ## Connection pooling The library supports connection pooling (and masking the connection pool as