Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## 3.5.7.

- Supporting URL-based connection-string specification in `Connection.openFromUrl` and `Pool.withUrl`. (Note: feature and supported settings is considered experimental.)

## 3.5.6

- Accept `null` values as part of the binary List encodings.
Expand Down
21 changes: 21 additions & 0 deletions lib/postgres.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'dart:io';

import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:postgres/src/connection_string.dart';
import 'package:postgres/src/v3/connection_info.dart';
import 'package:stream_channel/stream_channel.dart';

Expand Down Expand Up @@ -230,6 +231,26 @@ abstract class Connection implements Session, SessionExecutor {
connectionSettings: settings);
}

/// Open a new connection where the endpoint and the settings are encoded as an URL as
/// `postgresql://[userspec@][hostspec][/dbname][?paramspec]`
///
/// Note: Only a single endpoint is supported.
/// Note: Only a subset of settings can be set with parameters.
static Future<Connection> openFromUrl(String connectionString) async {
final parsed = parseConnectionString(connectionString);
return open(
parsed.endpoint,
settings: ConnectionSettings(
applicationName: parsed.applicationName,
connectTimeout: parsed.connectTimeout,
encoding: parsed.encoding,
replicationMode: parsed.replicationMode,
securityContext: parsed.securityContext,
sslMode: parsed.sslMode,
),
);
}

ConnectionInfo get info;

Channels get channels;
Expand Down
198 changes: 198 additions & 0 deletions lib/src/connection_string.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import 'dart:convert';
import 'dart:io';

import '../postgres.dart';

({
Endpoint endpoint,
String? applicationName,
Duration? connectTimeout,
Encoding? encoding,
ReplicationMode? replicationMode,
SecurityContext? securityContext,
SslMode? sslMode,
}) parseConnectionString(String connectionString) {
final uri = Uri.parse(connectionString);

if (uri.scheme != 'postgresql' && uri.scheme != 'postgres') {
throw ArgumentError(
'Invalid connection string scheme: ${uri.scheme}. Expected "postgresql" or "postgres".');
}

final host = uri.host.isEmpty ? 'localhost' : uri.host;
final port = uri.port == 0 ? 5432 : uri.port;
final database = uri.pathSegments.firstOrNull ?? 'postgres';
final username = uri.userInfo.isEmpty ? null : _parseUsername(uri.userInfo);
final password = uri.userInfo.isEmpty ? null : _parsePassword(uri.userInfo);

final validParams = {
'sslmode',
'sslcert',
'sslkey',
'sslrootcert',
'connect_timeout',
'application_name',
'client_encoding',
'replication'
};

final params = uri.queryParameters;
for (final key in params.keys) {
if (!validParams.contains(key)) {
throw ArgumentError('Unrecognized connection parameter: $key');
}
}

SslMode? sslMode;
if (params.containsKey('sslmode')) {
switch (params['sslmode']) {
case 'disable':
sslMode = SslMode.disable;
break;
case 'require':
sslMode = SslMode.require;
break;
case 'verify-ca':
case 'verify-full':
sslMode = SslMode.verifyFull;
break;
default:
throw ArgumentError(
'Invalid sslmode value: ${params['sslmode']}. Expected: disable, require, verify-ca, verify-full');
}
}

SecurityContext? securityContext;
if (params.containsKey('sslcert') ||
params.containsKey('sslkey') ||
params.containsKey('sslrootcert')) {
try {
securityContext = _createSecurityContext(
certPath: params['sslcert'],
keyPath: params['sslkey'],
caPath: params['sslrootcert'],
);
} catch (e) {
// re-throw with more context about connection string parsing
throw ArgumentError('SSL configuration error in connection string: $e');
}
}

Duration? connectTimeout;
if (params.containsKey('connect_timeout')) {
final timeoutSeconds = int.tryParse(params['connect_timeout']!);
if (timeoutSeconds == null || timeoutSeconds <= 0) {
throw ArgumentError(
'Invalid connect_timeout value: ${params['connect_timeout']}. Expected positive integer.');
}
connectTimeout = Duration(seconds: timeoutSeconds);
}

final applicationName = params['application_name'];

Encoding? encoding;
if (params.containsKey('client_encoding')) {
switch (params['client_encoding']?.toUpperCase()) {
case 'UTF8':
case 'UTF-8':
encoding = utf8;
break;
case 'LATIN1':
case 'ISO-8859-1':
encoding = latin1;
break;
default:
throw ArgumentError(
'Unsupported client_encoding: ${params['client_encoding']}. Supported: UTF8, LATIN1');
}
}

ReplicationMode? replicationMode;
if (params.containsKey('replication')) {
switch (params['replication']) {
case 'database':
replicationMode = ReplicationMode.logical;
break;
case 'true':
case 'physical':
replicationMode = ReplicationMode.physical;
break;
case 'false':
case 'no_select':
replicationMode = ReplicationMode.none;
break;
default:
throw ArgumentError(
'Invalid replication value: ${params['replication']}. Expected: database, true, physical, false, no_select');
}
}

final endpoint = Endpoint(
host: host,
port: port,
database: database,
username: username,
password: password,
);

return (
endpoint: endpoint,
sslMode: sslMode,
securityContext: securityContext,
connectTimeout: connectTimeout,
applicationName: applicationName,
encoding: encoding,
replicationMode: replicationMode,
);
}

String? _parseUsername(String userInfo) {
final colonIndex = userInfo.indexOf(':');
if (colonIndex == -1) {
return Uri.decodeComponent(userInfo);
}
return Uri.decodeComponent(userInfo.substring(0, colonIndex));
}

String? _parsePassword(String userInfo) {
final colonIndex = userInfo.indexOf(':');
if (colonIndex == -1) {
return null;
}
return Uri.decodeComponent(userInfo.substring(colonIndex + 1));
}

SecurityContext _createSecurityContext({
String? certPath,
String? keyPath,
String? caPath,
}) {
final context = SecurityContext();

if (certPath != null) {
try {
context.useCertificateChain(certPath);
} catch (e) {
throw ArgumentError('Failed to load SSL certificate from $certPath: $e');
}
}

if (keyPath != null) {
try {
context.usePrivateKey(keyPath);
} catch (e) {
throw ArgumentError('Failed to load SSL private key from $keyPath: $e');
}
}

if (caPath != null) {
try {
context.setTrustedCertificates(caPath);
} catch (e) {
throw ArgumentError(
'Failed to load SSL CA certificates from $caPath: $e');
}
}

return context;
}
21 changes: 21 additions & 0 deletions lib/src/pool/pool_api.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';

import 'package:meta/meta.dart';
import 'package:postgres/src/connection_string.dart';

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

/// Creates a new pool where the endpoint and the settings are encoded as an URL as
/// `postgresql://[userspec@][hostspec][/dbname][?paramspec]`
///
/// 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);
return PoolImplementation(
roundRobinSelector([parsed.endpoint]),
PoolSettings(
applicationName: parsed.applicationName,
connectTimeout: parsed.connectTimeout,
encoding: parsed.encoding,
replicationMode: parsed.replicationMode,
securityContext: parsed.securityContext,
sslMode: parsed.sslMode,
),
);
}

/// Acquires a connection from this pool, opening a new one if necessary, and
/// calls [fn] with it.
///
Expand Down
2 changes: 1 addition & 1 deletion lib/src/pool/pool_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class PoolImplementation<L> implements Pool<L> {
// one.
connection = await _selectOrCreate(
selection.endpoint,
ResolvedConnectionSettings(settings, this._settings),
ResolvedConnectionSettings(settings, _settings),
);

sw.start();
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: postgres
description: PostgreSQL database driver. Supports binary protocol, connection pooling and statement reuse.
version: 3.5.6
version: 3.5.7
homepage: https://github.com/isoos/postgresql-dart
topics:
- sql
Expand Down
Loading