@@ -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