Kea  1.9.9-git
pgsql_connection.cc
Go to the documentation of this file.
1 // Copyright (C) 2016-2020 Internet Systems Consortium, Inc. ("ISC")
2 //
3 // This Source Code Form is subject to the terms of the Mozilla Public
4 // License, v. 2.0. If a copy of the MPL was not distributed with this
5 // file, You can obtain one at http://mozilla.org/MPL/2.0/.
6 
7 #include <config.h>
8 
9 #include <database/db_log.h>
10 #include <pgsql/pgsql_connection.h>
11 #include <pgsql/pgsql_exchange.h>
12 
13 // PostgreSQL errors should be tested based on the SQL state code. Each state
14 // code is 5 decimal, ASCII, digits, the first two define the category of
15 // error, the last three are the specific error. PostgreSQL makes the state
16 // code as a char[5]. Macros for each code are defined in PostgreSQL's
17 // server/utils/errcodes.h, although they require a second macro,
18 // MAKE_SQLSTATE for completion. For example, duplicate key error as:
19 //
20 // #define ERRCODE_UNIQUE_VIOLATION MAKE_SQLSTATE('2','3','5','0','5')
21 //
22 // PostgreSQL deliberately omits the MAKE_SQLSTATE macro so callers can/must
23 // supply their own. We'll define it as an initialization list:
24 #define MAKE_SQLSTATE(ch1,ch2,ch3,ch4,ch5) {ch1,ch2,ch3,ch4,ch5}
25 // So we can use it like this: const char some_error[] = ERRCODE_xxxx;
26 #define PGSQL_STATECODE_LEN 5
27 #include <utils/errcodes.h>
28 
29 #include <sstream>
30 
31 using namespace std;
32 
33 namespace isc {
34 namespace db {
35 
36 // Default connection timeout
37 
39 const int PGSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
40 
41 const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
42 
43 PgSqlResult::PgSqlResult(PGresult *result)
44  : result_(result), rows_(0), cols_(0) {
45  if (!result) {
46  // Certain failures, like a loss of connectivity, can return a
47  // null PGresult and we still need to be able to create a PgSqlResult.
48  // We'll set row and col counts to -1 to prevent anyone going off the
49  // rails.
50  rows_ = -1;
51  cols_ = -1;
52  } else {
53  rows_ = PQntuples(result);
54  cols_ = PQnfields(result);
55  }
56 }
57 
58 void
59 PgSqlResult::rowCheck(int row) const {
60  if (row < 0 || row >= rows_) {
61  isc_throw (db::DbOperationError, "row: " << row
62  << ", out of range: 0.." << rows_);
63  }
64 }
65 
67  if (result_) {
68  PQclear(result_);
69  }
70 }
71 
72 void
73 PgSqlResult::colCheck(int col) const {
74  if (col < 0 || col >= cols_) {
75  isc_throw (DbOperationError, "col: " << col
76  << ", out of range: 0.." << cols_);
77  }
78 }
79 
80 void
81 PgSqlResult::rowColCheck(int row, int col) const {
82  rowCheck(row);
83  colCheck(col);
84 }
85 
86 std::string
87 PgSqlResult::getColumnLabel(const int col) const {
88  const char* label = NULL;
89  try {
90  colCheck(col);
91  label = PQfname(result_, col);
92  } catch (...) {
93  std::ostringstream os;
94  os << "Unknown column:" << col;
95  return (os.str());
96  }
97 
98  return (label);
99 }
100 
102  : conn_(conn), committed_(false) {
103  conn_.startTransaction();
104 }
105 
107  // If commit() wasn't explicitly called, rollback.
108  if (!committed_) {
109  conn_.rollback();
110  }
111 }
112 
113 void
115  conn_.commit();
116  committed_ = true;
117 }
118 
120  if (conn_) {
121  // Deallocate the prepared queries.
122  if (PQstatus(conn_) == CONNECTION_OK) {
123  PgSqlResult r(PQexec(conn_, "DEALLOCATE all"));
124  if (PQresultStatus(r) != PGRES_COMMAND_OK) {
125  // Highly unlikely but we'll log it and go on.
127  .arg(PQerrorMessage(conn_));
128  }
129  }
130  }
131 }
132 
133 std::pair<uint32_t, uint32_t>
135  // Get a connection.
136  PgSqlConnection conn(parameters);
137 
138  // Open the database.
139  conn.openDatabase();
140 
141  const char* version_sql = "SELECT version, minor FROM schema_version;";
142  PgSqlResult r(PQexec(conn.conn_, version_sql));
143  if (PQresultStatus(r) != PGRES_TUPLES_OK) {
144  isc_throw(DbOperationError, "unable to execute PostgreSQL statement <"
145  << version_sql << ", reason: " << PQerrorMessage(conn.conn_));
146  }
147 
148  uint32_t version;
149  PgSqlExchange::getColumnValue(r, 0, 0, version);
150 
151  uint32_t minor;
152  PgSqlExchange::getColumnValue(r, 0, 1, minor);
153 
154  return (make_pair(version, minor));
155 }
156 
157 void
159  // Prepare all statements queries with all known fields datatype
160  PgSqlResult r(PQprepare(conn_, statement.name, statement.text,
161  statement.nbparams, statement.types));
162  if (PQresultStatus(r) != PGRES_COMMAND_OK) {
163  isc_throw(DbOperationError, "unable to prepare PostgreSQL statement: "
164  << statement.text << ", reason: " << PQerrorMessage(conn_));
165  }
166 }
167 
168 void
170  const PgSqlTaggedStatement* end_statement) {
171  // Created the PostgreSQL prepared statements.
172  for (const PgSqlTaggedStatement* tagged_statement = start_statement;
173  tagged_statement != end_statement; ++tagged_statement) {
174  prepareStatement(*tagged_statement);
175  }
176 }
177 
178 void
180  string dbconnparameters;
181  string shost = "localhost";
182  try {
183  shost = getParameter("host");
184  } catch(...) {
185  // No host. Fine, we'll use "localhost"
186  }
187 
188  dbconnparameters += "host = '" + shost + "'" ;
189 
190  string sport;
191  try {
192  sport = getParameter("port");
193  } catch (...) {
194  // No port parameter, we are going to use the default port.
195  sport = "";
196  }
197 
198  if (sport.size() > 0) {
199  unsigned int port = 0;
200 
201  // Port was given, so try to convert it to an integer.
202  try {
203  port = boost::lexical_cast<unsigned int>(sport);
204  } catch (...) {
205  // Port given but could not be converted to an unsigned int.
206  // Just fall back to the default value.
207  port = 0;
208  }
209 
210  // The port is only valid when it is in the 0..65535 range.
211  // Again fall back to the default when the given value is invalid.
212  if (port > numeric_limits<uint16_t>::max()) {
213  port = 0;
214  }
215 
216  // Add it to connection parameters when not default.
217  if (port > 0) {
218  std::ostringstream oss;
219  oss << port;
220  dbconnparameters += " port = " + oss.str();
221  }
222  }
223 
224  string suser;
225  try {
226  suser = getParameter("user");
227  dbconnparameters += " user = '" + suser + "'";
228  } catch(...) {
229  // No user. Fine, we'll use NULL
230  }
231 
232  string spassword;
233  try {
234  spassword = getParameter("password");
235  dbconnparameters += " password = '" + spassword + "'";
236  } catch(...) {
237  // No password. Fine, we'll use NULL
238  }
239 
240  string sname;
241  try {
242  sname = getParameter("name");
243  dbconnparameters += " dbname = '" + sname + "'";
244  } catch(...) {
245  // No database name. Throw a "NoDatabaseName" exception
246  isc_throw(NoDatabaseName, "must specify a name for the database");
247  }
248 
249  unsigned int connect_timeout = PGSQL_DEFAULT_CONNECTION_TIMEOUT;
250  string stimeout;
251  try {
252  stimeout = getParameter("connect-timeout");
253  } catch (...) {
254  // No timeout parameter, we are going to use the default timeout.
255  stimeout = "";
256  }
257 
258  if (stimeout.size() > 0) {
259  // Timeout was given, so try to convert it to an integer.
260 
261  try {
262  connect_timeout = boost::lexical_cast<unsigned int>(stimeout);
263  } catch (...) {
264  // Timeout given but could not be converted to an unsigned int. Set
265  // the connection timeout to an invalid value to trigger throwing
266  // of an exception.
267  connect_timeout = 0;
268  }
269 
270  // The timeout is only valid if greater than zero, as depending on the
271  // database, a zero timeout might signify something like "wait
272  // indefinitely".
273  //
274  // The check below also rejects a value greater than the maximum
275  // integer value. The lexical_cast operation used to obtain a numeric
276  // value from a string can get confused if trying to convert a negative
277  // integer to an unsigned int: instead of throwing an exception, it may
278  // produce a large positive value.
279  if ((connect_timeout == 0) ||
280  (connect_timeout > numeric_limits<int>::max())) {
281  isc_throw(DbInvalidTimeout, "database connection timeout (" <<
282  stimeout << ") must be an integer greater than 0");
283  }
284  }
285 
286  std::ostringstream oss;
287  oss << connect_timeout;
288  dbconnparameters += " connect_timeout = " + oss.str();
289 
290  // Connect to Postgres, saving the low level connection pointer
291  // in the holder object
292  PGconn* new_conn = PQconnectdb(dbconnparameters.c_str());
293  if (!new_conn) {
294  isc_throw(DbOpenError, "could not allocate connection object");
295  }
296 
297  if (PQstatus(new_conn) != CONNECTION_OK) {
298  // If we have a connection object, we have to call finish
299  // to release it, but grab the error message first.
300  std::string error_message = PQerrorMessage(new_conn);
301  PQfinish(new_conn);
302  isc_throw(DbOpenError, error_message);
303  }
304 
305  // We have a valid connection, so let's save it to our holder
306  conn_.setConnection(new_conn);
307 }
308 
309 bool
310 PgSqlConnection::compareError(const PgSqlResult& r, const char* error_state) {
311  const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
312  // PostgreSQL guarantees it will always be 5 characters long
313  return ((sqlstate != NULL) &&
314  (memcmp(sqlstate, error_state, PGSQL_STATECODE_LEN) == 0));
315 }
316 
317 void
319  PgSqlTaggedStatement& statement) {
320  int s = PQresultStatus(r);
321  if (s != PGRES_COMMAND_OK && s != PGRES_TUPLES_OK) {
322  // We're testing the first two chars of SQLSTATE, as this is the
323  // error class. Note, there is a severity field, but it can be
324  // misleadingly returned as fatal. However, a loss of connectivity
325  // can lead to a NULL sqlstate with a status of PGRES_FATAL_ERROR.
326  const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
327  if ((sqlstate == NULL) ||
328  ((memcmp(sqlstate, "08", 2) == 0) || // Connection Exception
329  (memcmp(sqlstate, "53", 2) == 0) || // Insufficient resources
330  (memcmp(sqlstate, "54", 2) == 0) || // Program Limit exceeded
331  (memcmp(sqlstate, "57", 2) == 0) || // Operator intervention
332  (memcmp(sqlstate, "58", 2) == 0))) { // System error
334  .arg(statement.name)
335  .arg(PQerrorMessage(conn_))
336  .arg(sqlstate ? sqlstate : "<sqlstate null>");
337 
338  // Mark this connection as no longer usable.
339  markUnusable();
340 
341  // Start the connection recovery.
343 
344  // We still need to throw so caller can error out of the current
345  // processing.
347  "fatal database error or connectivity lost");
348  }
349 
350  // Apparently it wasn't fatal, so we throw with a helpful message.
351  const char* error_message = PQerrorMessage(conn_);
352  isc_throw(DbOperationError, "Statement exec failed:" << " for: "
353  << statement.name << ", status: " << s
354  << "sqlstate:[ " << (sqlstate ? sqlstate : "<null>")
355  << " ], reason: " << error_message);
356  }
357 }
358 
359 void
362  checkUnusable();
363  PgSqlResult r(PQexec(conn_, "START TRANSACTION"));
364  if (PQresultStatus(r) != PGRES_COMMAND_OK) {
365  const char* error_message = PQerrorMessage(conn_);
366  isc_throw(DbOperationError, "unable to start transaction"
367  << error_message);
368  }
369 }
370 
371 void
374  checkUnusable();
375  PgSqlResult r(PQexec(conn_, "COMMIT"));
376  if (PQresultStatus(r) != PGRES_COMMAND_OK) {
377  const char* error_message = PQerrorMessage(conn_);
378  isc_throw(DbOperationError, "commit failed: " << error_message);
379  }
380 }
381 
382 void
385  checkUnusable();
386  PgSqlResult r(PQexec(conn_, "ROLLBACK"));
387  if (PQresultStatus(r) != PGRES_COMMAND_OK) {
388  const char* error_message = PQerrorMessage(conn_);
389  isc_throw(DbOperationError, "rollback failed: " << error_message);
390  }
391 }
392 
393 } // end of isc::db namespace
394 } // end of isc namespace
We want to reuse the database backend connection and exchange code for other uses, in particular for hook libraries.
RAII wrapper for PostgreSQL Result sets.
const Oid types[PGSQL_MAX_PARAMETERS_IN_QUERY]
OID types.
void startTransaction()
Start a transaction.
void commit()
Commits transaction.
const int PGSQL_DEFAULT_CONNECTION_TIMEOUT
DB_LOG & arg(T first, Args...args)
Pass parameters to replace logger placeholders.
Definition: db_log.h:144
void colCheck(int col) const
Determines if a column index is valid.
bool compareError(const PgSqlResult &r, const char *error_state)
Checks a result set's SQL state against an error state.
void rowColCheck(int row, int col) const
Determines if both a row and column index are valid.
void rowCheck(int row) const
Determines if a row index is valid.
STL namespace.
void checkUnusable()
Throws an exception if the connection is not usable.
Exception thrown on failure to open database.
void commit()
Commit Transactions.
void startRecoverDbConnection()
The recover connection.
#define isc_throw(type, stream)
A shortcut macro to insert known values into exception arguments.
void prepareStatements(const PgSqlTaggedStatement *start_statement, const PgSqlTaggedStatement *end_statement)
Prepare statements.
Exception thrown if name of database is not specified.
void checkStatementError(const PgSqlResult &r, PgSqlTaggedStatement &statement)
Checks result of the r object.
void rollback()
Rollback Transactions.
PgSqlTransaction(PgSqlConnection &conn)
Constructor.
int version()
returns Kea hooks version.
static void getColumnValue(const PgSqlResult &r, const int row, const size_t col, std::string &value)
Fetches text column value as a string.
Common PgSql Connector Pool.
virtual ~PgSqlConnection()
Destructor.
void prepareStatement(const PgSqlTaggedStatement &statement)
Prepare Single Statement.
const int DB_DBG_TRACE_DETAIL
Database logging levels.
Definition: db_log.cc:21
Defines the logger used by the top-level component of kea-dhcp-ddns.
void setConnection(PGconn *connection)
Sets the connection to the value given.
Define a PostgreSQL statement.
std::string getParameter(const std::string &name) const
Returns value of a connection parameter.
#define PGSQL_STATECODE_LEN
const char * text
Text representation of the actual query.
~PgSqlResult()
Destructor.
void markUnusable()
Sets the unusable flag to true.
Exception thrown when a specific connection has been rendered unusable either through loss of connect...
std::string getColumnLabel(const int col) const
Fetches the name of the column in a result set.
void openDatabase()
Open Database.
const char * name
Short name of the query.
static std::pair< uint32_t, uint32_t > getVersion(const ParameterMap &parameters)
Get the schema version.
std::map< std::string, std::string > ParameterMap
Database configuration parameter map.
PgSqlHolder conn_
PgSql connection handle.
Exception thrown on failure to execute a database function.
int nbparams
Number of parameters for a given query.