Browse Source

[master] Created common PgSqlConnection class from PgSqlLeaseMgr

    Merged in trac 4276.
Thomas Markwalder 9 years ago
parent
commit
ff63173cf0

+ 2 - 0
src/lib/dhcpsrv/Makefile.am

@@ -133,6 +133,8 @@ endif
 libkea_dhcpsrv_la_SOURCES += ncr_generator.cc ncr_generator.h
 
 if HAVE_PGSQL
+libkea_dhcpsrv_la_SOURCES += pgsql_connection.cc pgsql_connection.h
+libkea_dhcpsrv_la_SOURCES += pgsql_exchange.cc pgsql_exchange.h
 libkea_dhcpsrv_la_SOURCES += pgsql_lease_mgr.cc pgsql_lease_mgr.h
 endif
 libkea_dhcpsrv_la_SOURCES += pool.cc pool.h

+ 216 - 0
src/lib/dhcpsrv/pgsql_connection.cc

@@ -0,0 +1,216 @@
+// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcpsrv/dhcpsrv_log.h>
+#include <dhcpsrv/pgsql_connection.h>
+
+// PostgreSQL errors should be tested based on the SQL state code.  Each state
+// code is 5 decimal, ASCII, digits, the first two define the category of
+// error, the last three are the specific error.  PostgreSQL makes the state
+// code as a char[5].  Macros for each code are defined in PostgreSQL's
+// server/utils/errcodes.h, although they require a second macro,
+// MAKE_SQLSTATE for completion.  For example, duplicate key error as:
+//
+// #define ERRCODE_UNIQUE_VIOLATION MAKE_SQLSTATE('2','3','5','0','5')
+//
+// PostgreSQL deliberately omits the MAKE_SQLSTATE macro so callers can/must
+// supply their own.  We'll define it as an initlizer_list:
+#define MAKE_SQLSTATE(ch1,ch2,ch3,ch4,ch5) {ch1,ch2,ch3,ch4,ch5}
+// So we can use it like this: const char some_error[] = ERRCODE_xxxx;
+#define PGSQL_STATECODE_LEN 5
+#include <utils/errcodes.h>
+
+using namespace std;
+
+namespace isc {
+namespace dhcp {
+
+// Default connection timeout
+const int PGSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
+
+const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
+
+PgSqlConnection::~PgSqlConnection() {
+    if (conn_) {
+        // Deallocate the prepared queries.
+        PgSqlResult r(PQexec(conn_, "DEALLOCATE all"));
+        if(PQresultStatus(r) != PGRES_COMMAND_OK) {
+            // Highly unlikely but we'll log it and go on.
+            LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_DEALLOC_ERROR)
+                      .arg(PQerrorMessage(conn_));
+        }
+    }
+}
+
+void
+PgSqlConnection::prepareStatement(const PgSqlTaggedStatement& statement) {
+    // Prepare all statements queries with all known fields datatype
+    PgSqlResult r(PQprepare(conn_, statement.name, statement.text,
+                            statement.nbparams, statement.types));
+    if(PQresultStatus(r) != PGRES_COMMAND_OK) {
+        isc_throw(DbOperationError, "unable to prepare PostgreSQL statement: "
+                  << statement.text << ", reason: " << PQerrorMessage(conn_));
+    }
+}
+
+void
+PgSqlConnection::openDatabase() {
+    string dbconnparameters;
+    string shost = "localhost";
+    try {
+        shost = getParameter("host");
+    } catch(...) {
+        // No host. Fine, we'll use "localhost"
+    }
+
+    dbconnparameters += "host = '" + shost + "'" ;
+
+    string suser;
+    try {
+        suser = getParameter("user");
+        dbconnparameters += " user = '" + suser + "'";
+    } catch(...) {
+        // No user. Fine, we'll use NULL
+    }
+
+    string spassword;
+    try {
+        spassword = getParameter("password");
+        dbconnparameters += " password = '" + spassword + "'";
+    } catch(...) {
+        // No password. Fine, we'll use NULL
+    }
+
+    string sname;
+    try {
+        sname = getParameter("name");
+        dbconnparameters += " dbname = '" + sname + "'";
+    } catch(...) {
+        // No database name.  Throw a "NoDatabaseName" exception
+        isc_throw(NoDatabaseName, "must specify a name for the database");
+    }
+
+    unsigned int connect_timeout = PGSQL_DEFAULT_CONNECTION_TIMEOUT;
+    string stimeout;
+    try {
+        stimeout = getParameter("connect-timeout");
+    } catch (...) {
+        // No timeout parameter, we are going to use the default timeout.
+        stimeout = "";
+    }
+
+    if (stimeout.size() > 0) {
+        // Timeout was given, so try to convert it to an integer.
+
+        try {
+            connect_timeout = boost::lexical_cast<unsigned int>(stimeout);
+        } catch (...) {
+            // Timeout given but could not be converted to an unsigned int. Set
+            // the connection timeout to an invalid value to trigger throwing
+            // of an exception.
+            connect_timeout = 0;
+        }
+
+        // The timeout is only valid if greater than zero, as depending on the
+        // database, a zero timeout might signify someting like "wait
+        // indefinitely".
+        //
+        // The check below also rejects a value greater than the maximum
+        // integer value.  The lexical_cast operation used to obtain a numeric
+        // value from a string can get confused if trying to convert a negative
+        // integer to an unsigned int: instead of throwing an exception, it may
+        // produce a large positive value.
+        if ((connect_timeout == 0) ||
+            (connect_timeout > numeric_limits<int>::max())) {
+            isc_throw(DbInvalidTimeout, "database connection timeout (" <<
+                      stimeout << ") must be an integer greater than 0");
+        }
+    }
+
+    std::ostringstream oss;
+    oss << connect_timeout;
+    dbconnparameters += " connect_timeout = " + oss.str();
+
+    // Connect to Postgres, saving the low level connection pointer
+    // in the holder object
+    PGconn* new_conn = PQconnectdb(dbconnparameters.c_str());
+    if (!new_conn) {
+        isc_throw(DbOpenError, "could not allocate connection object");
+    }
+
+    if (PQstatus(new_conn) != CONNECTION_OK) {
+        // If we have a connection object, we have to call finish
+        // to release it, but grab the error message first.
+        std::string error_message = PQerrorMessage(new_conn);
+        PQfinish(new_conn);
+        isc_throw(DbOpenError, error_message);
+    }
+
+    // We have a valid connection, so let's save it to our holder
+    conn_.setConnection(new_conn);
+}
+
+bool
+PgSqlConnection::compareError(const PgSqlResult& r, const char* error_state) {
+    const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
+    // PostgreSQL garuantees it will always be 5 characters long
+    return ((sqlstate != NULL) &&
+            (memcmp(sqlstate, error_state, PGSQL_STATECODE_LEN) == 0));
+}
+
+void
+PgSqlConnection::checkStatementError(const PgSqlResult& r,
+                                     PgSqlTaggedStatement& statement) const {
+    int s = PQresultStatus(r);
+    if (s != PGRES_COMMAND_OK && s != PGRES_TUPLES_OK) {
+        // We're testing the first two chars of SQLSTATE, as this is the
+        // error class. Note, there is a severity field, but it can be
+        // misleadingly returned as fatal.
+        const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
+        if ((sqlstate != NULL) &&
+            ((memcmp(sqlstate, "08", 2) == 0) ||  // Connection Exception
+             (memcmp(sqlstate, "53", 2) == 0) ||  // Insufficient resources
+             (memcmp(sqlstate, "54", 2) == 0) ||  // Program Limit exceeded
+             (memcmp(sqlstate, "57", 2) == 0) ||  // Operator intervention
+             (memcmp(sqlstate, "58", 2) == 0))) { // System error
+            LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_FATAL_ERROR)
+                         .arg(statement.name)
+                         .arg(PQerrorMessage(conn_))
+                         .arg(sqlstate);
+            exit (-1);
+        }
+
+        const char* error_message = PQerrorMessage(conn_);
+        isc_throw(DbOperationError, "Statement exec failed:" << " for: "
+                  << statement.name << ", reason: "
+                  << error_message);
+    }
+}
+
+void
+PgSqlConnection::commit() {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_COMMIT);
+    PgSqlResult r(PQexec(conn_, "COMMIT"));
+    if (PQresultStatus(r) != PGRES_COMMAND_OK) {
+        const char* error_message = PQerrorMessage(conn_);
+        isc_throw(DbOperationError, "commit failed: " << error_message);
+    }
+}
+
+void
+PgSqlConnection::rollback() {
+    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ROLLBACK);
+    PgSqlResult r(PQexec(conn_, "ROLLBACK"));
+    if (PQresultStatus(r) != PGRES_COMMAND_OK) {
+        const char* error_message = PQerrorMessage(conn_);
+        isc_throw(DbOperationError, "rollback failed: " << error_message);
+    }
+}
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace

+ 300 - 0
src/lib/dhcpsrv/pgsql_connection.h

@@ -0,0 +1,300 @@
+// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#ifndef PGSQL_CONNECTION_H
+#define PGSQL_CONNECTION_H
+
+#include <dhcpsrv/database_connection.h>
+
+#include <libpq-fe.h>
+#include <boost/scoped_ptr.hpp>
+
+#include <vector>
+
+
+namespace isc {
+namespace dhcp {
+
+// Maximum number of parameters that can be used a statement
+// @todo This allows us to use an initializer list (since we can't
+// require C++11).  It's unlikely we'd go past this many a single
+// statement.
+const size_t PGSQL_MAX_PARAMETERS_IN_QUERY = 32;
+
+/// @brief  Defines a Postgresql SQL statement
+///
+/// Each statement is associated with an index, which is used to reference the
+/// associated prepared statement.
+struct PgSqlTaggedStatement {
+
+    /// Number of parameters for a given query
+    int nbparams;
+
+    /// @brief OID types
+    ///
+    /// Specify parameter types. See /usr/include/postgresql/catalog/pg_type.h.
+    /// For some reason that header does not export those parameters.
+    /// Those OIDs must match both input and output parameters.
+    const Oid types[PGSQL_MAX_PARAMETERS_IN_QUERY];
+
+    /// Short name of the query.
+    const char* name;
+
+    /// Text representation of the actual query.
+    const char* text;
+};
+
+/// @brief Constants for PostgreSQL data types
+/// This are defined by PostreSQL in <catalog/pg_type.h>, but including
+/// this file is extrordinarily convoluted, so we'll use these to fill-in.
+const size_t OID_NONE = 0;   // PostgreSQL infers proper type
+const size_t OID_BOOL = 16;
+const size_t OID_BYTEA = 17;
+const size_t OID_INT8 = 20;  // 8 byte int
+const size_t OID_INT2 = 21;  // 2 byte int
+const size_t OID_TIMESTAMP = 1114;
+const size_t OID_VARCHAR = 1043;
+
+//@}
+
+/// @brief RAII wrapper for Posgtresql Result sets
+///
+/// When a Postgresql statement is executed, the results are returned
+/// in pointer allocated structure, PGresult*. Data and status information
+/// are accessed via calls to functions such as PQgetvalue() which require
+/// the results pointer.  In order to ensure this structure is freed, any
+/// invocation of Psql function which returns a PGresult* (e.g. PQexec and
+
+/// class. Examples:
+/// {{{
+///       PgSqlResult r(PQexec(conn_, "ROLLBACK"));
+/// }}}
+///
+/// This eliminates the need for an explicit release via, PQclear() and
+/// guarantees that the resources are released even if the an exception is
+/// thrown.
+
+class PgSqlResult : public boost::noncopyable {
+public:
+    /// @brief Constructor
+    ///
+    /// Store the pointer to the result set to being fetched.
+    ///
+    PgSqlResult(PGresult *result) : result_(result)
+    {}
+
+    /// @brief Destructor
+    ///
+    /// Frees the result set
+    ~PgSqlResult() {
+        if (result_)  {
+            PQclear(result_);
+        }
+    }
+
+    /// @brief Conversion Operator
+    ///
+    /// Allows the PgSqlResult object to be passed as the result set argument to
+    /// PQxxxx functions.
+    operator PGresult*() const {
+        return (result_);
+    }
+
+    /// @brief Boolean Operator
+    ///
+    /// Allows testing the PgSqlResult object for emptiness: "if (result)"
+    operator bool() const {
+        return (result_);
+    }
+
+private:
+    PGresult*     result_;     ///< Result set to be freed
+};
+
+
+/// @brief Postgresql connection handle Holder
+///
+/// Small RAII object for safer initialization, will close the database
+/// connection upon destruction.  This means that if an exception is thrown
+/// during database initialization, resources allocated to the database are
+/// guaranteed to be freed.
+///
+/// It makes no sense to copy an object of this class.  After the copy, both
+/// objects would contain pointers to the same PgSql context object.  The
+/// destruction of one would invalid the context in the remaining object.
+/// For this reason, the class is declared noncopyable.
+class PgSqlHolder : public boost::noncopyable {
+public:
+
+    /// @brief Constructor
+    ///
+    /// Sets the Postgresql API connector handle to NULL.
+    ///
+    PgSqlHolder() : pgconn_(NULL) {
+    }
+
+    /// @brief Destructor
+    ///
+    /// Frees up resources allocated by the connection.
+    ~PgSqlHolder() {
+        if (pgconn_ != NULL) {
+            PQfinish(pgconn_);
+        }
+    }
+
+    /// @brief Sets the connection to the value given
+    ///
+    /// @param connection - pointer to the Postgresql connection instance
+    void setConnection(PGconn* connection) {
+        if (pgconn_ != NULL) {
+            // Already set? Release the current connection first.
+            // Maybe this should be an error instead?
+            PQfinish(pgconn_);
+        }
+
+        pgconn_ = connection;
+    }
+
+    /// @brief Conversion Operator
+    ///
+    /// Allows the PgSqlHolder object to be passed as the context argument to
+    /// PQxxxx functions.
+    operator PGconn*() const {
+        return (pgconn_);
+    }
+
+    /// @brief Boolean Operator
+    ///
+    /// Allows testing the connection for emptiness: "if (holder)"
+    operator bool() const {
+        return (pgconn_);
+    }
+
+private:
+    PGconn* pgconn_;      ///< Postgresql connection
+};
+
+/// @brief Common PgSql Connector Pool
+///
+/// This class provides common operations for PgSql database connection
+/// used by both PgSqlLeaseMgr and PgSqlHostDataSource. It manages connecting
+/// to the database and preparing compiled statements. Its fields are
+/// public, because they are used (both set and retrieved) in classes
+/// that use instances of PgSqlConnection.
+class PgSqlConnection : public DatabaseConnection {
+public:
+    /// @brief Defines the PgSql error state for a duplicate key error
+    static const char DUPLICATE_KEY[];
+
+    /// @brief Constructor
+    ///
+    /// Initialize PgSqlConnection object with parameters needed for connection.
+    PgSqlConnection(const ParameterMap& parameters)
+        : DatabaseConnection(parameters) {
+    }
+
+    /// @brief Destructor
+    virtual ~PgSqlConnection();
+
+    /// @brief Prepare Single Statement
+    ///
+    /// Creates a prepared statement from the text given and adds it to the
+    /// statements_ vector at the given index.
+    ///
+    /// @param index Index into the statements_ vector into which the text
+    ///        should be placed.  The vector must be big enough for the index
+    ///        to be valid, else an exception will be thrown.
+    /// @param text Text of the SQL statement to be prepared.
+    ///
+    /// @throw isc::dhcp::DbOperationError An operation on the open database has
+    ///        failed.
+    void prepareStatement(const PgSqlTaggedStatement& statement);
+
+    /// @brief Open Database
+    ///
+    /// Opens the database using the information supplied in the parameters
+    /// passed to the constructor.
+    ///
+    /// @throw NoDatabaseName Mandatory database name not given
+    /// @throw DbOpenError Error opening the database
+    void openDatabase();
+
+    /// @brief Commit Transactions
+    ///
+    /// Commits all pending database operations. On databases that don't
+    /// support transactions, this is a no-op.
+    ///
+    /// @throw DbOperationError If the commit failed.
+    void commit();
+
+    /// @brief Rollback Transactions
+    ///
+    /// Rolls back all pending database operations. On databases that don't
+    /// support transactions, this is a no-op.
+    ///
+    /// @throw DbOperationError If the rollback failed.
+    void rollback();
+
+    /// @brief Checks a result set's SQL state against an error state.
+    ///
+    /// @param r result set to check
+    /// @param error_state error state to compare against
+    ///
+    /// @return True if the result set's SQL state equals the error_state,
+    /// false otherwise.
+    bool compareError(const PgSqlResult& r, const char* error_state);
+
+    /// @brief Checks result of the r object
+    ///
+    /// This function is used to determine whether or not the SQL statement
+    /// execution succeeded, and in the event of failures, decide whether or
+    /// not the failures are recoverable.
+    ///
+    /// If the error is recoverable, the method will throw a DbOperationError.
+    /// In the error is deemed unrecoverable, such as a loss of connectivity
+    /// with the server, this method will log the error and call exit(-1);
+    ///
+    /// @todo Calling exit() is viewed as a short term solution for Kea 1.0.
+    /// Two tickets are likely to alter this behavior, first is #3639, which
+    /// calls for the ability to attempt to reconnect to the database. The
+    /// second ticket, #4087 which calls for the implementation of a generic,
+    /// FatalException class which will propagate outward.
+    ///
+    /// @param r result of the last PostgreSQL operation
+    /// @param statement - tagged statement that was executed
+    ///
+    /// @throw isc::dhcp::DbOperationError Detailed PostgreSQL failure
+    void checkStatementError(const PgSqlResult& r,
+                             PgSqlTaggedStatement& statement) const;
+
+    /// @brief PgSql connection handle
+    ///
+    /// This field is public, because it is used heavily from PgSqlLeaseMgr
+    /// and from PgSqlHostDataSource.
+    PgSqlHolder conn_;
+
+    /// @brief Conversion Operator
+    ///
+    /// Allows the PgConnection object to be passed as the context argument to
+    /// PQxxxx functions.
+    operator PGconn*() const {
+        return (conn_);
+    }
+
+    /// @brief Boolean Operator
+    ///
+    /// Allows testing the PgConnection for initialized connection
+    operator bool() const {
+        return (conn_);
+    }
+
+};
+
+
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // PGSQL_CONNECTION_H

+ 223 - 0
src/lib/dhcpsrv/pgsql_exchange.cc

@@ -0,0 +1,223 @@
+// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <dhcpsrv/pgsql_exchange.h>
+
+#include <boost/lexical_cast.hpp>
+
+#include <iomanip>
+#include <sstream>
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+
+const int PsqlBindArray::TEXT_FMT = 0;
+const int PsqlBindArray::BINARY_FMT = 1;
+const char* PsqlBindArray::TRUE_STR = "TRUE";
+const char* PsqlBindArray::FALSE_STR = "FALSE";
+
+void PsqlBindArray::add(const char* value) {
+    values_.push_back(value);
+    lengths_.push_back(strlen(value));
+    formats_.push_back(TEXT_FMT);
+}
+
+void PsqlBindArray::add(const std::string& value) {
+    values_.push_back(value.c_str());
+    lengths_.push_back(value.size());
+    formats_.push_back(TEXT_FMT);
+}
+
+void PsqlBindArray::add(const std::vector<uint8_t>& data) {
+    values_.push_back(reinterpret_cast<const char*>(&(data[0])));
+    lengths_.push_back(data.size());
+    formats_.push_back(BINARY_FMT);
+}
+
+void PsqlBindArray::add(const bool& value)  {
+    add(value ? TRUE_STR : FALSE_STR);
+}
+
+std::string PsqlBindArray::toText() const {
+    std::ostringstream stream;
+    for (int i = 0; i < values_.size(); ++i) {
+        stream << i << " : ";
+        if (formats_[i] == TEXT_FMT) {
+            stream << "\"" << values_[i] << "\"" << std::endl;
+        } else {
+            const char *data = values_[i];
+            if (lengths_[i] == 0) {
+                stream << "empty" << std::endl;
+            } else {
+                stream << "0x";
+                for (int i = 0; i < lengths_[i]; ++i) {
+                    stream << std::setfill('0') << std::setw(2)
+                           << std::setbase(16)
+                           << static_cast<unsigned int>(data[i]);
+                }
+                stream << std::endl;
+            }
+        }
+    }
+
+    return (stream.str());
+}
+
+std::string
+PgSqlExchange::convertToDatabaseTime(const time_t input_time) {
+    struct tm tinfo;
+    char buffer[20];
+    localtime_r(&input_time, &tinfo);
+    strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tinfo);
+    return (std::string(buffer));
+}
+
+std::string
+PgSqlExchange::convertToDatabaseTime(const time_t cltt,
+                                     const uint32_t valid_lifetime) {
+    // Calculate expiry time. Store it in the 64-bit value so as we can
+    // detect overflows.
+    int64_t expire_time_64 = static_cast<int64_t>(cltt)
+                             + static_cast<int64_t>(valid_lifetime);
+
+    // It has been observed that the PostgreSQL doesn't deal well with the
+    // timestamp values beyond the DataSource::MAX_DB_TIME seconds since the
+    // beginning of the epoch (around year 2038). The value is often
+    // stored in the database but it is invalid when read back (overflow?).
+    // Hence, the maximum timestamp value is restricted here.
+    if (expire_time_64 > DatabaseConnection::MAX_DB_TIME) {
+        isc_throw(isc::BadValue, "Time value is too large: " << expire_time_64);
+    }
+
+    return (convertToDatabaseTime(static_cast<time_t>(expire_time_64)));
+}
+
+time_t
+PgSqlExchange::convertFromDatabaseTime(const std::string& db_time_val) {
+    // Convert string time value to time_t
+    time_t new_time;
+    try  {
+        new_time = (boost::lexical_cast<time_t>(db_time_val));
+    } catch (const std::exception& ex) {
+        isc_throw(BadValue, "Database time value is invalid: " << db_time_val);
+    }
+
+    return (new_time);
+}
+
+const char*
+PgSqlExchange::getRawColumnValue(const PgSqlResult& r, const int row,
+                                 const size_t col) const {
+    const char* value = PQgetvalue(r, row, col);
+    if (!value) {
+        isc_throw(DbOperationError, "getRawColumnValue no data for :"
+                    << getColumnLabel(col) << " row:" << row);
+    }
+    return (value);
+}
+
+void
+PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
+                              const size_t col, bool &value) const {
+    const char* data = getRawColumnValue(r, row, col);
+    if (!strlen(data) || *data == 'f') {
+        value = false;
+    } else if (*data == 't') {
+        value = true;
+    } else {
+        isc_throw(DbOperationError, "Invalid boolean data: " << data
+                  << " for: " << getColumnLabel(col) << " row:" << row
+                  << " : must be 't' or 'f'");
+    }
+}
+
+void
+PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
+                              const size_t col, uint32_t &value) const {
+    const char* data = getRawColumnValue(r, row, col);
+    try {
+        value = boost::lexical_cast<uint32_t>(data);
+    } catch (const std::exception& ex) {
+        isc_throw(DbOperationError, "Invalid uint32_t data: " << data
+                  << " for: " << getColumnLabel(col) << " row:" << row
+                  << " : " << ex.what());
+    }
+}
+
+void
+PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
+                              const size_t col, int32_t &value) const {
+    const char* data = getRawColumnValue(r, row, col);
+    try {
+        value = boost::lexical_cast<int32_t>(data);
+    } catch (const std::exception& ex) {
+        isc_throw(DbOperationError, "Invalid int32_t data: " << data
+                  << " for: " << getColumnLabel(col) << " row:" << row
+                  << " : " << ex.what());
+    }
+}
+
+void
+PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
+                              const size_t col, uint8_t &value) const {
+    const char* data = getRawColumnValue(r, row, col);
+    try {
+        // lexically casting as uint8_t doesn't convert from char
+        // so we use uint16_t and implicitly convert.
+        value = boost::lexical_cast<uint16_t>(data);
+    } catch (const std::exception& ex) {
+        isc_throw(DbOperationError, "Invalid uint8_t data: " << data
+                  << " for: " << getColumnLabel(col) << " row:" << row
+                  << " : " << ex.what());
+    }
+}
+
+void
+PgSqlExchange::convertFromBytea(const PgSqlResult& r, const int row,
+                                const size_t col, uint8_t* buffer,
+                                const size_t buffer_size,
+                                size_t &bytes_converted) const {
+    // Returns converted bytes in a dynamically allocated buffer, and
+    // sets bytes_converted.
+    unsigned char* bytes = PQunescapeBytea((const unsigned char*)
+                                           (getRawColumnValue(r, row, col)),
+                                           &bytes_converted);
+
+    // Unlikely it couldn't allocate it but you never know.
+    if (!bytes) {
+        isc_throw (DbOperationError, "PQunescapeBytea failed for:"
+                   << getColumnLabel(col) << " row:" << row);
+    }
+
+    // Make sure it's not larger than expected.
+    if (bytes_converted > buffer_size) {
+        // Free the allocated buffer first!
+        PQfreemem(bytes);
+        isc_throw (DbOperationError, "Converted data size: "
+                   << bytes_converted << " is too large for: "
+                   << getColumnLabel(col) << " row:" << row);
+    }
+
+    // Copy from the allocated buffer to caller's buffer the free up
+    // the allocated buffer.
+    memcpy(buffer, bytes, bytes_converted);
+    PQfreemem(bytes);
+}
+
+std::string
+PgSqlExchange::getColumnLabel(const size_t column) const {
+    if (column > column_labels_.size()) {
+        std::ostringstream os;
+        os << "Unknown column:" << column;
+        return (os.str());
+    }
+
+    return (column_labels_[column]);
+}
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace

+ 249 - 0
src/lib/dhcpsrv/pgsql_exchange.h

@@ -0,0 +1,249 @@
+// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#ifndef PGSQL_EXCHANGE_H
+#define PGSQL_EXCHANGE_H
+
+#include <dhcpsrv/pgsql_connection.h>
+
+#include <vector>
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Structure used to bind C++ input values to dynamic SQL parameters
+/// The structure contains three vectors which store the input values,
+/// data lengths, and formats.  These vectors are passed directly into the
+/// PostgreSQL execute call.
+///
+/// Note that the data values are stored as pointers. These pointers need to
+/// be valid for the duration of the PostgreSQL statement execution.  In other
+/// words populating them with pointers to values that go out of scope before
+/// statement is executed is a bad idea.
+struct PsqlBindArray {
+    /// @brief Vector of pointers to the data values.
+    std::vector<const char *> values_;
+    /// @brief Vector of data lengths for each value.
+    std::vector<int> lengths_;
+    /// @brief Vector of "format" for each value. A value of 0 means the
+    /// value is text, 1 means the value is binary.
+    std::vector<int> formats_;
+
+    /// @brief Format value for text data.
+    static const int TEXT_FMT;
+    /// @brief Format value for binary data.
+    static const int BINARY_FMT;
+
+    /// @brief Constant string passed to DB for boolean true values.
+    static const char* TRUE_STR;
+    /// @brief Constant string passed to DB for boolean false values.
+    static const char* FALSE_STR;
+
+    /// @brief Fetches the number of entries in the array.
+    /// @return Returns size_t containing the number of entries.
+    size_t size() const {
+        return (values_.size());
+    }
+
+    /// @brief Indicates it the array is empty.
+    /// @return Returns true if there are no entries in the array, false
+    /// otherwise.
+    bool empty() const {
+
+        return (values_.empty());
+    }
+
+    /// @brief Adds a char array to bind array based
+    ///
+    /// Adds a TEXT_FMT value to the end of the bind array, using the given
+    /// char* as the data source. Note that value is expected to be NULL
+    /// terminated.
+    ///
+    /// @param value char array containing the null-terminated text to add.
+    void add(const char* value);
+
+    /// @brief Adds an string value to the bind array
+    ///
+    /// Adds a TEXT formatted value to the end of the bind array using the
+    /// given string as the data source.
+    ///
+    /// @param value std::string containing the value to add.
+    void add(const std::string& value);
+
+    /// @brief Adds a binary value to the bind array.
+    ///
+    /// Adds a BINARY_FMT value to the end of the bind array using the
+    /// given vector as the data source.
+    ///
+    /// @param data vector of binary bytes.
+    void add(const std::vector<uint8_t>& data);
+
+    /// @brief Adds a boolean value to the bind array.
+    ///
+    /// Converts the given boolean value to its corresponding to PostgreSQL
+    /// string value and adds it as a TEXT_FMT value to the bind array.
+    ///
+    /// @param value bool value to add.
+    void add(const bool& value);
+
+    /// @brief Dumps the contents of the array to a string.
+    /// @return std::string containing the dump
+    std::string toText() const;
+};
+
+/// @brief Base class for marshalling data to and from PostgreSQL.
+///
+/// Provides the common functionality to set up binding information between
+/// application objects in the program and their representation in the
+/// database, and for retrieving column values from rows of a result set.
+class PgSqlExchange {
+public:
+    /// @brief Constructor
+    PgSqlExchange(){}
+
+    /// @brief Destructor
+    virtual ~PgSqlExchange(){}
+
+    /// @brief Converts time_t value to a text representation in local time.
+    ///
+    /// @param input_time A time_t value representing time.
+    /// @return std::string containing stringified time.
+    static std::string convertToDatabaseTime(const time_t input_time);
+
+    /// @brief Converts lease expiration time to a text representation in
+    /// local time.
+    ///
+    /// The expiration time is calculated as a sum of the cltt (client last
+    /// transmit time) and the valid lifetime.
+    ///
+    /// The format of the output string is "%Y-%m-%d %H:%M:%S".  Database
+    /// table columns using this value should be typed as TIMESTAMP WITH
+    /// TIME ZONE. For such columns PostgreSQL assumes input strings without
+    /// timezones should be treated as in local time and are converted to UTC
+    /// when stored.  Likewise, these columns are automatically adjusted
+    /// upon retrieval unless fetched via "extract(epoch from <column>))".
+    ///
+    /// Unless we start using binary input, timestamp columns must be input as
+    /// date/time strings.
+    ///
+    /// @param cltt Client last transmit time
+    /// @param valid_lifetime Valid lifetime
+    ///
+    /// @return std::string containing the stringified time
+    /// @throw isc::BadValue if the sum of the calculated expiration time is
+    /// greater than the value of @c DataSource::MAX_DB_TIME.
+    static std::string convertToDatabaseTime(const time_t cltt,
+                                             const uint32_t valid_lifetime);
+
+    /// @brief Converts time stamp from the database to a time_t
+    ///
+    /// We're fetching timestamps as an integer string of seconds since the
+    /// epoch.  This method converts such a string int a time_t.
+    ///
+    /// @param db_time_val timestamp to be converted.  This value
+    /// is expected to be the number of seconds since the epoch
+    /// expressed as base-10 integer string.
+    /// @return Converted timestamp as time_t value.
+    static time_t convertFromDatabaseTime(const std::string& db_time_val);
+
+    /// @brief Gets a pointer to the raw column value in a result set row
+    ///
+    /// Given a result set, row, and column return a const char* pointer to
+    /// the data value in the result set.  The pointer is valid as long as
+    /// the result set has not been freed.  It may point to text or binary
+    /// data depending on how query was structured.  You should not attempt
+    /// to free this pointer.
+    ///
+    /// @param r the result set containing the query results
+    /// @param row the row number within the result set
+    /// @param col the column number within the row
+    ///
+    /// @return a const char* pointer to the column's raw data
+    /// @throw  DbOperationError if the value cannot be fetched.
+    const char* getRawColumnValue(const PgSqlResult& r, const int row,
+                                  const size_t col) const;
+
+    /// @brief Fetches boolean text ('t' or 'f') as a bool.
+    ///
+    /// @param r the result set containing the query results
+    /// @param row the row number within the result set
+    /// @param col the column number within the row
+    /// @param[out] value parameter to receive the converted value
+    ///
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
+                        bool &value) const;
+
+    /// @brief Fetches an integer text column as a uint32_t.
+    ///
+    /// @param r the result set containing the query results
+    /// @param row the row number within the result set
+    /// @param col the column number within the row
+    /// @param[out] value parameter to receive the converted value
+    ///
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
+                        uint32_t &value) const;
+
+    /// @brief Fetches an integer text column as a int32_t.
+    ///
+    /// @param r the result set containing the query results
+    /// @param row the row number within the result set
+    /// @param col the column number within the row
+    /// @param[out] value parameter to receive the converted value
+    ///
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
+                        int32_t &value) const;
+
+    /// @brief Fetches an integer text column as a uint8_t.
+    ///
+    /// @param r the result set containing the query results
+    /// @param row the row number within the result set
+    /// @param col the column number within the row
+    /// @param[out] value parameter to receive the converted value
+    ///
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    void getColumnValue(const PgSqlResult& r, const int row, const size_t col,
+                        uint8_t &value) const;
+
+    /// @brief Converts a column in a row in a result set to a binary bytes
+    ///
+    /// Method is used to convert columns stored as BYTEA into a buffer of
+    /// binary bytes, (uint8_t).  It uses PQunescapeBytea to do the conversion.
+    ///
+    /// @param r the result set containing the query results
+    /// @param row the row number within the result set
+    /// @param col the column number within the row
+    /// @param[out] buffer pre-allocated buffer to which the converted bytes
+    /// will be stored.
+    /// @param buffer_size size of the output buffer
+    /// @param[out] bytes_converted number of bytes converted
+    /// value
+    ///
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    void convertFromBytea(const PgSqlResult& r, const int row, const size_t col,
+                          uint8_t* buffer, const size_t buffer_size,
+                          size_t &bytes_converted) const;
+
+    /// @brief Returns column label given a column number
+    std::string getColumnLabel(const size_t column) const;
+
+protected:
+    /// @brief Stores text labels for columns, currently only used for
+    /// logging and errors.
+    std::vector<std::string>column_labels_;
+};
+
+}; // end of isc::dhcp namespace
+}; // end of isc namespace
+
+#endif // PGSQL_EXCHANGE_H

+ 77 - 606
src/lib/dhcpsrv/pgsql_lease_mgr.cc

@@ -20,67 +20,16 @@
 #include <string>
 #include <time.h>
 
-// PostgreSQL errors should be tested based on the SQL state code.  Each state
-// code is 5 decimal, ASCII, digits, the first two define the category of
-// error, the last three are the specific error.  PostgreSQL makes the state
-// code as a char[5].  Macros for each code are defined in PostgreSQL's
-// errorcodes.h, although they require a second macro, MAKE_SQLSTATE for
-// completion.  PostgreSQL deliberately omits this macro from errocodes.h
-// so callers can supply their own.
-#define MAKE_SQLSTATE(ch1,ch2,ch3,ch4,ch5) {ch1,ch2,ch3,ch4,ch5}
-#include <utils/errcodes.h>
-const size_t STATECODE_LEN = 5;
-
-// Currently the only one we care to look for is duplicate key.
-const char DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
-
 using namespace isc;
 using namespace isc::dhcp;
 using namespace std;
 
 namespace {
 
-// Default connection timeout
-const int PGSQL_DEFAULT_CONNECTION_TIMEOUT = 5;	// seconds
-
-// Maximum number of parameters used in any single query
-const size_t MAX_PARAMETERS_IN_QUERY = 14;
-
-/// @brief  Defines a single query
-struct TaggedStatement {
-
-    /// Number of parameters for a given query
-    int nbparams;
-
-    /// @brief OID types
-    ///
-    /// Specify parameter types. See /usr/include/postgresql/catalog/pg_type.h.
-    /// For some reason that header does not export those parameters.
-    /// Those OIDs must match both input and output parameters.
-    const Oid types[MAX_PARAMETERS_IN_QUERY];
-
-    /// Short name of the query.
-    const char* name;
-
-    /// Text representation of the actual query.
-    const char* text;
-};
-
-/// @brief Constants for PostgreSQL data types
-/// This are defined by PostreSQL in <catalog/pg_type.h>, but including
-/// this file is extrordinarily convoluted, so we'll use these to fill-in.
-const size_t OID_NONE = 0;   // PostgreSQL infers proper type
-const size_t OID_BOOL = 16;
-const size_t OID_BYTEA = 17;
-const size_t OID_INT8 = 20;  // 8 byte int
-const size_t OID_INT2 = 21;  // 2 byte int
-const size_t OID_TIMESTAMP = 1114;
-const size_t OID_VARCHAR = 1043;
-
 /// @brief Catalog of all the SQL statements currently supported.  Note
 /// that the order columns appear in statement body must match the order they
 /// that the occur in the table.  This does not apply to the where clause.
-TaggedStatement tagged_statements[] = {
+PgSqlTaggedStatement tagged_statements[] = {
     // DELETE_LEASE4
     { 1, { OID_INT8 },
       "delete_lease4",
@@ -258,66 +207,14 @@ TaggedStatement tagged_statements[] = {
 namespace isc {
 namespace dhcp {
 
-const int PsqlBindArray::TEXT_FMT = 0;
-const int PsqlBindArray::BINARY_FMT = 1;
-const char* PsqlBindArray::TRUE_STR = "TRUE";
-const char* PsqlBindArray::FALSE_STR = "FALSE";
-
-void PsqlBindArray::add(const char* value) {
-    values_.push_back(value);
-    lengths_.push_back(strlen(value));
-    formats_.push_back(TEXT_FMT);
-}
-
-void PsqlBindArray::add(const std::string& value) {
-    values_.push_back(value.c_str());
-    lengths_.push_back(value.size());
-    formats_.push_back(TEXT_FMT);
-}
-
-void PsqlBindArray::add(const std::vector<uint8_t>& data) {
-    values_.push_back(reinterpret_cast<const char*>(&(data[0])));
-    lengths_.push_back(data.size());
-    formats_.push_back(BINARY_FMT);
-}
-
-void PsqlBindArray::add(const bool& value)  {
-    add(value ? TRUE_STR : FALSE_STR);
-}
-
-std::string PsqlBindArray::toText() {
-    std::ostringstream stream;
-    for (int i = 0; i < values_.size(); ++i) {
-        stream << i << " : ";
-        if (formats_[i] == TEXT_FMT) {
-            stream << "\"" << values_[i] << "\"" << std::endl;
-        } else {
-            const char *data = values_[i];
-            if (lengths_[i] == 0) {
-                stream << "empty" << std::endl;
-            } else {
-                stream << "0x";
-                for (int i = 0; i < lengths_[i]; ++i) {
-                    stream << setfill('0') << setw(2) << setbase(16)
-                         << static_cast<unsigned int>(data[i]);
-                }
-                stream << std::endl;
-            }
-        }
-    }
-
-    return (stream.str());
-}
-
 /// @brief Base class for marshalling leases to and from PostgreSQL.
 ///
 /// Provides the common functionality to set up binding information between
 /// lease objects in the program and their database representation in the
 /// database.
-class PgSqlLeaseExchange {
+class PgSqlLeaseExchange : public PgSqlExchange {
 public:
 
-
     PgSqlLeaseExchange()
         : addr_str_(""), valid_lifetime_(0), valid_lft_str_(""),
           expire_(0), expire_str_(""), subnet_id_(0), subnet_id_str_(""),
@@ -327,282 +224,7 @@ public:
 
     virtual ~PgSqlLeaseExchange(){}
 
-    /// @brief Converts time_t value to a text representation in local time.
-    ///
-    /// @param input_time A time_t value representing time.
-    /// @return std::string containing stringified time.
-    static std::string
-    convertToDatabaseTime(const time_t input_time) {
-        struct tm tinfo;
-        char buffer[20];
-        localtime_r(&input_time, &tinfo);
-        strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tinfo);
-        return (std::string(buffer));
-    }
-
-    /// @brief Converts lease expiration time to a text representation in
-    /// local time.
-    ///
-    /// The expiration time is calculated as a sum of the cltt (client last
-    /// transmit time) and the valid lifetime.
-    ///
-    /// The format of the output string is "%Y-%m-%d %H:%M:%S".  Database
-    /// table columns using this value should be typed as TIMESTAMP WITH
-    /// TIME ZONE. For such columns PostgreSQL assumes input strings without
-    /// timezones should be treated as in local time and are converted to UTC
-    /// when stored.  Likewise, these columns are automatically adjusted
-    /// upon retrieval unless fetched via "extract(epoch from <column>))".
-    ///
-    /// @param cltt Client last transmit time
-    /// @param valid_lifetime Valid lifetime
-    ///
-    /// @return std::string containing the stringified time
-    /// @throw isc::BadValue if the sum of the calculated expiration time is
-    /// greater than the value of @c DataSource::MAX_DB_TIME.
-    static std::string
-    convertToDatabaseTime(const time_t cltt, const uint32_t valid_lifetime) {
-        // Calculate expiry time. Store it in the 64-bit value so as we can detect
-        // overflows.
-        int64_t expire_time_64 = static_cast<int64_t>(cltt) +
-            static_cast<int64_t>(valid_lifetime);
-
-        // It has been observed that the PostgreSQL doesn't deal well with the
-        // timestamp values beyond the DataSource::MAX_DB_TIME seconds since the
-        // beginning of the epoch (around year 2038). The value is often
-        // stored in the database but it is invalid when read back (overflow?).
-        // Hence, the maximum timestamp value is restricted here.
-        if (expire_time_64 > DatabaseConnection::MAX_DB_TIME) {
-            isc_throw(isc::BadValue, "Time value is too large: " << expire_time_64);
-        }
-
-        return (convertToDatabaseTime(static_cast<time_t>(expire_time_64)));
-    }
-
-    /// @brief Converts time stamp from the database to a time_t
-    ///
-    /// @param db_time_val timestamp to be converted.  This value
-    /// is expected to be the number of seconds since the epoch
-    /// expressed as base-10 integer string.
-    /// @return Converted timestamp as time_t value.
-    static time_t convertFromDatabaseTime(const std::string& db_time_val) {
-        // Convert string time value to time_t
-        try  {
-            return (boost::lexical_cast<time_t>(db_time_val));
-        } catch (const std::exception& ex) {
-            isc_throw(BadValue, "Database time value is invalid: "
-                                << db_time_val);
-        }
-    }
-
-    /// @brief Gets a pointer to the raw column value in a result set row
-    ///
-    /// Given a result set, row, and column return a const char* pointer to
-    /// the data value in the result set.  The pointer is valid as long as
-    /// the result set has not been freed.  It may point to text or binary
-    /// data depending on how query was structured.  You should not attempt
-    /// to free this pointer.
-    ///
-    /// @param r the result set containing the query results
-    /// @param row the row number within the result set
-    /// @param col the column number within the row
-    ///
-    /// @return a const char* pointer to the column's raw data
-    /// @throw  DbOperationError if the value cannot be fetched.
-    const char* getRawColumnValue(PGresult*& r, const int row,
-                                  const size_t col) const {
-        const char* value = PQgetvalue(r, row, col);
-        if (!value) {
-            isc_throw(DbOperationError, "getRawColumnValue no data for :"
-                      << getColumnLabel(col) << " row:" << row);
-        }
-
-        return (value);
-    }
-
-    /// @brief Fetches boolean text ('t' or 'f') as a bool.
-    ///
-    /// @param r the result set containing the query results
-    /// @param row the row number within the result set
-    /// @param col the column number within the row
-    /// @param[out] value parameter to receive the converted value
-    ///
-    /// @throw  DbOperationError if the value cannot be fetched or is
-    /// invalid.
-    void getColumnValue(PGresult*& r, const int row, const size_t col,
-                        bool &value) const {
-        const char* data = getRawColumnValue(r, row, col);
-        if (!strlen(data) || *data == 'f') {
-            value = false;
-        } else if (*data == 't') {
-            value = true;
-        } else {
-            isc_throw(DbOperationError, "Invalid boolean data: " << data
-                      << " for: " << getColumnLabel(col) << " row:" << row
-                      << " : must be 't' or 'f'");
-        }
-    }
-
-    /// @brief Fetches an integer text column as a uint32_t.
-    ///
-    /// @param r the result set containing the query results
-    /// @param row the row number within the result set
-    /// @param col the column number within the row
-    /// @param[out] value parameter to receive the converted value
-    ///
-    /// @throw  DbOperationError if the value cannot be fetched or is
-    /// invalid.
-    void getColumnValue(PGresult*& r, const int row, const size_t col,
-                        uint32_t &value) const {
-        const char* data = getRawColumnValue(r, row, col);
-        try {
-            value = boost::lexical_cast<uint32_t>(data);
-        } catch (const std::exception& ex) {
-            isc_throw(DbOperationError, "Invalid uint32_t data: " << data
-                      << " for: " << getColumnLabel(col) << " row:" << row
-                      << " : " << ex.what());
-        }
-    }
-
-    /// @brief Fetches an integer text column as a int32_t.
-    ///
-    /// @param r the result set containing the query results
-    /// @param row the row number within the result set
-    /// @param col the column number within the row
-    /// @param[out] value parameter to receive the converted value
-    ///
-    /// @throw  DbOperationError if the value cannot be fetched or is
-    /// invalid.
-    void getColumnValue(PGresult*& r, const int row, const size_t col,
-                        int32_t &value) const {
-        const char* data = getRawColumnValue(r, row, col);
-        try {
-            value = boost::lexical_cast<int32_t>(data);
-        } catch (const std::exception& ex) {
-            isc_throw(DbOperationError, "Invalid int32_t data: " << data
-                      << " for: " << getColumnLabel(col) << " row:" << row
-                      << " : " << ex.what());
-        }
-    }
-
-    /// @brief Fetches an integer text column as a uint8_t.
-    ///
-    /// @param r the result set containing the query results
-    /// @param row the row number within the result set
-    /// @param col the column number within the row
-    /// @param[out] value parameter to receive the converted value
-    ///
-    /// @throw  DbOperationError if the value cannot be fetched or is
-    /// invalid.
-    void getColumnValue(PGresult*& r, const int row, const size_t col,
-                        uint8_t &value) const {
-        const char* data = getRawColumnValue(r, row, col);
-        try {
-            // lexically casting as uint8_t doesn't convert from char
-            // so we use uint16_t and implicitly convert.
-            value = boost::lexical_cast<uint16_t>(data);
-        } catch (const std::exception& ex) {
-            isc_throw(DbOperationError, "Invalid uint8_t data: " << data
-                      << " for: " << getColumnLabel(col) << " row:" << row
-                      << " : " << ex.what());
-        }
-    }
-
-    /// @brief Fetches an integer text column as a Lease6::Type
-    ///
-    /// @param r the result set containing the query results
-    /// @param row the row number within the result set
-    /// @param col the column number within the row
-    /// @param[out] value parameter to receive the converted value
-    ///
-    /// @throw  DbOperationError if the value cannot be fetched or is
-    /// invalid.
-    void getColumnValue(PGresult*& r, const int row, const size_t col,
-                        Lease6::Type& value) const {
-        uint32_t raw_value = 0;
-        getColumnValue(r, row , col, raw_value);
-        switch (raw_value) {
-            case Lease6::TYPE_NA:
-                value = Lease6::TYPE_NA;
-                break;
-
-            case Lease6::TYPE_TA:
-                value = Lease6::TYPE_TA;
-                break;
-
-            case Lease6::TYPE_PD:
-                value = Lease6::TYPE_PD;
-                break;
-
-            default:
-                isc_throw(DbOperationError, "Invalid lease type: " << raw_value
-                      << " for: " << getColumnLabel(col) << " row:" << row);
-        }
-    }
-
-    /// @brief Converts a column in a row in a result set to a binary bytes
-    ///
-    /// Method is used to convert columns stored as BYTEA into a buffer of
-    /// binary bytes, (uint8_t).  It uses PQunescapeBytea to do the conversion.
-    ///
-    /// @param r the result set containing the query results
-    /// @param row the row number within the result set
-    /// @param col the column number within the row
-    /// @param[out] buffer pre-allocated buffer to which the converted bytes
-    /// will be stored.
-    /// @param buffer_size size of the output buffer
-    /// @param[out] bytes_converted number of bytes converted
-    /// value
-    ///
-    /// @throw  DbOperationError if the value cannot be fetched or is
-    /// invalid.
-    void convertFromBytea(PGresult*& r, const int row, const size_t col,
-                          uint8_t* buffer,
-                          const size_t buffer_size,
-                          size_t &bytes_converted) const {
-
-        // Returns converted bytes in a dynamically allocated buffer, and
-        // sets bytes_converted.
-        unsigned char* bytes = PQunescapeBytea((const unsigned char*)
-                                               (getRawColumnValue(r, row, col)),
-                                               &bytes_converted);
-
-        // Unlikely it couldn't allocate it but you never know.
-        if (!bytes) {
-            isc_throw (DbOperationError, "PQunescapeBytea failed for:"
-                       << getColumnLabel(col) << " row:" << row);
-        }
-
-        // Make sure it's not larger than expected.
-        if (bytes_converted > buffer_size) {
-            // Free the allocated buffer first!
-            PQfreemem(bytes);
-            isc_throw (DbOperationError, "Converted data size: "
-                       << bytes_converted << " is too large for: "
-                       << getColumnLabel(col) << " row:" << row);
-        }
-
-        // Copy from the allocated buffer to caller's buffer the free up
-        // the allocated buffer.
-        memcpy(buffer, bytes, bytes_converted);
-        PQfreemem(bytes);
-    }
-
-    /// @brief Returns column label given a column number
-    std::string getColumnLabel(const size_t column) const {
-        if (column > column_labels_.size()) {
-            ostringstream os;
-            os << "Unknown column:" << column;
-            return (os.str());
-        }
-
-        return (column_labels_[column]);
-    }
-
 protected:
-    /// @brief Stores text labels for columns, currently only used for
-    /// logging and errors.
-    std::vector<std::string>column_labels_;
-
     /// @brief Common Instance members used for binding and conversion
     //@{
     std::string addr_str_;
@@ -748,7 +370,7 @@ public:
     ///
     /// @return Lease4Ptr to the newly created Lease4 object
     /// @throw DbOperationError if the lease cannot be created.
-    Lease4Ptr convertFromDatabase(PGresult*& r, int row) {
+    Lease4Ptr convertFromDatabase(const PgSqlResult& r, int row) {
         try {
             getColumnValue(r, row, ADDRESS_COL, addr4_);
 
@@ -936,7 +558,7 @@ public:
     ///
     /// @return Lease6Ptr to the newly created Lease4 object
     /// @throw DbOperationError if the lease cannot be created.
-    Lease6Ptr convertFromDatabase(PGresult*& r, int row) {
+    Lease6Ptr convertFromDatabase(const PgSqlResult& r, int row) {
         try {
             isc::asiolink::IOAddress addr(getIPv6Value(r, row, ADDRESS_COL));
 
@@ -955,7 +577,7 @@ public:
 
             getColumnValue(r, row , PREF_LIFETIME_COL, pref_lifetime_);
 
-            getColumnValue(r, row, LEASE_TYPE_COL, lease_type_);
+            getLeaseTypeColumnValue(r, row, LEASE_TYPE_COL, lease_type_);
 
             getColumnValue(r, row , IAID_COL, iaid_u_.ival_);
 
@@ -983,6 +605,35 @@ public:
         }
     }
 
+    /// @brief Fetches an integer text column as a Lease6::Type
+    ///
+    /// @param r the result set containing the query results
+    /// @param row the row number within the result set
+    /// @param col the column number within the row
+    /// @param[out] value parameter to receive the converted value
+    ///
+    /// Note we depart from overloading getColumnValue to avoid ambiguity
+    /// with base class methods for integers.
+    ///
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    void getLeaseTypeColumnValue(const PgSqlResult& r, const int row,
+                                 const size_t col, Lease6::Type& value) const {
+        uint32_t raw_value = 0;
+        getColumnValue(r, row , col, raw_value);
+        switch (raw_value) {
+            case Lease6::TYPE_NA:
+            case Lease6::TYPE_TA:
+            case Lease6::TYPE_PD:
+                value = static_cast<Lease6::Type>(raw_value);
+                break;
+
+            default:
+                isc_throw(DbOperationError, "Invalid lease type: " << raw_value
+                      << " for: " << getColumnLabel(col) << " row:" << row);
+        }
+    }
+
     /// @brief Converts a column in a row in a result set into IPv6 address.
     ///
     /// @param r the result set containing the query results
@@ -992,7 +643,7 @@ public:
     /// @return isc::asiolink::IOAddress containing the IPv6 address.
     /// @throw  DbOperationError if the value cannot be fetched or is
     /// invalid.
-    isc::asiolink::IOAddress getIPv6Value(PGresult*& r, const int row,
+    isc::asiolink::IOAddress getIPv6Value(const PgSqlResult& r, const int row,
                                           const size_t col) const {
         const char* data = getRawColumnValue(r, row, col);
         try {
@@ -1041,25 +692,21 @@ private:
 
 PgSqlLeaseMgr::PgSqlLeaseMgr(const DatabaseConnection::ParameterMap& parameters)
     : LeaseMgr(), exchange4_(new PgSqlLease4Exchange()),
-    exchange6_(new PgSqlLease6Exchange()), dbconn_(parameters), conn_(NULL) {
-    openDatabase();
-    prepareStatements();
+    exchange6_(new PgSqlLease6Exchange()), conn_(parameters) {
+    conn_.openDatabase();
+    int i = 0;
+    for( ; tagged_statements[i].text != NULL ; ++i) {
+        conn_.prepareStatement(tagged_statements[i]);
+    }
+
+    // Just in case somebody foo-barred things
+    if (i != NUM_STATEMENTS) {
+        isc_throw(DbOpenError, "Number of statements prepared: " << i
+                  << " does not match expected count:" << NUM_STATEMENTS);
+    }
 }
 
 PgSqlLeaseMgr::~PgSqlLeaseMgr() {
-    if (conn_) {
-        // Deallocate the prepared queries.
-        PGresult* r = PQexec(conn_, "DEALLOCATE all");
-        if(PQresultStatus(r) != PGRES_COMMAND_OK) {
-            // Highly unlikely but we'll log it and go on.
-            LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_DEALLOC_ERROR)
-                      .arg(PQerrorMessage(conn_));
-        }
-
-        PQclear(r);
-        PQfinish(conn_);
-        conn_ = NULL;
-    }
 }
 
 std::string
@@ -1071,127 +718,14 @@ PgSqlLeaseMgr::getDBVersion() {
     return (tmp.str());
 }
 
-void
-PgSqlLeaseMgr::prepareStatements() {
-    for(int i = 0; tagged_statements[i].text != NULL; ++ i) {
-        // Prepare all statements queries with all known fields datatype
-        PGresult* r = PQprepare(conn_, tagged_statements[i].name,
-                                tagged_statements[i].text,
-                                tagged_statements[i].nbparams,
-                                tagged_statements[i].types);
-
-        if(PQresultStatus(r) != PGRES_COMMAND_OK) {
-            PQclear(r);
-            isc_throw(DbOperationError,
-                      "unable to prepare PostgreSQL statement: "
-                      << tagged_statements[i].text << ", reason: "
-                      << PQerrorMessage(conn_));
-        }
-
-        PQclear(r);
-    }
-}
-
-void
-PgSqlLeaseMgr::openDatabase() {
-    string dbconnparameters;
-    string shost = "localhost";
-    try {
-        shost = dbconn_.getParameter("host");
-    } catch(...) {
-        // No host. Fine, we'll use "localhost"
-    }
-    dbconnparameters += "host = '" + shost + "'" ;
-
-    string suser;
-    try {
-        suser = dbconn_.getParameter("user");
-        dbconnparameters += " user = '" + suser + "'";
-    } catch(...) {
-        // No user. Fine, we'll use NULL
-    }
-
-    string spassword;
-    try {
-        spassword = dbconn_.getParameter("password");
-        dbconnparameters += " password = '" + spassword + "'";
-    } catch(...) {
-        // No password. Fine, we'll use NULL
-    }
-
-    string sname;
-    try {
-        sname= dbconn_.getParameter("name");
-        dbconnparameters += " dbname = '" + sname + "'";
-    } catch(...) {
-        // No database name.  Throw a "NoDatabaseName" exception
-        isc_throw(NoDatabaseName, "must specify a name for the database");
-    }
-
-    unsigned int connect_timeout = PGSQL_DEFAULT_CONNECTION_TIMEOUT;
-    string stimeout;
-    try {
-        stimeout = dbconn_.getParameter("connect-timeout");
-    } catch (...) {
-        // No timeout parameter, we are going to use the default timeout.
-        stimeout = "";
-    }
-
-    if (stimeout.size() > 0) {
-        // Timeout was given, so try to convert it to an integer.
-
-        try {
-            connect_timeout = boost::lexical_cast<unsigned int>(stimeout);
-        } catch (...) {
-            // Timeout given but could not be converted to an unsigned int. Set
-            // the connection timeout to an invalid value to trigger throwing
-            // of an exception.
-            connect_timeout = 0;
-        }
-
-        // The timeout is only valid if greater than zero, as depending on the
-        // database, a zero timeout might signify someting like "wait
-        // indefinitely".
-        //
-        // The check below also rejects a value greater than the maximum
-        // integer value.  The lexical_cast operation used to obtain a numeric
-        // value from a string can get confused if trying to convert a negative
-        // integer to an unsigned int: instead of throwing an exception, it may
-        // produce a large positive value.
-        if ((connect_timeout == 0) ||
-            (connect_timeout > numeric_limits<int>::max())) {
-            isc_throw(DbInvalidTimeout, "database connection timeout (" <<
-                      stimeout << ") must be an integer greater than 0");
-        }
-    }
-
-    std::ostringstream oss;
-    oss << connect_timeout;
-    dbconnparameters += " connect_timeout = " + oss.str();
-
-    conn_ = PQconnectdb(dbconnparameters.c_str());
-    if (conn_ == NULL) {
-        isc_throw(DbOpenError, "could not allocate connection object");
-    }
-
-    if (PQstatus(conn_) != CONNECTION_OK) {
-        // If we have a connection object, we have to call finish
-        // to release it, but grab the error message first.
-        std::string error_message = PQerrorMessage(conn_);
-        PQfinish(conn_);
-        conn_ = NULL;
-        isc_throw(DbOpenError, error_message);
-    }
-}
-
 bool
 PgSqlLeaseMgr::addLeaseCommon(StatementIndex stindex,
                               PsqlBindArray& bind_array) {
-    PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name,
-                                  tagged_statements[stindex].nbparams,
-                                  &bind_array.values_[0],
-                                  &bind_array.lengths_[0],
-                                  &bind_array.formats_[0], 0);
+    PgSqlResult r(PQexecPrepared(conn_, tagged_statements[stindex].name,
+                                 tagged_statements[stindex].nbparams,
+                                 &bind_array.values_[0],
+                                 &bind_array.lengths_[0],
+                                 &bind_array.formats_[0], 0));
 
     int s = PQresultStatus(r);
 
@@ -1199,26 +733,16 @@ PgSqlLeaseMgr::addLeaseCommon(StatementIndex stindex,
         // Failure: check for the special case of duplicate entry.  If this is
         // the case, we return false to indicate that the row was not added.
         // Otherwise we throw an exception.
-        if (compareError(r, DUPLICATE_KEY)) {
-            PQclear(r);
+        if (conn_.compareError(r, PgSqlConnection::DUPLICATE_KEY)) {
             return (false);
         }
 
-        checkStatementError(r, stindex);
+        conn_.checkStatementError(r, tagged_statements[stindex]);
     }
 
-    PQclear(r);
-
     return (true);
 }
 
-bool PgSqlLeaseMgr::compareError(PGresult*& r, const char* error_state) {
-    const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
-    // PostgreSQL garuantees it will always be 5 characters long
-    return ((sqlstate != NULL) &&
-            (memcmp(sqlstate, error_state, STATECODE_LEN) == 0));
-}
-
 bool
 PgSqlLeaseMgr::addLease(const Lease4Ptr& lease) {
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
@@ -1248,17 +772,16 @@ void PgSqlLeaseMgr::getLeaseCollection(StatementIndex stindex,
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
               DHCPSRV_PGSQL_GET_ADDR4).arg(tagged_statements[stindex].name);
 
-    PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name,
-                       tagged_statements[stindex].nbparams,
-                       &bind_array.values_[0],
-                       &bind_array.lengths_[0],
-                       &bind_array.formats_[0], 0);
+    PgSqlResult r(PQexecPrepared(conn_, tagged_statements[stindex].name,
+                                 tagged_statements[stindex].nbparams,
+                                 &bind_array.values_[0],
+                                 &bind_array.lengths_[0],
+                                 &bind_array.formats_[0], 0));
 
-    checkStatementError(r, stindex);
+    conn_.checkStatementError(r, tagged_statements[stindex]);
 
     int rows = PQntuples(r);
     if (single && rows > 1) {
-        PQclear(r);
         isc_throw(MultipleRecords, "multiple records were found in the "
                       "database where only one was expected for query "
                       << tagged_statements[stindex].name);
@@ -1267,8 +790,6 @@ void PgSqlLeaseMgr::getLeaseCollection(StatementIndex stindex,
     for(int i = 0; i < rows; ++ i) {
         result.push_back(exchange->convertFromDatabase(r, i));
     }
-
-    PQclear(r);
 }
 
 
@@ -1566,16 +1087,15 @@ PgSqlLeaseMgr::updateLeaseCommon(StatementIndex stindex,
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
               DHCPSRV_PGSQL_ADD_ADDR4).arg(tagged_statements[stindex].name);
 
-    PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name,
-                                  tagged_statements[stindex].nbparams,
-                                  &bind_array.values_[0],
-                                  &bind_array.lengths_[0],
-                                  &bind_array.formats_[0], 0);
+    PgSqlResult r(PQexecPrepared(conn_, tagged_statements[stindex].name,
+                                 tagged_statements[stindex].nbparams,
+                                 &bind_array.values_[0],
+                                 &bind_array.lengths_[0],
+                                 &bind_array.formats_[0], 0));
 
-    checkStatementError(r, stindex);
+    conn_.checkStatementError(r, tagged_statements[stindex]);
 
     int affected_rows = boost::lexical_cast<int>(PQcmdTuples(r));
-    PQclear(r);
 
     // Check success case first as it is the most likely outcome.
     if (affected_rows == 1) {
@@ -1637,15 +1157,14 @@ PgSqlLeaseMgr::updateLease6(const Lease6Ptr& lease) {
 uint64_t
 PgSqlLeaseMgr::deleteLeaseCommon(StatementIndex stindex,
                                  PsqlBindArray& bind_array) {
-    PGresult* r = PQexecPrepared(conn_, tagged_statements[stindex].name,
-                                  tagged_statements[stindex].nbparams,
-                                  &bind_array.values_[0],
-                                  &bind_array.lengths_[0],
-                                  &bind_array.formats_[0], 0);
+    PgSqlResult r(PQexecPrepared(conn_, tagged_statements[stindex].name,
+                                 tagged_statements[stindex].nbparams,
+                                 &bind_array.values_[0],
+                                 &bind_array.lengths_[0],
+                                 &bind_array.formats_[0], 0));
 
-    checkStatementError(r, stindex);
+    conn_.checkStatementError(r, tagged_statements[stindex]);
     int affected_rows = boost::lexical_cast<int>(PQcmdTuples(r));
-    PQclear(r);
 
     return (affected_rows);
 }
@@ -1708,43 +1227,13 @@ string
 PgSqlLeaseMgr::getName() const {
     string name = "";
     try {
-        name = dbconn_.getParameter("name");
+        name = conn_.getParameter("name");
     } catch (...) {
         // Return an empty name
     }
     return (name);
 }
 
-void
-PgSqlLeaseMgr::checkStatementError(PGresult*& r, StatementIndex index) const {
-    int s = PQresultStatus(r);
-    if (s != PGRES_COMMAND_OK && s != PGRES_TUPLES_OK) {
-        // We're testing the first two chars of SQLSTATE, as this is the
-        // error class. Note, there is a severity field, but it can be
-        // misleadingly returned as fatal.
-        const char* sqlstate = PQresultErrorField(r, PG_DIAG_SQLSTATE);
-        if ((sqlstate != NULL) &&
-            ((memcmp(sqlstate, "08", 2) == 0) ||  // Connection Exception
-             (memcmp(sqlstate, "53", 2) == 0) ||  // Insufficient resources
-             (memcmp(sqlstate, "54", 2) == 0) ||  // Program Limit exceeded
-             (memcmp(sqlstate, "57", 2) == 0) ||  // Operator intervention
-             (memcmp(sqlstate, "58", 2) == 0))) { // System error
-            LOG_ERROR(dhcpsrv_logger, DHCPSRV_PGSQL_FATAL_ERROR)
-                         .arg(tagged_statements[index].name)
-                         .arg(PQerrorMessage(conn_))
-                         .arg(sqlstate);
-            PQclear(r);
-            exit (-1);
-        }
-
-        const char* error_message = PQerrorMessage(conn_);
-        PQclear(r);
-        isc_throw(DbOperationError, "Statement exec faild:" << " for: "
-                  << tagged_statements[index].name << ", reason: "
-                  << error_message);
-    }
-}
-
 string
 PgSqlLeaseMgr::getDescription() const {
     return (string("PostgreSQL Database"));
@@ -1755,8 +1244,8 @@ PgSqlLeaseMgr::getVersion() const {
     LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL,
               DHCPSRV_PGSQL_GET_VERSION);
 
-    PGresult* r = PQexecPrepared(conn_, "get_version", 0, NULL, NULL, NULL, 0);
-    checkStatementError(r, GET_VERSION);
+    PgSqlResult r(PQexecPrepared(conn_, "get_version", 0, NULL, NULL, NULL, 0));
+    conn_.checkStatementError(r, tagged_statements[GET_VERSION]);
 
     istringstream tmp;
     uint32_t version;
@@ -1769,35 +1258,17 @@ PgSqlLeaseMgr::getVersion() const {
     tmp.str(PQgetvalue(r, 0, 1));
     tmp >> minor;
 
-    PQclear(r);
-
     return make_pair<uint32_t, uint32_t>(version, minor);
 }
 
 void
 PgSqlLeaseMgr::commit() {
-    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_COMMIT);
-    PGresult* r = PQexec(conn_, "COMMIT");
-    if (PQresultStatus(r) != PGRES_COMMAND_OK) {
-        const char* error_message = PQerrorMessage(conn_);
-        PQclear(r);
-        isc_throw(DbOperationError, "commit failed: " << error_message);
-    }
-
-    PQclear(r);
+    conn_.commit();
 }
 
 void
 PgSqlLeaseMgr::rollback() {
-    LOG_DEBUG(dhcpsrv_logger, DHCPSRV_DBG_TRACE_DETAIL, DHCPSRV_PGSQL_ROLLBACK);
-    PGresult* r = PQexec(conn_, "ROLLBACK");
-    if (PQresultStatus(r) != PGRES_COMMAND_OK) {
-        const char* error_message = PQerrorMessage(conn_);
-        PQclear(r);
-        isc_throw(DbOperationError, "rollback failed: " << error_message);
-    }
-
-    PQclear(r);
+    conn_.rollback();
 }
 
 }; // end of isc::dhcp namespace

+ 7 - 142
src/lib/dhcpsrv/pgsql_lease_mgr.h

@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2015 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
 //
 // This Source Code Form is subject to the terms of the Mozilla Public
 // License, v. 2.0. If a copy of the MPL was not distributed with this
@@ -9,96 +9,17 @@
 
 #include <dhcp/hwaddr.h>
 #include <dhcpsrv/lease_mgr.h>
-#include <dhcpsrv/database_connection.h>
+#include <dhcpsrv/pgsql_connection.h>
+#include <dhcpsrv/pgsql_exchange.h>
+
 #include <boost/scoped_ptr.hpp>
 #include <boost/utility.hpp>
-#include <libpq-fe.h>
 
 #include <vector>
 
 namespace isc {
 namespace dhcp {
 
-/// @brief Structure used to bind C++ input values to dynamic SQL parameters
-/// The structure contains three vectors which store the input values,
-/// data lengths, and formats.  These vectors are passed directly into the
-/// PostgreSQL execute call.
-///
-/// Note that the data values are stored as pointers. These pointers need to
-/// valid for the duration of the PostgreSQL statement execution.  In other
-/// words populating them with pointers to values that go out of scope before
-/// statement is executed is a bad idea.
-struct PsqlBindArray {
-    /// @brief Vector of pointers to the data values.
-    std::vector<const char *> values_;
-    /// @brief Vector of data lengths for each value.
-    std::vector<int> lengths_;
-    /// @brief Vector of "format" for each value. A value of 0 means the
-    /// value is text, 1 means the value is binary.
-    std::vector<int> formats_;
-
-    /// @brief Format value for text data.
-    static const int TEXT_FMT;
-    /// @brief Format value for binary data.
-    static const int BINARY_FMT;
-
-    /// @brief Constant string passed to DB for boolean true values.
-    static const char* TRUE_STR;
-    /// @brief Constant string passed to DB for boolean false values.
-    static const char* FALSE_STR;
-
-    /// @brief Fetches the number of entries in the array.
-    /// @return Returns size_t containing the number of entries.
-    size_t size() {
-        return (values_.size());
-    }
-
-    /// @brief Indicates it the array is empty.
-    /// @return Returns true if there are no entries in the array, false
-    /// otherwise.
-    bool empty() {
-
-        return (values_.empty());
-    }
-
-    /// @brief Adds a char array to bind array based
-    ///
-    /// Adds a TEXT_FMT value to the end of the bind array, using the given
-    /// char* as the data source. Note that value is expected to be NULL
-    /// terminated.
-    ///
-    /// @param value char array containing the null-terminated text to add.
-    void add(const char* value);
-
-    /// @brief Adds an string value to the bind array
-    ///
-    /// Adds a TEXT formatted value to the end of the bind array using the
-    /// given string as the data source.
-    ///
-    /// @param value std::string containing the value to add.
-    void add(const std::string& value);
-
-    /// @brief Adds a binary value to the bind array.
-    ///
-    /// Adds a BINARY_FMT value to the end of the bind array using the
-    /// given vector as the data source.
-    ///
-    /// @param data vector of binary bytes.
-    void add(const std::vector<uint8_t>& data);
-
-    /// @brief Adds a boolean value to the bind array.
-    ///
-    /// Converts the given boolean value to its corresponding to PostgreSQL
-    /// string value and adds it as a TEXT_FMT value to the bind array.
-    ///
-    /// @param value bool value to add.
-    void add(const bool& value);
-
-    /// @brief Dumps the contents of the array to a string.
-    /// @return std::string containing the dump
-    std::string toText();
-};
-
 // Forward definitions (needed for shared_ptr definitions)
 // See pgsql_lease_mgr.cc file for actual class definitions
 class PgSqlLease4Exchange;
@@ -440,15 +361,6 @@ public:
     /// @throw DbOperationError If the rollback failed.
     virtual void rollback();
 
-    /// @brief Checks a result set's SQL state against an error state.
-    ///
-    /// @param r result set to check
-    /// @param error_state error state to compare against
-    ///
-    /// @return True if the result set's SQL state equals the error_state,
-    /// false otherwise.
-    bool compareError(PGresult*& r, const char* error_state);
-
     /// @brief Statement Tags
     ///
     /// The contents of the enum are indexes into the list of compiled SQL
@@ -478,26 +390,6 @@ public:
 
 private:
 
-    /// @brief Prepare statements
-    ///
-    /// Creates the prepared statements for all of the SQL statements used
-    /// by the PostgreSQL backend.
-    ///
-    /// @throw isc::dhcp::DbOperationError An operation on the open database has
-    ///        failed.
-    /// @throw isc::InvalidParameter 'index' is not valid for the vector.  This
-    ///        represents an internal error within the code.
-    void prepareStatements();
-
-    /// @brief Open Database
-    ///
-    /// Opens the database using the information supplied in the parameters
-    /// passed to the constructor.
-    ///
-    /// @throw NoDatabaseName Mandatory database name not given
-    /// @throw DbOpenError Error opening the database
-    void openDatabase();
-
     /// @brief Add Lease Common Code
     ///
     /// This method performs the common actions for both flavours (V4 and V6)
@@ -581,28 +473,6 @@ private:
         getLeaseCollection(stindex, bind_array, exchange6_, result);
     }
 
-    /// @brief Checks result of the r object
-    ///
-    /// This function is used to determine whether or not the SQL statement
-    /// execution succeeded, and in the event of failures, decide whether or
-    /// not the failures are recoverable.
-    ///
-    /// If the error is recoverable, the method will throw a DbOperationError.
-    /// In the error is deemed unrecoverable, such as a loss of connectivity
-    /// with the server, this method will log the error and call exit(-1);
-    ///
-    /// @todo Calling exit() is viewed as a short term solution for Kea 1.0.
-    /// Two tickets are likely to alter this behavior, first is #3639, which
-    /// calls for the ability to attempt to reconnect to the database. The
-    /// second ticket, #4087 which calls for the implementation of a generic,
-    /// FatalException class which will propagate outward.
-    ///
-    /// @param r result of the last PostgreSQL operation
-    /// @param index will be used to print out compiled statement name
-    ///
-    /// @throw isc::dhcp::DbOperationError Detailed PostgreSQL failure
-    void checkStatementError(PGresult*& r, StatementIndex index) const;
-
     /// @brief Get Lease4 Common Code
     ///
     /// This method performs the common actions for the various getLease4()
@@ -679,7 +549,8 @@ private:
     ///
     /// @throw isc::dhcp::DbOperationError An operation on the open database has
     ///        failed.
-    uint64_t deleteLeaseCommon(StatementIndex stindex, PsqlBindArray& bind_array);
+    uint64_t deleteLeaseCommon(StatementIndex stindex,
+                               PsqlBindArray& bind_array);
 
     /// @brief Delete expired-reclaimed leases.
     ///
@@ -700,14 +571,8 @@ private:
     boost::scoped_ptr<PgSqlLease4Exchange> exchange4_; ///< Exchange object
     boost::scoped_ptr<PgSqlLease6Exchange> exchange6_; ///< Exchange object
 
-    /// Database connection object
-    ///
-    /// @todo: Implement PgSQLConnection object and collapse
-    /// dbconn_ and conn_ into a single object.
-    DatabaseConnection dbconn_;
-
     /// PostgreSQL connection handle
-    PGconn* conn_;
+    PgSqlConnection conn_;
 };
 
 }; // end of isc::dhcp namespace

+ 1 - 0
src/lib/dhcpsrv/tests/Makefile.am

@@ -118,6 +118,7 @@ endif
 libdhcpsrv_unittests_SOURCES += ncr_generator_unittest.cc
 
 if HAVE_PGSQL
+libdhcpsrv_unittests_SOURCES += pgsql_exchange_unittest.cc
 libdhcpsrv_unittests_SOURCES += pgsql_lease_mgr_unittest.cc
 endif
 libdhcpsrv_unittests_SOURCES += pool_unittest.cc

+ 73 - 0
src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc

@@ -0,0 +1,73 @@
+// Copyright (C) 2016 Internet Systems Consortium, Inc. ("ISC")
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#include <config.h>
+
+#include <dhcpsrv/pgsql_exchange.h>
+
+#include <boost/lexical_cast.hpp>
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::dhcp;
+
+namespace {
+
+/// @brief Converts a time_t into a string matching our Postgres input format
+///
+/// @param time_val Time value to convert
+/// @retrun A string containing the converted time
+std::string timeToDbString(const time_t time_val) {
+    struct tm tinfo;
+    char buffer[20];
+
+    localtime_r(&time_val, &tinfo);
+    strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tinfo);
+    return(std::string(buffer));
+}
+
+/// @brief Basic checks on time conversion functions in PgSqlExchange
+/// We input timestamps as date/time strings and we output them as
+/// an integer string of seconds since the epoch.  There is no meangingful
+/// way to test them round-trip without Postgres involved.
+TEST(PgSqlExchangeTest, convertTimeTest) {
+    // Get a reference time and time string
+    time_t ref_time;
+    time(&ref_time);
+
+    std::string ref_time_str(timeToDbString(ref_time));
+
+    // Verify convertToDatabaseTime gives us the expected localtime string
+    std::string time_str = PgSqlExchange::convertToDatabaseTime(ref_time);
+    EXPECT_EQ(time_str, ref_time_str);
+
+    // Verify convertToDatabaseTime with valid_lifetime = 0  gives us the
+    // expected localtime string
+    time_str = PgSqlExchange::convertToDatabaseTime(ref_time, 0);
+    EXPECT_EQ(time_str, ref_time_str);
+
+    // Verify we can add time by adding a day.
+    ref_time_str = timeToDbString(ref_time + (24*3600));
+    ASSERT_NO_THROW(time_str = PgSqlExchange::convertToDatabaseTime(ref_time,
+                                                                    24*3600));
+    EXPECT_EQ(time_str, ref_time_str);
+
+    // Verify too large of a value is detected.
+    ASSERT_THROW(PgSqlExchange::convertToDatabaseTime(DatabaseConnection::
+                                                      MAX_DB_TIME - 1,
+                                                      24*3600),
+                 isc::BadValue);
+
+    // Make sure Conversion "from" database time functions
+    std::string ref_secs_str = boost::lexical_cast<std::string>(ref_time);
+    time_t from_time = PgSqlExchange::convertFromDatabaseTime(ref_secs_str);
+    from_time = PgSqlExchange::convertFromDatabaseTime(ref_secs_str);
+    EXPECT_EQ(ref_time, from_time);
+}
+
+}; // namespace
+

+ 2 - 9
src/lib/dhcpsrv/tests/pgsql_lease_mgr_unittest.cc

@@ -16,14 +16,6 @@
 
 #include <gtest/gtest.h>
 
-#include <algorithm>
-#include <iostream>
-#include <sstream>
-#include <string>
-#include <utility>
-
-#include <stdlib.h>
-
 using namespace isc;
 using namespace isc::asiolink;
 using namespace isc::dhcp;
@@ -410,4 +402,5 @@ TEST_F(PgSqlLeaseMgrTest, getExpiredLeases6) {
     testGetExpiredLeases6();
 }
 
-};
+}; // namespace
+