@@ -4,7 +4,7 @@ import 'dart:io';
44import '../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+
252402SecurityContext _createSecurityContext ({
253403 String ? certPath,
254404 String ? keyPath,
0 commit comments