using Invercargill.DataStructures; namespace InvercargillSql { /** * PostgreSQL implementation of ConnectionProvider. * * Handles connection strings in standard URI format: * - `postgresql://user:pass@host:port/database` - TCP connection * - `postgresql://user@/run/postgresql/dbname?host=/run/postgresql` - Unix socket dir * - `postgres://user@localhost:5432/db` - Short scheme alias * * The provider builds a libpq conninfo string from the parsed * ConnectionString components. Components that are empty are omitted * so libpq falls back to its own defaults (environment, ~/.pgpass, * compiled-in defaults) — "trust env" behaviour. * * Supported query options: any option key that is a known libpq * conninfo keyword (host, port, user, password, dbname, connect_timeout, * sslmode, application_name, ...) is forwarded verbatim; other options * are ignored. * * Note: This class is internal to the library. Use ConnectionFactory * to create connections. */ internal class PostgresProvider : Object, ConnectionProvider { /** * The scheme this provider handles: "postgresql". */ public string scheme { get { return "postgresql"; } } /** * Creates a new PostgreSQL connection from a connection string. * * @param cs The parsed connection string * @return A new PostgresConnection instance * @throws SqlError if connection creation fails */ public Connection create_connection(ConnectionString cs) throws SqlError { string conninfo = build_conninfo(cs); return new PostgresConnection(conninfo, cs.database); } /** * Builds a libpq conninfo string from the parsed connection string. * * Empty components are omitted. A leading "/" on the host means a * Unix socket directory (libpq "host" accepts a directory path). * * @param cs The parsed connection string * @return conninfo string suitable for PQconnectdb * @throws SqlError.INVALID_CONNECTION_STRING if the database name is empty */ internal string build_conninfo(ConnectionString cs) throws SqlError { if (cs.database.length == 0) { throw new SqlError.INVALID_CONNECTION_STRING( "PostgreSQL connection string missing database name" ); } var parts = new StringBuilder(); // Host/port may arrive either as URI components or as query // options (the common unix-socket form is // postgresql://user@/dbname?host=/run/postgresql). URI // components win; otherwise the options fill in. string host = cs.host; if (host.length == 0 && cs.has_option("host")) { host = cs.get_option("host"); } append_part(parts, "host", host); uint16 port = cs.port; if (port == 0 && cs.has_option("port")) { port = (uint16)int64.parse(cs.get_option("port")); } if (port != 0) { append_part(parts, "port", port.to_string()); } append_part(parts, "user", cs.user); append_part(parts, "password", cs.password); append_part(parts, "dbname", cs.database); // Forward recognized libpq conninfo keywords from query options. foreach (var kvp in cs.options) { string key = kvp.key.down(); if (key == "user" || key == "password" || key == "dbname") { // Already covered by the parsed components above. continue; } if (key == "host" || key == "port") { // Handled above (URI component or fallback). continue; } switch (key) { case "connect_timeout": case "client_encoding": case "application_name": case "fallback_application_name": case "keepalives": case "keepalives_idle": case "keepalives_interval": case "keepalives_count": case "sslmode": case "sslcompression": case "sslcrl": case "sslrootcert": case "sslcert": case "sslkey": case "sslnegotiation": case "requiressl": case "krbsrvname": case "gsslib": case "gssdelegation": case "gssencmode": case "service": case "options": append_part(parts, key, kvp.value); break; default: // Unknown option: ignored, matching sqlite's // tolerance of unsupported options. break; } } return parts.str; } /** * Appends "key = 'value'" to the conninfo builder, escaping single * quotes in the value. Skips empty values. */ private void append_part(StringBuilder parts, string key, string value) { if (value.length == 0) { return; } if (parts.len > 0) { parts.append(" "); } parts.append(key); parts.append(" = '"); parts.append(value.replace("'", "\\'")); parts.append("'"); } } }