postgres-connection.vala 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. using Postgres;
  2. namespace InvercargillSql {
  3. /*
  4. * PostgreSQL implementation of the Connection interface.
  5. * Wraps a libpq PGconn (Postgres.Database) for database connectivity.
  6. *
  7. * last_insert_rowid is implemented with SELECT lastval(): the last value
  8. * obtained from any sequence on this connection, which matches SQLite's
  9. * "rowid of the most recent INSERT" for identity/serial tables. If no
  10. * sequence has been used yet, lastval() errors and we surface 0.
  11. *
  12. * Internal class: use ConnectionFactory to create connections.
  13. */
  14. internal class PostgresConnection : Object, Connection {
  15. private Database? _db;
  16. private bool _is_open;
  17. private int _last_changes;
  18. public string conninfo { get; construct; }
  19. public string database_name { get; construct; }
  20. internal PostgresConnection(string conninfo, string database_name) {
  21. Object(conninfo: conninfo, database_name: database_name);
  22. }
  23. public bool is_open {
  24. get { return _is_open && _db != null && _db.get_status() == ConnectionStatus.OK; }
  25. }
  26. public int64 last_insert_rowid {
  27. get { return query_lastval(); }
  28. }
  29. public int changes {
  30. get { return _last_changes; }
  31. }
  32. public void open() throws SqlError {
  33. if (_is_open) {
  34. return;
  35. }
  36. _db = Postgres.connect_db(conninfo);
  37. if (_db.get_status() != ConnectionStatus.OK) {
  38. string message = _db.get_error_message();
  39. _db = null;
  40. throw new SqlError.CONNECTION_FAILED(
  41. "Failed to connect to PostgreSQL database '%s': %s".printf(database_name, message)
  42. );
  43. }
  44. _is_open = true;
  45. _last_changes = 0;
  46. }
  47. public void close() throws SqlError {
  48. if (_is_open && _db != null) {
  49. _db = null; // PQfinish via unref
  50. _is_open = false;
  51. }
  52. }
  53. public Command create_command(string sql) {
  54. return new PostgresCommand(this, sql);
  55. }
  56. public Transaction begin_transaction() throws SqlError {
  57. ensure_open();
  58. exec_internal("BEGIN");
  59. return new PostgresTransaction(this);
  60. }
  61. public void execute(string sql) throws SqlError {
  62. ensure_open();
  63. exec_internal(sql);
  64. }
  65. internal unowned Database get_database() {
  66. return _db;
  67. }
  68. internal void ensure_open() throws SqlError {
  69. if (!_is_open || _db == null) {
  70. throw new SqlError.CONNECTION_CLOSED("Connection is not open");
  71. }
  72. }
  73. /*
  74. * Runs SQL and checks the result status, updating the cached
  75. * changes counter. Shared by execute(), PostgresTransaction and
  76. * PostgresCommand (which keeps the owned Result for rows).
  77. */
  78. internal Postgres.Result exec_internal(string sql) throws SqlError {
  79. Postgres.Result result = _db.exec(sql);
  80. check_result(result, sql);
  81. update_counters(result);
  82. return result;
  83. }
  84. /*
  85. * Runs parameterized SQL via PQexecParams. n_params/values/lengths/
  86. * formats are the libpq arrays (values may contain nulls for SQL
  87. * NULL). Returns the owned PGresult after status checking.
  88. */
  89. internal Postgres.Result exec_params_internal(
  90. string command,
  91. int n_params,
  92. string[]? param_values,
  93. int[]? param_lengths,
  94. int[]? param_formats
  95. ) throws SqlError {
  96. Postgres.Result result = _db.exec_params(
  97. command, n_params, null, param_values, param_lengths, param_formats, 0
  98. );
  99. check_result(result, command);
  100. update_counters(result);
  101. return result;
  102. }
  103. /*
  104. * Checks a PGresult status and throws an appropriate SqlError.
  105. * Codes derived from SQLSTATE:
  106. * 08/57/58 prefixes (connection, intervention, system) -> CONNECTION_FAILED,
  107. * 25/26 prefixes (transaction states) -> TRANSACTION_FAILED,
  108. * 22xxx/42883 (data/type errors) -> TYPE_ERROR,
  109. * otherwise EXECUTION_FAILED.
  110. */
  111. internal void check_result(Postgres.Result result, string sql) throws SqlError {
  112. var status = result.get_status();
  113. if (status == ExecStatus.COMMAND_OK || status == ExecStatus.TUPLES_OK ||
  114. status == ExecStatus.EMPTY_QUERY) {
  115. return;
  116. }
  117. string message = result.get_error_message();
  118. string sqlstate = result.get_error_field(FieldCode.SQLSTATE) ?? "";
  119. string context = sql.length > 120 ? sql.substring(0, 120) + "..." : sql;
  120. string full_message = "%s (SQL: %s)".printf(message, context);
  121. if (sqlstate.has_prefix("08") || sqlstate.has_prefix("57") || sqlstate.has_prefix("58")) {
  122. _is_open = false;
  123. throw new SqlError.CONNECTION_FAILED(full_message);
  124. }
  125. if (sqlstate.has_prefix("25") || sqlstate.has_prefix("26")) {
  126. throw new SqlError.TRANSACTION_FAILED(full_message);
  127. }
  128. if (sqlstate.has_prefix("22") || sqlstate == "42883") {
  129. throw new SqlError.TYPE_ERROR(full_message);
  130. }
  131. throw new SqlError.EXECUTION_FAILED(full_message);
  132. }
  133. /*
  134. * Updates the cached changes counter from a command's PGresult
  135. * (PQcmdTuples). last_insert_rowid is fetched lazily via lastval().
  136. */
  137. internal void update_counters(Postgres.Result result) {
  138. unowned string tuples = result.get_cmd_tuples();
  139. int64 parsed = 0;
  140. if (tuples != null && tuples.length > 0 && int64.try_parse(tuples, out parsed)) {
  141. _last_changes = (int)parsed;
  142. } else {
  143. _last_changes = 0;
  144. }
  145. }
  146. /*
  147. * Runs SELECT lastval() on this connection.
  148. * Returns 0 when no sequence has been used yet in the session
  149. * (lastval() raises 55006 then, which is not fatal for callers).
  150. */
  151. private int64 query_lastval() throws SqlError {
  152. ensure_open();
  153. Postgres.Result result = _db.exec("SELECT lastval()");
  154. var status = result.get_status();
  155. if (status == ExecStatus.TUPLES_OK && result.get_n_tuples() > 0) {
  156. return int64.parse(result.get_value(0, 0));
  157. }
  158. return 0;
  159. }
  160. }
  161. }