postgres-provider.vala 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. using Invercargill.DataStructures;
  2. namespace InvercargillSql {
  3. /**
  4. * PostgreSQL implementation of ConnectionProvider.
  5. *
  6. * Handles connection strings in standard URI format:
  7. * - `postgresql://user:pass@host:port/database` - TCP connection
  8. * - `postgresql://user@/run/postgresql/dbname?host=/run/postgresql` - Unix socket dir
  9. * - `postgres://user@localhost:5432/db` - Short scheme alias
  10. *
  11. * The provider builds a libpq conninfo string from the parsed
  12. * ConnectionString components. Components that are empty are omitted
  13. * so libpq falls back to its own defaults (environment, ~/.pgpass,
  14. * compiled-in defaults) — "trust env" behaviour.
  15. *
  16. * Supported query options: any option key that is a known libpq
  17. * conninfo keyword (host, port, user, password, dbname, connect_timeout,
  18. * sslmode, application_name, ...) is forwarded verbatim; other options
  19. * are ignored.
  20. *
  21. * Note: This class is internal to the library. Use ConnectionFactory
  22. * to create connections.
  23. */
  24. internal class PostgresProvider : Object, ConnectionProvider {
  25. /**
  26. * The scheme this provider handles: "postgresql".
  27. */
  28. public string scheme {
  29. get { return "postgresql"; }
  30. }
  31. /**
  32. * Creates a new PostgreSQL connection from a connection string.
  33. *
  34. * @param cs The parsed connection string
  35. * @return A new PostgresConnection instance
  36. * @throws SqlError if connection creation fails
  37. */
  38. public Connection create_connection(ConnectionString cs) throws SqlError {
  39. string conninfo = build_conninfo(cs);
  40. return new PostgresConnection(conninfo, cs.database);
  41. }
  42. /**
  43. * Builds a libpq conninfo string from the parsed connection string.
  44. *
  45. * Empty components are omitted. A leading "/" on the host means a
  46. * Unix socket directory (libpq "host" accepts a directory path).
  47. *
  48. * @param cs The parsed connection string
  49. * @return conninfo string suitable for PQconnectdb
  50. * @throws SqlError.INVALID_CONNECTION_STRING if the database name is empty
  51. */
  52. internal string build_conninfo(ConnectionString cs) throws SqlError {
  53. if (cs.database.length == 0) {
  54. throw new SqlError.INVALID_CONNECTION_STRING(
  55. "PostgreSQL connection string missing database name"
  56. );
  57. }
  58. var parts = new StringBuilder();
  59. // Host/port may arrive either as URI components or as query
  60. // options (the common unix-socket form is
  61. // postgresql://user@/dbname?host=/run/postgresql). URI
  62. // components win; otherwise the options fill in.
  63. string host = cs.host;
  64. if (host.length == 0 && cs.has_option("host")) {
  65. host = cs.get_option("host");
  66. }
  67. append_part(parts, "host", host);
  68. uint16 port = cs.port;
  69. if (port == 0 && cs.has_option("port")) {
  70. port = (uint16)int64.parse(cs.get_option("port"));
  71. }
  72. if (port != 0) {
  73. append_part(parts, "port", port.to_string());
  74. }
  75. append_part(parts, "user", cs.user);
  76. append_part(parts, "password", cs.password);
  77. append_part(parts, "dbname", cs.database);
  78. // Forward recognized libpq conninfo keywords from query options.
  79. foreach (var kvp in cs.options) {
  80. string key = kvp.key.down();
  81. if (key == "user" || key == "password" || key == "dbname") {
  82. // Already covered by the parsed components above.
  83. continue;
  84. }
  85. if (key == "host" || key == "port") {
  86. // Handled above (URI component or fallback).
  87. continue;
  88. }
  89. switch (key) {
  90. case "connect_timeout":
  91. case "client_encoding":
  92. case "application_name":
  93. case "fallback_application_name":
  94. case "keepalives":
  95. case "keepalives_idle":
  96. case "keepalives_interval":
  97. case "keepalives_count":
  98. case "sslmode":
  99. case "sslcompression":
  100. case "sslcrl":
  101. case "sslrootcert":
  102. case "sslcert":
  103. case "sslkey":
  104. case "sslnegotiation":
  105. case "requiressl":
  106. case "krbsrvname":
  107. case "gsslib":
  108. case "gssdelegation":
  109. case "gssencmode":
  110. case "service":
  111. case "options":
  112. append_part(parts, key, kvp.value);
  113. break;
  114. default:
  115. // Unknown option: ignored, matching sqlite's
  116. // tolerance of unsupported options.
  117. break;
  118. }
  119. }
  120. return parts.str;
  121. }
  122. /**
  123. * Appends "key = 'value'" to the conninfo builder, escaping single
  124. * quotes in the value. Skips empty values.
  125. */
  126. private void append_part(StringBuilder parts, string key, string value) {
  127. if (value.length == 0) {
  128. return;
  129. }
  130. if (parts.len > 0) {
  131. parts.append(" ");
  132. }
  133. parts.append(key);
  134. parts.append(" = '");
  135. parts.append(value.replace("'", "\\'"));
  136. parts.append("'");
  137. }
  138. }
  139. }