Skip to content

Commit 43345da

Browse files
authored
Support more parameters (#438)
1 parent 45e5e85 commit 43345da

File tree

7 files changed

+310
-47
lines changed

7 files changed

+310
-47
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
## 3.5.9
4+
5+
- Supporting multiple hosts in connection strings via comma-separated hosts or multiple `host` query parameters.
6+
- Unix socket connections via `host` query parameter (automatically detected when path contains `/`).
7+
- Supporting `user`/`username`, `password`, `database`, and `port` as query parameters in connection strings (override URL components).
8+
9+
310
## 3.5.8
411

512
- Upgraded SDK constraints and lints.

README.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,10 @@ Pool.withUrl(
7070
`postgresql://[userspec@][hostspec][:port][/dbname][?paramspec]`
7171

7272
- **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`)
73+
- **User**: `username` or `username:password` (can also be set via `user`/`username` and `password` query parameters)
74+
- **Host**: hostname or IP address (defaults to `localhost`). Supports multiple hosts via comma-separated list (`host1:5433,host2:5434`) or multiple `host` query parameters (`?host=host1:5433&host=host2:5434`)
75+
- **Port**: port number (defaults to `5432`, can be overridden via `port` query parameter)
76+
- **Database**: database name (defaults to `postgres`, can be overridden via `database` query parameter)
7777
- **Parameters**: query parameters (see below)
7878

7979
### Standard Parameters
@@ -85,6 +85,11 @@ These parameters are supported by `Connection.openFromUrl()`:
8585
| `application_name` | String | Sets the application name | `application_name=myapp` |
8686
| `client_encoding` | String | Character encoding | `UTF8`, `LATIN1` |
8787
| `connect_timeout` | Integer | Connection timeout in seconds | `connect_timeout=30` |
88+
| `database` | String | Database name (overrides URL path) | `database=mydb` |
89+
| `host` | String | Alternative host specification (supports Unix sockets) | `host=/var/run/postgresql`, `host=host1:5433` |
90+
| `password` | String | Password (overrides URL userspec) | `password=secret` |
91+
| `port` | Integer | Port number (overrides URL port) | `port=5433` |
92+
| `user` / `username` | String | Username (overrides URL userspec) | `user=myuser` |
8893
| `sslmode` | String | SSL mode | `disable`, `require`, `verify-ca`, `verify-full` |
8994
| `sslcert` | String | Path to client certificate | `sslcert=/path/to/cert.pem` |
9095
| `sslkey` | String | Path to client private key | `sslkey=/path/to/key.pem` |

lib/postgres.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,12 +236,12 @@ abstract class Connection implements Session, SessionExecutor {
236236
/// Open a new connection where the endpoint and the settings are encoded as an URL as
237237
/// `postgresql://[userspec@][hostspec][/dbname][?paramspec]`
238238
///
239-
/// Note: Only a single endpoint is supported.
239+
/// Note: When multiple endpoints are specified, only the first one is used.
240240
/// Note: Only a subset of settings can be set with parameters.
241241
static Future<Connection> openFromUrl(String connectionString) async {
242242
final parsed = parseConnectionString(connectionString);
243243
return open(
244-
parsed.endpoint,
244+
parsed.endpoints.first,
245245
settings: ConnectionSettings(
246246
applicationName: parsed.applicationName,
247247
connectTimeout: parsed.connectTimeout,

lib/src/connection_string.dart

Lines changed: 166 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'dart:io';
44
import '../postgres.dart';
55

66
({
7-
Endpoint endpoint,
7+
List<Endpoint> endpoints,
88
// standard parameters
99
String? applicationName,
1010
Duration? connectTimeout,
@@ -24,30 +24,83 @@ parseConnectionString(
2424
String connectionString, {
2525
bool enablePoolSettings = false,
2626
}) {
27-
final uri = Uri.parse(connectionString);
27+
// Pre-process connection string to extract comma-separated hosts from authority
28+
final preProcessed = _preprocessConnectionString(connectionString);
29+
30+
final uri = Uri.parse(preProcessed.uri);
2831

2932
if (uri.scheme != 'postgresql' && uri.scheme != 'postgres') {
3033
throw ArgumentError(
3134
'Invalid connection string scheme: ${uri.scheme}. Expected "postgresql" or "postgres".',
3235
);
3336
}
3437

35-
final host = uri.host.isEmpty ? 'localhost' : uri.host;
36-
final port = uri.port == 0 ? 5432 : uri.port;
37-
final database = uri.pathSegments.firstOrNull ?? 'postgres';
38-
final username = uri.userInfo.isEmpty ? null : _parseUsername(uri.userInfo);
39-
final password = uri.userInfo.isEmpty ? null : _parsePassword(uri.userInfo);
38+
final params = uri.queryParameters;
39+
40+
// Database: query parameter overrides path
41+
final database =
42+
params['database'] ?? uri.pathSegments.firstOrNull ?? 'postgres';
43+
44+
// Username: 'user' or 'username' query parameter overrides userInfo
45+
final username =
46+
params['user'] ??
47+
params['username'] ??
48+
(uri.userInfo.isEmpty ? null : _parseUsername(uri.userInfo));
49+
50+
// Password: query parameter overrides userInfo
51+
final password =
52+
params['password'] ??
53+
(uri.userInfo.isEmpty ? null : _parsePassword(uri.userInfo));
54+
55+
// Parse hosts
56+
final hosts = <({String host, int port, bool isUnixSocket})>[];
57+
58+
// Add hosts from authority (extracted during preprocessing)
59+
if (preProcessed.hosts.isNotEmpty) {
60+
hosts.addAll(preProcessed.hosts);
61+
} else if (uri.host.isNotEmpty) {
62+
// No comma-separated hosts, use standard URI host
63+
hosts.add((
64+
host: uri.host,
65+
port: uri.port == 0 ? 5432 : uri.port,
66+
isUnixSocket: false,
67+
));
68+
}
69+
70+
// Parse host query parameters
71+
final defaultPort = params['port'] != null
72+
? int.tryParse(params['port']!) ?? 5432
73+
: 5432;
74+
75+
if (uri.queryParametersAll.containsKey('host')) {
76+
final hostParams = uri.queryParametersAll['host'] ?? [];
77+
for (final hostParam in hostParams) {
78+
final parsed = _parseHostPort(hostParam, defaultPort: defaultPort);
79+
hosts.add(parsed);
80+
}
81+
}
82+
83+
// Default to localhost if no hosts specified
84+
if (hosts.isEmpty) {
85+
hosts.add((host: 'localhost', port: defaultPort, isUnixSocket: false));
86+
}
4087

4188
final validParams = {
4289
// Note: parameters here should be matched to https://www.postgresql.org/docs/current/libpq-connect.html
4390
'application_name',
4491
'client_encoding',
4592
'connect_timeout',
93+
'database',
94+
'host',
95+
'password',
96+
'port',
4697
'replication',
4798
'sslcert',
4899
'sslkey',
49100
'sslmode',
50101
'sslrootcert',
102+
'user',
103+
'username',
51104
// Note: some parameters are not part of the libpq-connect above
52105
'query_timeout',
53106
// Note: parameters here are only for pool-settings
@@ -59,7 +112,6 @@ parseConnectionString(
59112
],
60113
};
61114

62-
final params = uri.queryParameters;
63115
for (final key in params.keys) {
64116
if (!validParams.contains(key)) {
65117
throw ArgumentError('Unrecognized connection parameter: $key');
@@ -209,16 +261,21 @@ parseConnectionString(
209261
maxQueryCount = count;
210262
}
211263

212-
final endpoint = Endpoint(
213-
host: host,
214-
port: port,
215-
database: database,
216-
username: username,
217-
password: password,
218-
);
264+
final endpoints = hosts
265+
.map(
266+
(h) => Endpoint(
267+
host: h.host,
268+
port: h.port,
269+
database: database,
270+
username: username,
271+
password: password,
272+
isUnixSocket: h.isUnixSocket,
273+
),
274+
)
275+
.toList();
219276

220277
return (
221-
endpoint: endpoint,
278+
endpoints: endpoints,
222279
sslMode: sslMode,
223280
securityContext: securityContext,
224281
connectTimeout: connectTimeout,
@@ -249,6 +306,99 @@ String? _parsePassword(String userInfo) {
249306
return Uri.decodeComponent(userInfo.substring(colonIndex + 1));
250307
}
251308

309+
({String host, int port, bool isUnixSocket}) _parseHostPort(
310+
String hostPort, {
311+
required int defaultPort,
312+
}) {
313+
// Check if it's a Unix socket (contains '/')
314+
final isUnixSocket = hostPort.contains('/');
315+
316+
String host;
317+
int port;
318+
319+
if (isUnixSocket) {
320+
// Unix socket - don't parse for port (may have colons in filename)
321+
host = hostPort;
322+
port = defaultPort;
323+
} else {
324+
// Regular host - check for port after colon
325+
final colonIndex = hostPort.lastIndexOf(':');
326+
if (colonIndex != -1) {
327+
host = hostPort.substring(0, colonIndex);
328+
port = int.tryParse(hostPort.substring(colonIndex + 1)) ?? defaultPort;
329+
} else {
330+
host = hostPort;
331+
port = defaultPort;
332+
}
333+
}
334+
335+
return (host: host, port: port, isUnixSocket: isUnixSocket);
336+
}
337+
338+
({String uri, List<({String host, int port, bool isUnixSocket})> hosts})
339+
_preprocessConnectionString(String connectionString) {
340+
// Extract scheme
341+
final schemeEnd = connectionString.indexOf('://');
342+
if (schemeEnd == -1) {
343+
return (uri: connectionString, hosts: []);
344+
}
345+
346+
final scheme = connectionString.substring(0, schemeEnd + 3);
347+
final rest = connectionString.substring(schemeEnd + 3);
348+
349+
// Find where authority ends (at '/', '?', or end of string)
350+
final pathStart = rest.indexOf('/');
351+
final queryStart = rest.indexOf('?');
352+
353+
int authorityEnd;
354+
if (pathStart != -1 && queryStart != -1) {
355+
authorityEnd = pathStart < queryStart ? pathStart : queryStart;
356+
} else if (pathStart != -1) {
357+
authorityEnd = pathStart;
358+
} else if (queryStart != -1) {
359+
authorityEnd = queryStart;
360+
} else {
361+
authorityEnd = rest.length;
362+
}
363+
364+
final authority = rest.substring(0, authorityEnd);
365+
final remainder = rest.substring(authorityEnd);
366+
367+
// Check if authority contains comma-separated hosts
368+
if (!authority.contains(',')) {
369+
// No comma-separated hosts, return as-is
370+
return (uri: connectionString, hosts: []);
371+
}
372+
373+
// Split authority into userinfo and hostlist
374+
final atIndex = authority.indexOf('@');
375+
final String userInfo;
376+
final String hostlist;
377+
378+
if (atIndex != -1) {
379+
userInfo = authority.substring(0, atIndex + 1); // includes '@'
380+
hostlist = authority.substring(atIndex + 1);
381+
} else {
382+
userInfo = '';
383+
hostlist = authority;
384+
}
385+
386+
// Parse comma-separated hosts
387+
final hostParts = hostlist.split(',');
388+
final hosts = <({String host, int port, bool isUnixSocket})>[];
389+
390+
for (final hostPart in hostParts) {
391+
final parsed = _parseHostPort(hostPart.trim(), defaultPort: 5432);
392+
hosts.add(parsed);
393+
}
394+
395+
// Rebuild URI with only the first host for Uri.parse to work
396+
final firstHost = hosts.isNotEmpty ? hostParts[0] : '';
397+
final modifiedUri = '$scheme$userInfo$firstHost$remainder';
398+
399+
return (uri: modifiedUri, hosts: hosts);
400+
}
401+
252402
SecurityContext _createSecurityContext({
253403
String? certPath,
254404
String? keyPath,

lib/src/pool/pool_api.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,15 @@ abstract class Pool<L> implements Session, SessionExecutor {
7272
/// Creates a new pool where the endpoint and the settings are encoded as an URL as
7373
/// `postgresql://[userspec@][hostspec][/dbname][?paramspec]`
7474
///
75-
/// Note: Only a single endpoint is supported for now.
75+
/// Note: Multiple endpoints can be specified via comma-separated hosts or multiple host query parameters.
7676
/// Note: Only a subset of settings can be set with parameters.
7777
factory Pool.withUrl(String connectionString) {
7878
final parsed = parseConnectionString(
7979
connectionString,
8080
enablePoolSettings: true,
8181
);
8282
return PoolImplementation(
83-
roundRobinSelector([parsed.endpoint]),
83+
roundRobinSelector(parsed.endpoints),
8484
PoolSettings(
8585
applicationName: parsed.applicationName,
8686
connectTimeout: parsed.connectTimeout,

pubspec.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: postgres
22
description: PostgreSQL database driver. Supports binary protocol, connection pooling and statement reuse.
3-
version: 3.5.8
3+
version: 3.5.9
44
homepage: https://github.com/isoos/postgresql-dart
55
topics:
66
- sql

0 commit comments

Comments
 (0)