Skip to content

Commit 9a96878

Browse files
authored
Supporting more URL-based connection-string parameters (mostly for pool). (#435)
1 parent 2439394 commit 9a96878

File tree

6 files changed

+282
-7
lines changed

6 files changed

+282
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
## 3.5.8
44

55
- Upgraded SDK constraints and lints.
6+
- Supporting more URL-based connection-string parameters (mostly for pool).
67

78
## 3.5.7.
89

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,61 @@ Execute queries in a transaction:
4848

4949
See the API documentation: https://pub.dev/documentation/postgres/latest/
5050

51+
## Connection string URLs
52+
53+
The package supports connection strings for both single connections and connection pools:
54+
55+
```dart
56+
await Connection.openFromUrl('postgresql://localhost/mydb');
57+
await Connection.openFromUrl(
58+
'postgresql://user:[email protected]:5432/production?sslmode=verify-full'
59+
);
60+
await Connection.openFromUrl(
61+
'postgresql://localhost/mydb?connect_timeout=10&query_timeout=60'
62+
);
63+
Pool.withUrl(
64+
'postgresql://localhost/mydb?max_connection_count=10&max_connection_age=3600'
65+
);
66+
```
67+
68+
### URL Format
69+
70+
`postgresql://[userspec@][hostspec][:port][/dbname][?paramspec]`
71+
72+
- **Scheme**: `postgresql://` or `postgres://`
73+
- **User**: `username` or `username:password`
74+
- **Host**: hostname or IP address (defaults to `localhost`)
75+
- **Port**: port number (defaults to `5432`)
76+
- **Database**: database name (defaults to `postgres`)
77+
- **Parameters**: query parameters (see below)
78+
79+
### Standard Parameters
80+
81+
These parameters are supported by `Connection.openFromUrl()`:
82+
83+
| Parameter | Type | Description | Example Values |
84+
|-----------|------|-------------|----------------|
85+
| `application_name` | String | Sets the application name | `application_name=myapp` |
86+
| `client_encoding` | String | Character encoding | `UTF8`, `LATIN1` |
87+
| `connect_timeout` | Integer | Connection timeout in seconds | `connect_timeout=30` |
88+
| `sslmode` | String | SSL mode | `disable`, `require`, `verify-ca`, `verify-full` |
89+
| `sslcert` | String | Path to client certificate | `sslcert=/path/to/cert.pem` |
90+
| `sslkey` | String | Path to client private key | `sslkey=/path/to/key.pem` |
91+
| `sslrootcert` | String | Path to root certificate | `sslrootcert=/path/to/ca.pem` |
92+
| `replication` | String | Replication mode | `database` (logical), `true`/`physical`, `false`/`no_select` (none) |
93+
| `query_timeout` | Integer | Query timeout in seconds | `query_timeout=300` |
94+
95+
### Pool-Specific Parameters
96+
97+
These additional parameters are supported by `Pool.withUrl()`:
98+
99+
| Parameter | Type | Description | Example Values |
100+
|-----------|------|-------------|----------------|
101+
| `max_connection_count` | Integer | Maximum number of concurrent connections | `max_connection_count=20` |
102+
| `max_connection_age` | Integer | Maximum connection lifetime in seconds | `max_connection_age=3600` |
103+
| `max_session_use` | Integer | Maximum session duration in seconds | `max_session_use=600` |
104+
| `max_query_count` | Integer | Maximum queries per connection | `max_query_count=1000` |
105+
51106
## Connection pooling
52107

53108
The library supports connection pooling (and masking the connection pool as

lib/postgres.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,7 @@ abstract class Connection implements Session, SessionExecutor {
247247
connectTimeout: parsed.connectTimeout,
248248
encoding: parsed.encoding,
249249
replicationMode: parsed.replicationMode,
250+
queryTimeout: parsed.queryTimeout,
250251
securityContext: parsed.securityContext,
251252
sslMode: parsed.sslMode,
252253
),

lib/src/connection_string.dart

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,25 @@ import '../postgres.dart';
55

66
({
77
Endpoint endpoint,
8+
// standard parameters
89
String? applicationName,
910
Duration? connectTimeout,
1011
Encoding? encoding,
1112
ReplicationMode? replicationMode,
1213
SecurityContext? securityContext,
1314
SslMode? sslMode,
15+
// non-standard parameters
16+
Duration? queryTimeout,
17+
// pool parameters
18+
Duration? maxConnectionAge,
19+
int? maxConnectionCount,
20+
Duration? maxSessionUse,
21+
int? maxQueryCount,
1422
})
15-
parseConnectionString(String connectionString) {
23+
parseConnectionString(
24+
String connectionString, {
25+
bool enablePoolSettings = false,
26+
}) {
1627
final uri = Uri.parse(connectionString);
1728

1829
if (uri.scheme != 'postgresql' && uri.scheme != 'postgres') {
@@ -28,14 +39,24 @@ parseConnectionString(String connectionString) {
2839
final password = uri.userInfo.isEmpty ? null : _parsePassword(uri.userInfo);
2940

3041
final validParams = {
31-
'sslmode',
32-
'sslcert',
33-
'sslkey',
34-
'sslrootcert',
35-
'connect_timeout',
42+
// Note: parameters here should be matched to https://www.postgresql.org/docs/current/libpq-connect.html
3643
'application_name',
3744
'client_encoding',
45+
'connect_timeout',
3846
'replication',
47+
'sslcert',
48+
'sslkey',
49+
'sslmode',
50+
'sslrootcert',
51+
// Note: some parameters are not part of the libpq-connect above
52+
'query_timeout',
53+
// Note: parameters here are only for pool-settings
54+
if (enablePoolSettings) ...[
55+
'max_connection_age',
56+
'max_connection_count',
57+
'max_session_use',
58+
'max_query_count',
59+
],
3960
};
4061

4162
final params = uri.queryParameters;
@@ -133,6 +154,61 @@ parseConnectionString(String connectionString) {
133154
}
134155
}
135156

157+
Duration? queryTimeout;
158+
if (params.containsKey('query_timeout')) {
159+
final timeoutSeconds = int.tryParse(params['query_timeout']!);
160+
if (timeoutSeconds == null || timeoutSeconds <= 0) {
161+
throw ArgumentError(
162+
'Invalid query_timeout value: ${params['query_timeout']}. Expected positive integer.',
163+
);
164+
}
165+
queryTimeout = Duration(seconds: timeoutSeconds);
166+
}
167+
168+
Duration? maxConnectionAge;
169+
if (enablePoolSettings && params.containsKey('max_connection_age')) {
170+
final ageSeconds = int.tryParse(params['max_connection_age']!);
171+
if (ageSeconds == null || ageSeconds <= 0) {
172+
throw ArgumentError(
173+
'Invalid max_connection_age value: ${params['max_connection_age']}. Expected positive integer.',
174+
);
175+
}
176+
maxConnectionAge = Duration(seconds: ageSeconds);
177+
}
178+
179+
int? maxConnectionCount;
180+
if (enablePoolSettings && params.containsKey('max_connection_count')) {
181+
final count = int.tryParse(params['max_connection_count']!);
182+
if (count == null || count <= 0) {
183+
throw ArgumentError(
184+
'Invalid max_connection_count value: ${params['max_connection_count']}. Expected positive integer.',
185+
);
186+
}
187+
maxConnectionCount = count;
188+
}
189+
190+
Duration? maxSessionUse;
191+
if (enablePoolSettings && params.containsKey('max_session_use')) {
192+
final sessionSeconds = int.tryParse(params['max_session_use']!);
193+
if (sessionSeconds == null || sessionSeconds <= 0) {
194+
throw ArgumentError(
195+
'Invalid max_session_use value: ${params['max_session_use']}. Expected positive integer.',
196+
);
197+
}
198+
maxSessionUse = Duration(seconds: sessionSeconds);
199+
}
200+
201+
int? maxQueryCount;
202+
if (enablePoolSettings && params.containsKey('max_query_count')) {
203+
final count = int.tryParse(params['max_query_count']!);
204+
if (count == null || count <= 0) {
205+
throw ArgumentError(
206+
'Invalid max_query_count value: ${params['max_query_count']}. Expected positive integer.',
207+
);
208+
}
209+
maxQueryCount = count;
210+
}
211+
136212
final endpoint = Endpoint(
137213
host: host,
138214
port: port,
@@ -149,6 +225,11 @@ parseConnectionString(String connectionString) {
149225
applicationName: applicationName,
150226
encoding: encoding,
151227
replicationMode: replicationMode,
228+
queryTimeout: queryTimeout,
229+
maxConnectionAge: maxConnectionAge,
230+
maxConnectionCount: maxConnectionCount,
231+
maxSessionUse: maxSessionUse,
232+
maxQueryCount: maxQueryCount,
152233
);
153234
}
154235

lib/src/pool/pool_api.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,16 +75,24 @@ abstract class Pool<L> implements Session, SessionExecutor {
7575
/// Note: Only a single endpoint is supported for now.
7676
/// Note: Only a subset of settings can be set with parameters.
7777
factory Pool.withUrl(String connectionString) {
78-
final parsed = parseConnectionString(connectionString);
78+
final parsed = parseConnectionString(
79+
connectionString,
80+
enablePoolSettings: true,
81+
);
7982
return PoolImplementation(
8083
roundRobinSelector([parsed.endpoint]),
8184
PoolSettings(
8285
applicationName: parsed.applicationName,
8386
connectTimeout: parsed.connectTimeout,
8487
encoding: parsed.encoding,
88+
queryTimeout: parsed.queryTimeout,
8589
replicationMode: parsed.replicationMode,
8690
securityContext: parsed.securityContext,
8791
sslMode: parsed.sslMode,
92+
maxConnectionAge: parsed.maxConnectionAge,
93+
maxConnectionCount: parsed.maxConnectionCount,
94+
maxSessionUse: parsed.maxSessionUse,
95+
maxQueryCount: parsed.maxQueryCount,
8896
),
8997
);
9098
}

test/connection_string_test.dart

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,5 +387,134 @@ void main() {
387387
expect(result.applicationName, equals(''));
388388
});
389389
});
390+
391+
group('Query timeout and pool parameters', () {
392+
test('query_timeout parameter', () {
393+
final result = parseConnectionString(
394+
'postgresql://localhost/test?query_timeout=45',
395+
);
396+
expect(result.queryTimeout, equals(Duration(seconds: 45)));
397+
});
398+
399+
test('query_timeout validation', () {
400+
expect(
401+
() => parseConnectionString(
402+
'postgresql://localhost/test?query_timeout=invalid',
403+
),
404+
throwsA(
405+
isA<ArgumentError>().having(
406+
(e) => e.message,
407+
'message',
408+
contains('Invalid query_timeout'),
409+
),
410+
),
411+
);
412+
expect(
413+
() => parseConnectionString(
414+
'postgresql://localhost/test?query_timeout=0',
415+
),
416+
throwsA(
417+
isA<ArgumentError>().having(
418+
(e) => e.message,
419+
'message',
420+
contains('Invalid query_timeout'),
421+
),
422+
),
423+
);
424+
});
425+
426+
test('pool parameters with enablePoolSettings', () {
427+
final result = parseConnectionString(
428+
'postgresql://localhost/test?max_connection_age=3600&max_connection_count=10&max_session_use=7200&max_query_count=1000',
429+
enablePoolSettings: true,
430+
);
431+
expect(result.maxConnectionAge, equals(Duration(seconds: 3600)));
432+
expect(result.maxConnectionCount, equals(10));
433+
expect(result.maxSessionUse, equals(Duration(seconds: 7200)));
434+
expect(result.maxQueryCount, equals(1000));
435+
});
436+
437+
test('pool parameters rejected without enablePoolSettings', () {
438+
expect(
439+
() => parseConnectionString(
440+
'postgresql://localhost/test?max_connection_age=3600',
441+
),
442+
throwsA(
443+
isA<ArgumentError>().having(
444+
(e) => e.message,
445+
'message',
446+
contains('Unrecognized connection parameter'),
447+
),
448+
),
449+
);
450+
});
451+
452+
test('pool parameter validation', () {
453+
expect(
454+
() => parseConnectionString(
455+
'postgresql://localhost/test?max_connection_age=0',
456+
enablePoolSettings: true,
457+
),
458+
throwsA(
459+
isA<ArgumentError>().having(
460+
(e) => e.message,
461+
'message',
462+
contains('Invalid max_connection_age'),
463+
),
464+
),
465+
);
466+
expect(
467+
() => parseConnectionString(
468+
'postgresql://localhost/test?max_connection_count=invalid',
469+
enablePoolSettings: true,
470+
),
471+
throwsA(
472+
isA<ArgumentError>().having(
473+
(e) => e.message,
474+
'message',
475+
contains('Invalid max_connection_count'),
476+
),
477+
),
478+
);
479+
expect(
480+
() => parseConnectionString(
481+
'postgresql://localhost/test?max_session_use=-5',
482+
enablePoolSettings: true,
483+
),
484+
throwsA(
485+
isA<ArgumentError>().having(
486+
(e) => e.message,
487+
'message',
488+
contains('Invalid max_session_use'),
489+
),
490+
),
491+
);
492+
expect(
493+
() => parseConnectionString(
494+
'postgresql://localhost/test?max_query_count=0',
495+
enablePoolSettings: true,
496+
),
497+
throwsA(
498+
isA<ArgumentError>().having(
499+
(e) => e.message,
500+
'message',
501+
contains('Invalid max_query_count'),
502+
),
503+
),
504+
);
505+
});
506+
507+
test('all timeout and pool parameters combined', () {
508+
final result = parseConnectionString(
509+
'postgresql://localhost/test?query_timeout=30&max_connection_age=3600&max_connection_count=20&max_session_use=7200&max_query_count=500',
510+
enablePoolSettings: true,
511+
);
512+
expect(result.queryTimeout, equals(Duration(seconds: 30)));
513+
expect(result.maxConnectionAge, equals(Duration(seconds: 3600)));
514+
expect(result.maxConnectionCount, equals(20));
515+
expect(result.maxSessionUse, equals(Duration(seconds: 7200)));
516+
expect(result.maxQueryCount, equals(500));
517+
});
518+
});
390519
});
391520
}

0 commit comments

Comments
 (0)