Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
1 change: 1 addition & 0 deletions lib/postgres.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
),
Expand Down
93 changes: 87 additions & 6 deletions lib/src/connection_string.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the non-standard parameters perhaps be mentioned in the readme? I assume they'd be very hard to discover otherwise?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, already on it.

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') {
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -149,6 +225,11 @@ parseConnectionString(String connectionString) {
applicationName: applicationName,
encoding: encoding,
replicationMode: replicationMode,
queryTimeout: queryTimeout,
maxConnectionAge: maxConnectionAge,
maxConnectionCount: maxConnectionCount,
maxSessionUse: maxSessionUse,
maxQueryCount: maxQueryCount,
);
}

Expand Down
10 changes: 9 additions & 1 deletion lib/src/pool/pool_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,16 +75,24 @@ abstract class Pool<L> 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,
),
);
}
Expand Down
129 changes: 129 additions & 0 deletions test/connection_string_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentError>().having(
(e) => e.message,
'message',
contains('Invalid query_timeout'),
),
),
);
expect(
() => parseConnectionString(
'postgresql://localhost/test?query_timeout=0',
),
throwsA(
isA<ArgumentError>().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<ArgumentError>().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<ArgumentError>().having(
(e) => e.message,
'message',
contains('Invalid max_connection_age'),
),
),
);
expect(
() => parseConnectionString(
'postgresql://localhost/test?max_connection_count=invalid',
enablePoolSettings: true,
),
throwsA(
isA<ArgumentError>().having(
(e) => e.message,
'message',
contains('Invalid max_connection_count'),
),
),
);
expect(
() => parseConnectionString(
'postgresql://localhost/test?max_session_use=-5',
enablePoolSettings: true,
),
throwsA(
isA<ArgumentError>().having(
(e) => e.message,
'message',
contains('Invalid max_session_use'),
),
),
);
expect(
() => parseConnectionString(
'postgresql://localhost/test?max_query_count=0',
enablePoolSettings: true,
),
throwsA(
isA<ArgumentError>().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));
});
});
});
}