Skip to content

Commit 3da76d1

Browse files
authored
Support connection strings (#434)
1 parent f79e5f8 commit 3da76d1

File tree

8 files changed

+593
-5
lines changed

8 files changed

+593
-5
lines changed

CHANGELOG.md

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

3+
## 3.5.7.
4+
5+
- Supporting URL-based connection-string specification in `Connection.openFromUrl` and `Pool.withUrl`. (Note: feature and supported settings is considered experimental.)
6+
37
## 3.5.6
48

59
- Accept `null` values as part of the binary List encodings.

lib/postgres.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'dart:io';
44

55
import 'package:collection/collection.dart';
66
import 'package:meta/meta.dart';
7+
import 'package:postgres/src/connection_string.dart';
78
import 'package:postgres/src/v3/connection_info.dart';
89
import 'package:stream_channel/stream_channel.dart';
910

@@ -230,6 +231,26 @@ abstract class Connection implements Session, SessionExecutor {
230231
connectionSettings: settings);
231232
}
232233

234+
/// Open a new connection where the endpoint and the settings are encoded as an URL as
235+
/// `postgresql://[userspec@][hostspec][/dbname][?paramspec]`
236+
///
237+
/// Note: Only a single endpoint is supported.
238+
/// Note: Only a subset of settings can be set with parameters.
239+
static Future<Connection> openFromUrl(String connectionString) async {
240+
final parsed = parseConnectionString(connectionString);
241+
return open(
242+
parsed.endpoint,
243+
settings: ConnectionSettings(
244+
applicationName: parsed.applicationName,
245+
connectTimeout: parsed.connectTimeout,
246+
encoding: parsed.encoding,
247+
replicationMode: parsed.replicationMode,
248+
securityContext: parsed.securityContext,
249+
sslMode: parsed.sslMode,
250+
),
251+
);
252+
}
253+
233254
ConnectionInfo get info;
234255

235256
Channels get channels;

lib/src/connection_string.dart

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import 'dart:convert';
2+
import 'dart:io';
3+
4+
import '../postgres.dart';
5+
6+
({
7+
Endpoint endpoint,
8+
String? applicationName,
9+
Duration? connectTimeout,
10+
Encoding? encoding,
11+
ReplicationMode? replicationMode,
12+
SecurityContext? securityContext,
13+
SslMode? sslMode,
14+
}) parseConnectionString(String connectionString) {
15+
final uri = Uri.parse(connectionString);
16+
17+
if (uri.scheme != 'postgresql' && uri.scheme != 'postgres') {
18+
throw ArgumentError(
19+
'Invalid connection string scheme: ${uri.scheme}. Expected "postgresql" or "postgres".');
20+
}
21+
22+
final host = uri.host.isEmpty ? 'localhost' : uri.host;
23+
final port = uri.port == 0 ? 5432 : uri.port;
24+
final database = uri.pathSegments.firstOrNull ?? 'postgres';
25+
final username = uri.userInfo.isEmpty ? null : _parseUsername(uri.userInfo);
26+
final password = uri.userInfo.isEmpty ? null : _parsePassword(uri.userInfo);
27+
28+
final validParams = {
29+
'sslmode',
30+
'sslcert',
31+
'sslkey',
32+
'sslrootcert',
33+
'connect_timeout',
34+
'application_name',
35+
'client_encoding',
36+
'replication'
37+
};
38+
39+
final params = uri.queryParameters;
40+
for (final key in params.keys) {
41+
if (!validParams.contains(key)) {
42+
throw ArgumentError('Unrecognized connection parameter: $key');
43+
}
44+
}
45+
46+
SslMode? sslMode;
47+
if (params.containsKey('sslmode')) {
48+
switch (params['sslmode']) {
49+
case 'disable':
50+
sslMode = SslMode.disable;
51+
break;
52+
case 'require':
53+
sslMode = SslMode.require;
54+
break;
55+
case 'verify-ca':
56+
case 'verify-full':
57+
sslMode = SslMode.verifyFull;
58+
break;
59+
default:
60+
throw ArgumentError(
61+
'Invalid sslmode value: ${params['sslmode']}. Expected: disable, require, verify-ca, verify-full');
62+
}
63+
}
64+
65+
SecurityContext? securityContext;
66+
if (params.containsKey('sslcert') ||
67+
params.containsKey('sslkey') ||
68+
params.containsKey('sslrootcert')) {
69+
try {
70+
securityContext = _createSecurityContext(
71+
certPath: params['sslcert'],
72+
keyPath: params['sslkey'],
73+
caPath: params['sslrootcert'],
74+
);
75+
} catch (e) {
76+
// re-throw with more context about connection string parsing
77+
throw ArgumentError('SSL configuration error in connection string: $e');
78+
}
79+
}
80+
81+
Duration? connectTimeout;
82+
if (params.containsKey('connect_timeout')) {
83+
final timeoutSeconds = int.tryParse(params['connect_timeout']!);
84+
if (timeoutSeconds == null || timeoutSeconds <= 0) {
85+
throw ArgumentError(
86+
'Invalid connect_timeout value: ${params['connect_timeout']}. Expected positive integer.');
87+
}
88+
connectTimeout = Duration(seconds: timeoutSeconds);
89+
}
90+
91+
final applicationName = params['application_name'];
92+
93+
Encoding? encoding;
94+
if (params.containsKey('client_encoding')) {
95+
switch (params['client_encoding']?.toUpperCase()) {
96+
case 'UTF8':
97+
case 'UTF-8':
98+
encoding = utf8;
99+
break;
100+
case 'LATIN1':
101+
case 'ISO-8859-1':
102+
encoding = latin1;
103+
break;
104+
default:
105+
throw ArgumentError(
106+
'Unsupported client_encoding: ${params['client_encoding']}. Supported: UTF8, LATIN1');
107+
}
108+
}
109+
110+
ReplicationMode? replicationMode;
111+
if (params.containsKey('replication')) {
112+
switch (params['replication']) {
113+
case 'database':
114+
replicationMode = ReplicationMode.logical;
115+
break;
116+
case 'true':
117+
case 'physical':
118+
replicationMode = ReplicationMode.physical;
119+
break;
120+
case 'false':
121+
case 'no_select':
122+
replicationMode = ReplicationMode.none;
123+
break;
124+
default:
125+
throw ArgumentError(
126+
'Invalid replication value: ${params['replication']}. Expected: database, true, physical, false, no_select');
127+
}
128+
}
129+
130+
final endpoint = Endpoint(
131+
host: host,
132+
port: port,
133+
database: database,
134+
username: username,
135+
password: password,
136+
);
137+
138+
return (
139+
endpoint: endpoint,
140+
sslMode: sslMode,
141+
securityContext: securityContext,
142+
connectTimeout: connectTimeout,
143+
applicationName: applicationName,
144+
encoding: encoding,
145+
replicationMode: replicationMode,
146+
);
147+
}
148+
149+
String? _parseUsername(String userInfo) {
150+
final colonIndex = userInfo.indexOf(':');
151+
if (colonIndex == -1) {
152+
return Uri.decodeComponent(userInfo);
153+
}
154+
return Uri.decodeComponent(userInfo.substring(0, colonIndex));
155+
}
156+
157+
String? _parsePassword(String userInfo) {
158+
final colonIndex = userInfo.indexOf(':');
159+
if (colonIndex == -1) {
160+
return null;
161+
}
162+
return Uri.decodeComponent(userInfo.substring(colonIndex + 1));
163+
}
164+
165+
SecurityContext _createSecurityContext({
166+
String? certPath,
167+
String? keyPath,
168+
String? caPath,
169+
}) {
170+
final context = SecurityContext();
171+
172+
if (certPath != null) {
173+
try {
174+
context.useCertificateChain(certPath);
175+
} catch (e) {
176+
throw ArgumentError('Failed to load SSL certificate from $certPath: $e');
177+
}
178+
}
179+
180+
if (keyPath != null) {
181+
try {
182+
context.usePrivateKey(keyPath);
183+
} catch (e) {
184+
throw ArgumentError('Failed to load SSL private key from $keyPath: $e');
185+
}
186+
}
187+
188+
if (caPath != null) {
189+
try {
190+
context.setTrustedCertificates(caPath);
191+
} catch (e) {
192+
throw ArgumentError(
193+
'Failed to load SSL CA certificates from $caPath: $e');
194+
}
195+
}
196+
197+
return context;
198+
}

lib/src/pool/pool_api.dart

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:async';
22

33
import 'package:meta/meta.dart';
4+
import 'package:postgres/src/connection_string.dart';
45

56
import '../../postgres.dart';
67
import 'pool_impl.dart';
@@ -70,6 +71,26 @@ abstract class Pool<L> implements Session, SessionExecutor {
7071
}) =>
7172
PoolImplementation(roundRobinSelector(endpoints), settings);
7273

74+
/// Creates a new pool where the endpoint and the settings are encoded as an URL as
75+
/// `postgresql://[userspec@][hostspec][/dbname][?paramspec]`
76+
///
77+
/// Note: Only a single endpoint is supported for now.
78+
/// Note: Only a subset of settings can be set with parameters.
79+
factory Pool.withUrl(String connectionString) {
80+
final parsed = parseConnectionString(connectionString);
81+
return PoolImplementation(
82+
roundRobinSelector([parsed.endpoint]),
83+
PoolSettings(
84+
applicationName: parsed.applicationName,
85+
connectTimeout: parsed.connectTimeout,
86+
encoding: parsed.encoding,
87+
replicationMode: parsed.replicationMode,
88+
securityContext: parsed.securityContext,
89+
sslMode: parsed.sslMode,
90+
),
91+
);
92+
}
93+
7394
/// Acquires a connection from this pool, opening a new one if necessary, and
7495
/// calls [fn] with it.
7596
///

lib/src/pool/pool_impl.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ class PoolImplementation<L> implements Pool<L> {
145145
// one.
146146
connection = await _selectOrCreate(
147147
selection.endpoint,
148-
ResolvedConnectionSettings(settings, this._settings),
148+
ResolvedConnectionSettings(settings, _settings),
149149
);
150150

151151
sw.start();

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.6
3+
version: 3.5.7
44
homepage: https://github.com/isoos/postgresql-dart
55
topics:
66
- sql

0 commit comments

Comments
 (0)