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