Parcourir la source

[4277] Addressed bulk of review comments

src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc
    - Added PgSqlBasicsTest test fixture class and tests which exercise all of
    the PostgreSQL data types we currently use with round-trip database writes
    and reads

src/lib/dhcpsrv/pgsql_connection.cc
src/lib/dhcpsrv/pgsql_connection.h
    - Moved PgSqlResult function impls from .h
    - Added exception safe implementation of getColumnLabel() to PgSqlResult

src/lib/dhcpsrv/pgsql_exchange.cc
src/lib/dhcpsrv/pgsql_exchange.h
    - PsqlBindArray::add() variants which accept raw pointers now throw
    if the pointer is NULL
    - PgSqlExchange::getColumnLabel() is now a wrapper around PgSqlResult method

src/lib/dhcpsrv/pgsql_host_data_source.h
src/lib/dhcpsrv/pgsql_host_data_source.cc
     - Commentary clean up

src/lib/dhcpsrv/pgsql_lease_mgr.cc
     - Commentary clean up
Thomas Markwalder il y a 8 ans
Parent
commit
15b51b6229

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

@@ -35,6 +35,59 @@ const int PGSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds
 
 
 const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
 const char PgSqlConnection::DUPLICATE_KEY[] = ERRCODE_UNIQUE_VIOLATION;
 
 
+PgSqlResult::PgSqlResult(PGresult *result) 
+    : result_(result), rows_(0), cols_(0) {
+    if (!result) {
+        isc_throw (BadValue, "PgSqlResult result pointer cannot be null");
+    }
+
+    rows_ = PQntuples(result);
+    cols_ = PQnfields(result);
+}
+
+void 
+PgSqlResult::rowCheck(int row) const {
+    if (row < 0 || row >= rows_) {
+        isc_throw (DbOperationError, "row: " << row 
+                   << ", out of range: 0.." << rows_);
+    }
+}
+
+PgSqlResult::~PgSqlResult() {
+    if (result_)  {
+        PQclear(result_);
+    }
+}
+
+void
+PgSqlResult::colCheck(int col) const {
+    if (col < 0 || col >= cols_) {
+        isc_throw (DbOperationError, "col: " << col
+                   << ", out of range: 0.." << cols_);
+    }
+}
+
+void
+PgSqlResult::rowColCheck(int row, int col) const {
+    rowCheck(row);
+    colCheck(col);
+}
+
+std::string
+PgSqlResult::getColumnLabel(const int col) const {
+    const char* label = NULL;
+    try {
+        colCheck(col);
+        label = PQfname(result_, col);
+    } catch (...) {
+        std::ostringstream os;
+        os << "Unknown column:" << col;
+        return (os.str());
+    }
+
+    return (label);
+}
+
 PgSqlTransaction::PgSqlTransaction(PgSqlConnection& conn)
 PgSqlTransaction::PgSqlTransaction(PgSqlConnection& conn)
     : conn_(conn), committed_(false) {
     : conn_(conn), committed_(false) {
     conn_.startTransaction();
     conn_.startTransaction();

+ 22 - 37
src/lib/dhcpsrv/pgsql_connection.h

@@ -28,7 +28,6 @@ const size_t PGSQL_MAX_PARAMETERS_IN_QUERY = 32;
 /// Each statement is associated with an index, which is used to reference the
 /// Each statement is associated with an index, which is used to reference the
 /// associated prepared statement.
 /// associated prepared statement.
 struct PgSqlTaggedStatement {
 struct PgSqlTaggedStatement {
-
     /// Number of parameters for a given query
     /// Number of parameters for a given query
     int nbparams;
     int nbparams;
 
 
@@ -48,16 +47,16 @@ struct PgSqlTaggedStatement {
 
 
 /// @brief Constants for PostgreSQL data types
 /// @brief Constants for PostgreSQL data types
 /// This are defined by PostreSQL in <catalog/pg_type.h>, but including
 /// 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.
+/// this file is extraordinarily convoluted, so we'll use these to fill-in.
 const size_t OID_NONE = 0;   // PostgreSQL infers proper type
 const size_t OID_NONE = 0;   // PostgreSQL infers proper type
 const size_t OID_BOOL = 16;
 const size_t OID_BOOL = 16;
 const size_t OID_BYTEA = 17;
 const size_t OID_BYTEA = 17;
 const size_t OID_INT8 = 20;  // 8 byte int
 const size_t OID_INT8 = 20;  // 8 byte int
-const size_t OID_INT4 = 23;  // 4 byte int
 const size_t OID_INT2 = 21;  // 2 byte int
 const size_t OID_INT2 = 21;  // 2 byte int
+const size_t OID_INT4 = 23;  // 4 byte int
 const size_t OID_TEXT = 25;
 const size_t OID_TEXT = 25;
-const size_t OID_TIMESTAMP = 1114;
 const size_t OID_VARCHAR = 1043;
 const size_t OID_VARCHAR = 1043;
+const size_t OID_TIMESTAMP = 1114;
 
 
 //@}
 //@}
 
 
@@ -85,23 +84,12 @@ public:
     /// Store the pointer to the result set to being fetched.  Set row
     /// Store the pointer to the result set to being fetched.  Set row
     /// and column counts for convenience.
     /// and column counts for convenience.
     ///
     ///
-    PgSqlResult(PGresult *result) : result_(result), rows_(0), cols_(0) {
-        if (!result) {
-            isc_throw (BadValue, "PgSqlResult result pointer cannot be null");
-        }
-
-        rows_ = PQntuples(result);
-        cols_ = PQnfields(result);
-    }
+    PgSqlResult(PGresult *result);
 
 
     /// @brief Destructor
     /// @brief Destructor
     ///
     ///
     /// Frees the result set
     /// Frees the result set
-    ~PgSqlResult() {
-        if (result_)  {
-            PQclear(result_);
-        }
-    }
+    ~PgSqlResult();
 
 
     /// @brief Returns the number of rows in the result set.
     /// @brief Returns the number of rows in the result set.
     int getRows() const {
     int getRows() const {
@@ -117,35 +105,34 @@ public:
     ///
     ///
     /// @param row index to range check
     /// @param row index to range check
     ///
     ///
-    /// @throw throws DbOperationError if the row index is out of range
-    void rowCheck(int row) const {
-        if (row >= rows_) {
-            isc_throw (DbOperationError, "row: " << row << ", out of range: 0.." << rows_);
-        }
-    }
+    /// @throw DbOperationError if the row index is out of range
+    void rowCheck(int row) const;
 
 
     /// @brief Determines if a column index is valid
     /// @brief Determines if a column index is valid
     ///
     ///
     /// @param col index to range check
     /// @param col index to range check
     ///
     ///
-    /// @throw throws DbOperationError if the column index is out of range
-    void colCheck(int col) const {
-        if (col >= cols_) {
-            isc_throw (DbOperationError, "col: " << col << ", out of range: 0.." << cols_);
-        }
-    }
+    /// @throw DbOperationError if the column index is out of range
+    void colCheck(int col) const;
 
 
     /// @brief Determines if both a row and column index are valid
     /// @brief Determines if both a row and column index are valid
     ///
     ///
     /// @param row index to range check
     /// @param row index to range check
     /// @param col index to range check
     /// @param col index to range check
     ///
     ///
-    /// @throw throws DbOperationError if either the row or column index
+    /// @throw DbOperationError if either the row or column index
     /// is out of range
     /// is out of range
-    void rowColCheck(int row, int col) const {
-        rowCheck(row);
-        colCheck(col);
-    }
+    void rowColCheck(int row, int col) const;
+
+    /// @brief Fetches the name of the column in a result set
+    ///
+    /// Returns the column name of the column from the result set.
+    /// If the column index is out of range it will return the
+    /// string "Unknown column:<index>"
+    ///
+    /// @param col index of the column name to fetch
+    /// @return string containing the name of the column
+    std::string getColumnLabel(const int col) const;
 
 
     /// @brief Conversion Operator
     /// @brief Conversion Operator
     ///
     ///
@@ -275,7 +262,7 @@ public:
     /// @brief Commits transaction.
     /// @brief Commits transaction.
     ///
     ///
     /// Commits all changes made during the transaction by executing the
     /// Commits all changes made during the transaction by executing the
-    /// SQL statement: "COMMIT">
+    /// SQL statement: "COMMIT"
     ///
     ///
     /// @throw DbOperationError if statement execution fails
     /// @throw DbOperationError if statement execution fails
     void commit();
     void commit();
@@ -410,8 +397,6 @@ public:
 
 
 };
 };
 
 
-
-
 }; // end of isc::dhcp namespace
 }; // end of isc::dhcp namespace
 }; // end of isc namespace
 }; // end of isc namespace
 
 

+ 15 - 11
src/lib/dhcpsrv/pgsql_exchange.cc

@@ -21,6 +21,10 @@ const char* PsqlBindArray::TRUE_STR = "TRUE";
 const char* PsqlBindArray::FALSE_STR = "FALSE";
 const char* PsqlBindArray::FALSE_STR = "FALSE";
 
 
 void PsqlBindArray::add(const char* value) {
 void PsqlBindArray::add(const char* value) {
+    if (!value) {
+        isc_throw(BadValue, "PsqlBindArray::add - char* value cannot be NULL");
+    }
+
     values_.push_back(value);
     values_.push_back(value);
     lengths_.push_back(strlen(value));
     lengths_.push_back(strlen(value));
     formats_.push_back(TEXT_FMT);
     formats_.push_back(TEXT_FMT);
@@ -39,6 +43,10 @@ void PsqlBindArray::add(const std::vector<uint8_t>& data) {
 }
 }
 
 
 void PsqlBindArray::add(const uint8_t* data, const size_t len) {
 void PsqlBindArray::add(const uint8_t* data, const size_t len) {
+    if (!data) {
+        isc_throw(BadValue, "PsqlBindArray::add - uint8_t data cannot be NULL");
+    }
+
     values_.push_back(reinterpret_cast<const char*>(&(data[0])));
     values_.push_back(reinterpret_cast<const char*>(&(data[0])));
     lengths_.push_back(len);
     lengths_.push_back(len);
     formats_.push_back(BINARY_FMT);
     formats_.push_back(BINARY_FMT);
@@ -70,7 +78,11 @@ void PsqlBindArray::addNull(const int format) {
     formats_.push_back(format);
     formats_.push_back(format);
 }
 }
 
 
-// Eventually this could replace add(std::string&) ?
+/// @todo Eventually this could replace add(std::string&)? This would mean
+/// all bound strings would be internally copies rather than perhaps belonging
+/// to the originating object such as Host::hostname_.  One the one hand it
+/// would make all strings handled one-way only, on the other hand it would
+/// mean duplicating strings where it isn't strictly necessary.
 void PsqlBindArray::addTempString(const std::string& str) {
 void PsqlBindArray::addTempString(const std::string& str) {
     bound_strs_.push_back(ConstStringPtr(new std::string(str)));
     bound_strs_.push_back(ConstStringPtr(new std::string(str)));
     PsqlBindArray::add((bound_strs_.back())->c_str());
     PsqlBindArray::add((bound_strs_.back())->c_str());
@@ -246,15 +258,7 @@ PgSqlExchange::convertFromBytea(const PgSqlResult& r, const int row,
 
 
 std::string
 std::string
 PgSqlExchange::getColumnLabel(const PgSqlResult& r, const size_t column) {
 PgSqlExchange::getColumnLabel(const PgSqlResult& r, const size_t column) {
-    r.colCheck(column);
-    const char* label = PQfname(r, column);
-    if (!label) {
-        std::ostringstream os;
-        os << "Unknown column:" << column;
-        return (os.str());
-    }
-
-    return (label);
+    return (r.getColumnLabel(column));
 }
 }
 
 
 std::string 
 std::string 
@@ -264,7 +268,7 @@ PgSqlExchange::dumpRow(const PgSqlResult& r, int row) {
     int columns = r.getCols();
     int columns = r.getCols();
     for (int col = 0; col < columns; ++col) {
     for (int col = 0; col < columns; ++col) {
         const char* val = getRawColumnValue(r, row, col);
         const char* val = getRawColumnValue(r, row, col);
-        std::string name = getColumnLabel(r, col);
+        std::string name = r.getColumnLabel(col);
         int format = PQfformat(r, col); 
         int format = PQfformat(r, col); 
 
 
         stream << col << "   " << name << " : " ;
         stream << col << "   " << name << " : " ;

+ 16 - 2
src/lib/dhcpsrv/pgsql_exchange.h

@@ -82,6 +82,7 @@ struct PsqlBindArray {
     /// remains in scope until the bind array has been discarded.
     /// remains in scope until the bind array has been discarded.
     ///
     ///
     /// @param value char array containing the null-terminated text to add.
     /// @param value char array containing the null-terminated text to add.
+    /// @throw DbOperationError if value is NULL.
     void add(const char* value);
     void add(const char* value);
 
 
     /// @brief Adds an string value to the bind array
     /// @brief Adds an string value to the bind array
@@ -113,6 +114,7 @@ struct PsqlBindArray {
     ///
     ///
     /// @param data buffer of binary data.
     /// @param data buffer of binary data.
     /// @param len  number of bytes of data in buffer
     /// @param len  number of bytes of data in buffer
+    /// @throw DbOperationError if data is NULL.
     void add(const uint8_t* data, const size_t len);
     void add(const uint8_t* data, const size_t len);
 
 
     /// @brief Adds a boolean value to the bind array.
     /// @brief Adds a boolean value to the bind array.
@@ -159,7 +161,7 @@ struct PsqlBindArray {
     /// @brief Binds a the given string to the bind array.
     /// @brief Binds a the given string to the bind array.
     ///
     ///
     /// Prior to added the The given string the vector of exchange values,
     /// Prior to added the The given string the vector of exchange values,
-    /// it duplicated as a ConstStringPtr and saved internally.  This garauntees
+    /// it duplicated as a ConstStringPtr and saved internally.  This guarantees
     /// the string remains in scope until the PsqlBindArray is destroyed,
     /// the string remains in scope until the PsqlBindArray is destroyed,
     /// without the caller maintaining the string values.
     /// without the caller maintaining the string values.
     ///
     ///
@@ -262,7 +264,15 @@ public:
     static const char* getRawColumnValue(const PgSqlResult& r, const int row,
     static const char* getRawColumnValue(const PgSqlResult& r, const int row,
                                          const size_t col);
                                          const size_t col);
 
 
-    /// @todo
+    /// @brief Fetches the name of the column in a result set
+    ///
+    /// Returns the column name of the column from the result set.
+    /// If the column index is out of range it will return the
+    /// string "Unknown column:<index>".  Note this is NOT from the
+    /// list of columns defined in the exchange.
+    ///
+    /// @param col index of the column name to fetch
+    /// @return string containing the name of the column
     static std::string getColumnLabel(const PgSqlResult& r, const size_t col);
     static std::string getColumnLabel(const PgSqlResult& r, const size_t col);
 
 
     /// @brief Fetches text column value as a string
     /// @brief Fetches text column value as a string
@@ -319,6 +329,8 @@ public:
     /// @param r the result set containing the query results
     /// @param r the result set containing the query results
     /// @param row the row number within the result set
     /// @param row the row number within the result set
     /// @param col the column number within the row
     /// @param col the column number within the row
+    ///
+    /// @return True if the column values in the row is NULL, false otherwise.
     static bool isColumnNull(const PgSqlResult& r, const int row,
     static bool isColumnNull(const PgSqlResult& r, const int row,
                              const size_t col);
                              const size_t col);
 
 
@@ -372,6 +384,8 @@ public:
     ///
     ///
     /// @param r the result set containing the query results
     /// @param r the result set containing the query results
     /// @param row the row number within the result set
     /// @param row the row number within the result set
+    ///
+    /// @return A string depiction of the row contents.
     static std::string dumpRow(const PgSqlResult& r, int row);
     static std::string dumpRow(const PgSqlResult& r, int row);
 
 
 protected:
 protected:

+ 63 - 29
src/lib/dhcpsrv/pgsql_host_data_source.cc

@@ -34,6 +34,8 @@ using namespace std;
 namespace {
 namespace {
 
 
 /// @brief Maximum length of option value.
 /// @brief Maximum length of option value.
+/// The maximum size of the raw option data that may be read from the
+/// database. 
 const size_t OPTION_VALUE_MAX_LEN = 4096;
 const size_t OPTION_VALUE_MAX_LEN = 4096;
 
 
 /// @brief Numeric value representing last supported identifier.
 /// @brief Numeric value representing last supported identifier.
@@ -106,7 +108,7 @@ public:
     /// @brief Reinitializes state information
     /// @brief Reinitializes state information
     ///
     ///
     /// This function should be called in between statement executions.
     /// This function should be called in between statement executions.
-    /// Deriving classes should inovke this method as well as be reset
+    /// Deriving classes should invoke this method as well as be reset
     /// all of their own stateful values.
     /// all of their own stateful values.
     virtual void clear() {
     virtual void clear() {
         host_.reset();
         host_.reset();
@@ -375,13 +377,30 @@ private:
         /// @brief Creates instance of the currently processed option.
         /// @brief Creates instance of the currently processed option.
         ///
         ///
         /// This method detects if the currently processed option is a new
         /// This method detects if the currently processed option is a new
-        /// instance. It makes it determination by comparing the identifier
+        /// instance. It makes its determination by comparing the identifier
         /// of the currently processed option, with the most recently processed
         /// of the currently processed option, with the most recently processed
         /// option. If the current value is greater than the id of the recently
         /// option. If the current value is greater than the id of the recently
         /// processed option it is assumed that the processed row holds new
         /// processed option it is assumed that the processed row holds new
         /// option information. In such case the option instance is created and
         /// option information. In such case the option instance is created and
         /// inserted into the configuration passed as argument.
         /// inserted into the configuration passed as argument.
         ///
         ///
+        /// This logic is necessary to deal with result sets made from multiple
+        /// left joins which contain duplicated data.  For instance queries
+        /// returning both v4 and v6 options for a host would generate result
+        /// sets similar to this:
+        /// @code
+        ///
+        /// row 0: host-1  v4-opt-1  v6-opt-1
+        /// row 1: host-1  v4-opt-1  v6-opt-2
+        /// row 2: host-1  v4-opt-1  v6-opt-3
+        /// row 4: host-1  v4-opt-2  v6-opt-1
+        /// row 5: host-1  v4-opt-2  v6-opt-2
+        /// row 6: host-1  v4-opt-2  v6-opt-3
+        /// row 7: host-2  v4-opt-1  v6-opt-1
+        /// row 8: host-2  v4-opt-2  v6-opt-1
+        ///  :
+        /// @endcode
+        ///
         /// @param cfg Pointer to the configuration object into which new
         /// @param cfg Pointer to the configuration object into which new
         /// option instances should be inserted.
         /// option instances should be inserted.
         /// @param r result set containing one or more rows from a dhcp
         /// @param r result set containing one or more rows from a dhcp
@@ -421,8 +440,6 @@ private:
                                             sizeof(value), value_len);
                                             sizeof(value), value_len);
 
 
             // formatted_value: TEXT
             // formatted_value: TEXT
-            // @todo Should we attempt to enforce max value of 8K?
-            // If so, we should declare this VARCHAR[8K] in the table
             std::string formatted_value;
             std::string formatted_value;
             PgSqlExchange::getColumnValue(r, row, formatted_value_index_,
             PgSqlExchange::getColumnValue(r, row, formatted_value_index_,
                                           formatted_value);
                                           formatted_value);
@@ -596,7 +613,7 @@ public:
     /// @brief Clears state information
     /// @brief Clears state information
     ///
     ///
     /// This function should be called in between statement executions.
     /// This function should be called in between statement executions.
-    /// Deriving classes should inovke this method as well as be reset
+    /// Deriving classes should invoke this method as well as be reset
     /// all of their own stateful values.
     /// all of their own stateful values.
     virtual void clear() {
     virtual void clear() {
         PgSqlHostExchange::clear();
         PgSqlHostExchange::clear();
@@ -611,10 +628,11 @@ public:
 
 
     /// @brief Processes the current row.
     /// @brief Processes the current row.
     ///
     ///
-    /// The processed row includes both host information and DHCP option
-    /// information. Because used SELECT query use LEFT JOIN clause, the
-    /// some rows contain duplicated host or options entries. This method
-    /// detects duplicated information and discards such entries.
+    /// The fetched row includes both host information and DHCP option
+    /// information. Because the SELECT queries use one or more LEFT JOIN
+    /// clauses, the result set may contain duplicated host or options 
+    /// entries. This method detects duplicated information and discards such 
+    /// entries.
     ///
     ///
     /// @param [out] hosts Container holding parsed hosts and options.
     /// @param [out] hosts Container holding parsed hosts and options.
     virtual void processRowData(ConstHostCollection& hosts,
     virtual void processRowData(ConstHostCollection& hosts,
@@ -685,7 +703,7 @@ private:
 /// host information, DHCPv4 options, DHCPv6 options and IPv6 reservations.
 /// host information, DHCPv4 options, DHCPv6 options and IPv6 reservations.
 ///
 ///
 /// This class extends the @ref PgSqlHostWithOptionsExchange class with the
 /// This class extends the @ref PgSqlHostWithOptionsExchange class with the
-/// mechanisms to retrieve IPv6 reservations. This class is used in sitations
+/// mechanisms to retrieve IPv6 reservations. This class is used in situations
 /// when it is desired to retrieve DHCPv6 specific information about the host
 /// when it is desired to retrieve DHCPv6 specific information about the host
 /// (DHCPv6 options and reservations), or entire information about the host
 /// (DHCPv6 options and reservations), or entire information about the host
 /// (DHCPv4 options, DHCPv6 options and reservations). The following are the
 /// (DHCPv4 options, DHCPv6 options and reservations). The following are the
@@ -727,7 +745,7 @@ public:
     /// @brief Reinitializes state information
     /// @brief Reinitializes state information
     ///
     ///
     /// This function should be called in between statement executions.
     /// This function should be called in between statement executions.
-    /// Deriving classes should inovke this method as well as be reset
+    /// Deriving classes should invoke this method as well as be reset
     /// all of their own stateful values.
     /// all of their own stateful values.
     void clear() {
     void clear() {
         PgSqlHostWithOptionsExchange::clear();
         PgSqlHostWithOptionsExchange::clear();
@@ -921,7 +939,7 @@ public:
             bind_array->add(resv.getPrefixLen());
             bind_array->add(resv.getPrefixLen());
 
 
             // type: SMALLINT NOT NULL
             // type: SMALLINT NOT NULL
-            // See lease6_types for values (0 = IA_NA, 1 = IA_TA, 2 = IA_PD)
+            // See lease6_types table for values (0 = IA_NA, 2 = IA_PD)
             uint16_t type = resv.getType() == IPv6Resrv::TYPE_NA ? 0 : 2;
             uint16_t type = resv.getType() == IPv6Resrv::TYPE_NA ? 0 : 2;
             bind_array->add(type);
             bind_array->add(type);
 
 
@@ -1089,8 +1107,8 @@ public:
     enum StatementIndex {
     enum StatementIndex {
         INSERT_HOST,            // Insert new host to collection
         INSERT_HOST,            // Insert new host to collection
         INSERT_V6_RESRV,        // Insert v6 reservation
         INSERT_V6_RESRV,        // Insert v6 reservation
-        INSERT_V4_HOST_OPTION,       // Insert DHCPv4 option
-        INSERT_V6_HOST_OPTION,       // Insert DHCPv6 option
+        INSERT_V4_HOST_OPTION,  // Insert DHCPv4 option
+        INSERT_V6_HOST_OPTION,  // Insert DHCPv6 option
         GET_HOST_DHCPID,        // Gets hosts by host identifier
         GET_HOST_DHCPID,        // Gets hosts by host identifier
         GET_HOST_ADDR,          // Gets hosts by IPv4 address
         GET_HOST_ADDR,          // Gets hosts by IPv4 address
         GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID
         GET_HOST_SUBID4_DHCPID, // Gets host by IPv4 SubnetID, HW address/DUID
@@ -1122,7 +1140,7 @@ public:
     /// of a single row with one column, the value of the primary key.
     /// of a single row with one column, the value of the primary key.
     /// Defaults to false.
     /// Defaults to false.
     ///
     ///
-    /// @returns 0 if return_last_id is false, otherwise it returns the
+    /// @return 0 if return_last_id is false, otherwise it returns the
     /// the value in the result set in the first col of the first row.
     /// the value in the result set in the first col of the first row.
     ///
     ///
     /// @throw isc::dhcp::DuplicateEntry Database throws duplicate entry error
     /// @throw isc::dhcp::DuplicateEntry Database throws duplicate entry error
@@ -1245,8 +1263,9 @@ public:
 /// @brief Prepared MySQL statements used by the backend to insert and
 /// @brief Prepared MySQL statements used by the backend to insert and
 /// retrieve hosts from the database.
 /// retrieve hosts from the database.
 PgSqlTaggedStatement tagged_statements[] = {
 PgSqlTaggedStatement tagged_statements[] = {
+    // PgSqlHostDataSourceImpl::INSERT_HOST
     // Inserts a host into the 'hosts' table. Returns the inserted host id.
     // Inserts a host into the 'hosts' table. Returns the inserted host id.
-    {8, // PgSqlHostDataSourceImpl::INSERT_HOST,
+    {8, 
      { OID_BYTEA, OID_INT2,
      { OID_BYTEA, OID_INT2,
        OID_INT4, OID_INT4, OID_INT8, OID_VARCHAR,
        OID_INT4, OID_INT4, OID_INT8, OID_VARCHAR,
        OID_VARCHAR, OID_VARCHAR },
        OID_VARCHAR, OID_VARCHAR },
@@ -1257,8 +1276,9 @@ PgSqlTaggedStatement tagged_statements[] = {
      "VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING host_id"
      "VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING host_id"
     },
     },
 
 
+    //PgSqlHostDataSourceImpl::INSERT_V6_RESRV
     // Inserts a single IPv6 reservation into 'reservations' table.
     // Inserts a single IPv6 reservation into 'reservations' table.
-    {5, //PgSqlHostDataSourceImpl::INSERT_V6_RESRV,
+    {5, 
      { OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4 },
      { OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4 },
      "insert_v6_resrv",
      "insert_v6_resrv",
      "INSERT INTO ipv6_reservations(address, prefix_len, type, "
      "INSERT INTO ipv6_reservations(address, prefix_len, type, "
@@ -1266,9 +1286,10 @@ PgSqlTaggedStatement tagged_statements[] = {
      "VALUES ($1, $2, $3, $4, $5)"
      "VALUES ($1, $2, $3, $4, $5)"
     },
     },
 
 
+    // PgSqlHostDataSourceImpl::INSERT_V4_HOST_OPTION
     // Inserts a single DHCPv4 option into 'dhcp4_options' table.
     // Inserts a single DHCPv4 option into 'dhcp4_options' table.
     // Using fixed scope_id = 3, which associates an option with host.
     // Using fixed scope_id = 3, which associates an option with host.
-    {6, // PgSqlHostDataSourceImpl::INSERT_V4_HOST_OPTION,
+    {6, 
      { OID_INT2, OID_BYTEA, OID_TEXT,
      { OID_INT2, OID_BYTEA, OID_TEXT,
        OID_VARCHAR, OID_BOOL, OID_INT8},
        OID_VARCHAR, OID_BOOL, OID_INT8},
      "insert_v4_host_option",
      "insert_v4_host_option",
@@ -1277,9 +1298,10 @@ PgSqlTaggedStatement tagged_statements[] = {
      "VALUES ($1, $2, $3, $4, $5, $6, 3)"
      "VALUES ($1, $2, $3, $4, $5, $6, 3)"
     },
     },
 
 
+    // PgSqlHostDataSourceImpl::INSERT_V6_HOST_OPTION
     // Inserts a single DHCPv6 option into 'dhcp6_options' table.
     // Inserts a single DHCPv6 option into 'dhcp6_options' table.
     // Using fixed scope_id = 3, which associates an option with host.
     // Using fixed scope_id = 3, which associates an option with host.
-    {6, // PgSqlHostDataSourceImpl::INSERT_V6_HOST_OPTION,
+    {6,
      { OID_INT2, OID_BYTEA, OID_TEXT,
      { OID_INT2, OID_BYTEA, OID_TEXT,
        OID_VARCHAR, OID_BOOL, OID_INT8},
        OID_VARCHAR, OID_BOOL, OID_INT8},
      "insert_v6_host_option",
      "insert_v6_host_option",
@@ -1288,11 +1310,12 @@ PgSqlTaggedStatement tagged_statements[] = {
      "VALUES ($1, $2, $3, $4, $5, $6, 3)"
      "VALUES ($1, $2, $3, $4, $5, $6, 3)"
     },
     },
 
 
+    // PgSqlHostDataSourceImpl::GET_HOST_DHCPID
     // Retrieves host information, IPv6 reservations and both DHCPv4 and
     // Retrieves host information, IPv6 reservations and both DHCPv4 and
     // DHCPv6 options associated with the host. The LEFT JOIN clause is used
     // DHCPv6 options associated with the host. The LEFT JOIN clause is used
     // to retrieve information from 4 different tables using a single query.
     // to retrieve information from 4 different tables using a single query.
     // Hence, this query returns multiple rows for a single host.
     // Hence, this query returns multiple rows for a single host.
-    {2, // PgSqlHostDataSourceImpl::GET_HOST_DHCPID,
+    {2, 
      { OID_BYTEA, OID_INT2 },
      { OID_BYTEA, OID_INT2 },
      "get_host_dhcpid",
      "get_host_dhcpid",
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
@@ -1311,10 +1334,11 @@ PgSqlTaggedStatement tagged_statements[] = {
      "ORDER BY h.host_id, o4.option_id, o6.option_id, r.reservation_id"
      "ORDER BY h.host_id, o4.option_id, o6.option_id, r.reservation_id"
     },
     },
 
 
+    // PgSqlHostDataSourceImpl::GET_HOST_ADDR
     // Retrieves host information along with the DHCPv4 options associated with
     // Retrieves host information along with the DHCPv4 options associated with
     // it. Left joining the dhcp4_options table results in multiple rows being
     // it. Left joining the dhcp4_options table results in multiple rows being
     // returned for the same host. The host is retrieved by IPv4 address.
     // returned for the same host. The host is retrieved by IPv4 address.
-    { 1, // PgSqlHostDataSourceImpl::GET_HOST_ADDR,
+    {1,
      { OID_INT8 }, "get_host_addr",
      { OID_INT8 }, "get_host_addr",
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "  h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
      "  h.dhcp4_subnet_id, h.dhcp6_subnet_id, h.ipv4_address, h.hostname, "
@@ -1326,10 +1350,11 @@ PgSqlTaggedStatement tagged_statements[] = {
      "ORDER BY h.host_id, o.option_id"
      "ORDER BY h.host_id, o.option_id"
     },
     },
 
 
+    //PgSqlHostDataSourceImpl::GET_HOST_SUBID4_DHCPID
     // Retrieves host information and DHCPv4 options using subnet identifier
     // Retrieves host information and DHCPv4 options using subnet identifier
     // and client's identifier. Left joining the dhcp4_options table results in
     // and client's identifier. Left joining the dhcp4_options table results in
     // multiple rows being returned for the same host.
     // multiple rows being returned for the same host.
-    { 3, //PgSqlHostDataSourceImpl::GET_HOST_SUBID4_DHCPID,
+    {3,
      { OID_INT4, OID_INT2, OID_BYTEA },
      { OID_INT4, OID_INT2, OID_BYTEA },
      "get_host_subid4_dhcpid",
      "get_host_subid4_dhcpid",
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
@@ -1343,10 +1368,11 @@ PgSqlTaggedStatement tagged_statements[] = {
      "ORDER BY h.host_id, o.option_id"
      "ORDER BY h.host_id, o.option_id"
     },
     },
 
 
+    //PgSqlHostDataSourceImpl::GET_HOST_SUBID6_DHCPID
     // Retrieves host information, IPv6 reservations and DHCPv6 options
     // Retrieves host information, IPv6 reservations and DHCPv6 options
     // associated with a host. The number of rows returned is a multiplication
     // associated with a host. The number of rows returned is a multiplication
     // of number of IPv6 reservations and DHCPv6 options.
     // of number of IPv6 reservations and DHCPv6 options.
-    {3, //PgSqlHostDataSourceImpl::GET_HOST_SUBID6_DHCPID,
+    {3,
      { OID_INT4, OID_INT2, OID_BYTEA },
      { OID_INT4, OID_INT2, OID_BYTEA },
      "get_host_subid6_dhcpid",
      "get_host_subid6_dhcpid",
      "SELECT h.host_id, h.dhcp_identifier, "
      "SELECT h.host_id, h.dhcp_identifier, "
@@ -1364,11 +1390,12 @@ PgSqlTaggedStatement tagged_statements[] = {
      "ORDER BY h.host_id, o.option_id, r.reservation_id"
      "ORDER BY h.host_id, o.option_id, r.reservation_id"
     },
     },
 
 
+    //PgSqlHostDataSourceImpl::GET_HOST_SUBID_ADDR
     // Retrieves host information and DHCPv4 options for the host using subnet
     // Retrieves host information and DHCPv4 options for the host using subnet
     // identifier and IPv4 reservation. Left joining the dhcp4_options table
     // identifier and IPv4 reservation. Left joining the dhcp4_options table
     // results in multiple rows being returned for the host. The number of
     // results in multiple rows being returned for the host. The number of
     // rows depends on the number of options defined for the host.
     // rows depends on the number of options defined for the host.
-    { 2,  //PgSqlHostDataSourceImpl::GET_HOST_SUBID_ADDR,
+    {2,
      { OID_INT4, OID_INT8 },
      { OID_INT4, OID_INT8 },
      "get_host_subid_addr",
      "get_host_subid_addr",
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
      "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, "
@@ -1381,13 +1408,14 @@ PgSqlTaggedStatement tagged_statements[] = {
      "ORDER BY h.host_id, o.option_id"
      "ORDER BY h.host_id, o.option_id"
     },
     },
 
 
+    // PgSqlHostDataSourceImpl::GET_HOST_PREFIX
     // Retrieves host information, IPv6 reservations and DHCPv6 options
     // Retrieves host information, IPv6 reservations and DHCPv6 options
     // associated with a host using prefix and prefix length. This query
     // associated with a host using prefix and prefix length. This query
     // returns host information for a single host. However, multiple rows
     // returns host information for a single host. However, multiple rows
     // are returned due to left joining IPv6 reservations and DHCPv6 options.
     // are returned due to left joining IPv6 reservations and DHCPv6 options.
     // The number of rows returned is multiplication of number of existing
     // The number of rows returned is multiplication of number of existing
     // IPv6 reservations and DHCPv6 options.
     // IPv6 reservations and DHCPv6 options.
-    {2, // PgSqlHostDataSourceImpl::GET_HOST_PREFIX,
+    {2, 
      { OID_VARCHAR, OID_INT2 },
      { OID_VARCHAR, OID_INT2 },
      "get_host_prefix",
      "get_host_prefix",
      "SELECT h.host_id, h.dhcp_identifier, "
      "SELECT h.host_id, h.dhcp_identifier, "
@@ -1407,8 +1435,9 @@ PgSqlTaggedStatement tagged_statements[] = {
      "ORDER BY h.host_id, o.option_id, r.reservation_id"
      "ORDER BY h.host_id, o.option_id, r.reservation_id"
     },
     },
 
 
+    //PgSqlHostDataSourceImpl::GET_VERSION
     // Retrieves MySQL schema version.
     // Retrieves MySQL schema version.
-    { 0, //PgSqlHostDataSourceImpl::GET_VERSION,
+    {0, 
      { OID_NONE },
      { OID_NONE },
      "get_version",
      "get_version",
      "SELECT version, minor FROM schema_version"
      "SELECT version, minor FROM schema_version"
@@ -1460,13 +1489,13 @@ PgSqlHostDataSourceImpl::addStatement(StatementIndex stindex,
     int s = PQresultStatus(r);
     int s = PQresultStatus(r);
 
 
     if (s != PGRES_COMMAND_OK) {
     if (s != PGRES_COMMAND_OK) {
-        // 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.
+        // Failure: check for the special case of duplicate entry.
         if (conn_.compareError(r, PgSqlConnection::DUPLICATE_KEY)) {
         if (conn_.compareError(r, PgSqlConnection::DUPLICATE_KEY)) {
             isc_throw(DuplicateEntry, "Database duplicate entry error");
             isc_throw(DuplicateEntry, "Database duplicate entry error");
         }
         }
 
 
+        // Connection determines if the error is fatal or not, and
+        // throws the appropriate exception
         conn_.checkStatementError(r, tagged_statements[stindex]);
         conn_.checkStatementError(r, tagged_statements[stindex]);
     }
     }
 
 
@@ -1748,6 +1777,11 @@ PgSqlHostDataSource::get4(const SubnetID& subnet_id,
 ConstHostPtr
 ConstHostPtr
 PgSqlHostDataSource::get4(const SubnetID& subnet_id,
 PgSqlHostDataSource::get4(const SubnetID& subnet_id,
                           const asiolink::IOAddress& address) const {
                           const asiolink::IOAddress& address) const {
+    if (!address.isV4()) {
+        isc_throw(BadValue, "PgSqlHostDataSource::get4(id, address) - "
+                  " wrong address type, address supplied is an IPv6 address");
+    }
+
     // Set up the WHERE clause value
     // Set up the WHERE clause value
     PsqlBindArrayPtr bind_array(new PsqlBindArray());
     PsqlBindArrayPtr bind_array(new PsqlBindArray());
 
 

+ 46 - 15
src/lib/dhcpsrv/pgsql_host_data_source.h

@@ -22,6 +22,16 @@ class PgSqlHostDataSourceImpl;
 /// This class implements the @ref isc::dhcp::BaseHostDataSource interface to
 /// This class implements the @ref isc::dhcp::BaseHostDataSource interface to
 /// the PostgreSQL database. Use of this backend presupposes that a PostgreSQL
 /// the PostgreSQL database. Use of this backend presupposes that a PostgreSQL
 /// database is available and that the Kea schema has been created within it.
 /// database is available and that the Kea schema has been created within it.
+///
+/// Reservations are uniquely identified by identifier type and value. Currently
+/// The currently supported values are defined in @ref Host::IdentifierType
+/// as well as in host_identifier_table:
+///
+/// - IDENT_HWADDR
+/// - IDENT_DUID
+/// - IDENT_CIRCUIT_ID
+/// - IDENT_CLIENT_ID
+///
 class PgSqlHostDataSource: public BaseHostDataSource {
 class PgSqlHostDataSource: public BaseHostDataSource {
 public:
 public:
 
 
@@ -50,6 +60,8 @@ public:
     PgSqlHostDataSource(const DatabaseConnection::ParameterMap& parameters);
     PgSqlHostDataSource(const DatabaseConnection::ParameterMap& parameters);
 
 
     /// @brief Virtual destructor.
     /// @brief Virtual destructor.
+    /// Frees database resources and closes the database connection through
+    /// the destruction of member impl_.
     virtual ~PgSqlHostDataSource();
     virtual ~PgSqlHostDataSource();
 
 
     /// @brief Return all hosts for the specified HW address or DUID.
     /// @brief Return all hosts for the specified HW address or DUID.
@@ -145,13 +157,11 @@ public:
     /// if this address is not reserved for some other host and do not allocate
     /// if this address is not reserved for some other host and do not allocate
     /// this address if reservation is present.
     /// this address if reservation is present.
     ///
     ///
-    /// Implementations of this method should guard against invalid addresses,
-    /// such as IPv6 address.
-    ///
     /// @param subnet_id Subnet identifier.
     /// @param subnet_id Subnet identifier.
     /// @param address reserved IPv4 address.
     /// @param address reserved IPv4 address.
     ///
     ///
     /// @return Const @c Host object using a specified IPv4 address.
     /// @return Const @c Host object using a specified IPv4 address.
+    /// @throw BadValue is given an IPv6 address
     virtual ConstHostPtr
     virtual ConstHostPtr
     get4(const SubnetID& subnet_id, const asiolink::IOAddress& address) const;
     get4(const SubnetID& subnet_id, const asiolink::IOAddress& address) const;
 
 
@@ -198,30 +208,51 @@ public:
 
 
     /// @brief Adds a new host to the collection.
     /// @brief Adds a new host to the collection.
     ///
     ///
-    /// The implementations of this method should guard against duplicate
-    /// reservations for the same host, where possible. For example, when the
-    /// reservation for the same HW address and subnet id is added twice, the
-    /// addHost method should throw an DuplicateEntry exception. Note, that
-    /// usually it is impossible to guard against adding duplicated host, where
-    /// one instance is identified by HW address, another one by DUID.
+    /// The method will insert the given host and all of its children (v4
+    /// options, v6 options, and v6 reservations) into the database.  It
+    /// relies on constraints defined as part of the PostgreSQL schema to
+    /// defend against duplicate entries and to ensure referential 
+    /// integrity.
+    ///
+    /// Violation of any of these constraints for a host will result in a
+    /// DuplicateEntry exception:
+    ///
+    /// -# IPV4_ADDRESS and DHCP4_SUBNET_ID combination must be unique
+    /// -# DHCP ID, DHCP ID TYPE, and DHCP4_SUBNET_ID combination must be unique
+    /// -# DHCP ID, DHCP ID TYPE, and DHCP6_SUBNET_ID combination must be unique
+    ///
+    /// In addition, violating the following referential contraints will
+    /// a DbOperationError exception:
+    ///
+    /// -# DHCP ID TYPE must be defined in the HOST_IDENTIFIER_TYPE table
+    /// -# For DHCP4 Options:
+    ///  -# HOST_ID must exist with HOSTS
+    ///  -# SCOPE_ID must be defined in DHCP_OPTION_SCOPE
+    /// -# For DHCP6 Options:
+    ///  -# HOST_ID must exist with HOSTS
+    ///  -# SCOPE_ID must be defined in DHCP_OPTION_SCOPE
+    /// -# For IPV6 Reservations:
+    ///  -# HOST_ID must exist with HOSTS
+    ///  -# Address and Prefix Length must be unique (DuplicateEntry)
     ///
     ///
     /// @param host Pointer to the new @c Host object being added.
     /// @param host Pointer to the new @c Host object being added.
+    /// @throw DuplicateEntry or DbOperationError dependent on the constraint
+    /// violation
     virtual void add(const HostPtr& host);
     virtual void add(const HostPtr& host);
 
 
     /// @brief Return backend type
     /// @brief Return backend type
     ///
     ///
-    /// Returns the type of the backend (e.g. "mysql", "memfile" etc.)
+    /// Returns the type of database as the string "postgresql".  This is
+    /// same value as used for configuration purposes.
     ///
     ///
     /// @return Type of the backend.
     /// @return Type of the backend.
     virtual std::string getType() const {
     virtual std::string getType() const {
         return (std::string("postgresql"));
         return (std::string("postgresql"));
     }
     }
 
 
-    /// @brief Returns backend name.
-    ///
-    /// Each backend have specific name.
+    /// @brief Returns the name of the open database
     ///
     ///
-    /// @return "mysql".
+    /// @return String containing the name of the database
     virtual std::string getName() const;
     virtual std::string getName() const;
 
 
     /// @brief Returns description of the backend.
     /// @brief Returns description of the backend.
@@ -244,7 +275,7 @@ public:
 private:
 private:
 
 
     /// @brief Pointer to the implementation of the @ref PgSqlHostDataSource.
     /// @brief Pointer to the implementation of the @ref PgSqlHostDataSource.
-    PgSqlHostDataSourceImpl* impl_; 
+    PgSqlHostDataSourceImpl* impl_;
 };
 };
 
 
 }
 }

+ 2 - 1
src/lib/dhcpsrv/pgsql_lease_mgr.cc

@@ -26,7 +26,8 @@ using namespace std;
 
 
 namespace {
 namespace {
 
 
-/// @todo TKM lease6 needs to accomodate hwaddr,hwtype, and hwaddr source columns
+/// @todo TKM lease6 needs to accomodate hwaddr,hwtype, and hwaddr source 
+/// columns.  This is coverd by tickets #3557, #4530, and PR#9.
 
 
 /// @brief Catalog of all the SQL statements currently supported.  Note
 /// @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 order columns appear in statement body must match the order they

+ 840 - 53
src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc

@@ -6,69 +6,21 @@
 
 
 #include <config.h>
 #include <config.h>
 
 
+#include <dhcpsrv/pgsql_connection.h>
 #include <dhcpsrv/pgsql_exchange.h>
 #include <dhcpsrv/pgsql_exchange.h>
 
 
 #include <boost/lexical_cast.hpp>
 #include <boost/lexical_cast.hpp>
 
 
 #include <gtest/gtest.h>
 #include <gtest/gtest.h>
 
 
+#include <sstream>
+#include <vector>
+
 using namespace isc;
 using namespace isc;
 using namespace isc::dhcp;
 using namespace isc::dhcp;
 
 
 namespace {
 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);
-}
-
 /// @brief Verifies the ability to add various data types to
 /// @brief Verifies the ability to add various data types to
 /// the bind array.
 /// the bind array.
 TEST(PsqlBindArray, addDataTest) {
 TEST(PsqlBindArray, addDataTest) {
@@ -138,5 +90,840 @@ TEST(PsqlBindArray, addDataTest) {
     EXPECT_EQ(expected, b.toText());
     EXPECT_EQ(expected, b.toText());
 }
 }
 
 
-}; // namespace
+/// @brief Defines a pointer to a PgSqlConnection
+typedef boost::shared_ptr<PgSqlConnection> PgSqlConnectionPtr;
+/// @brief Defines a pointer to a PgSqlResult
+typedef boost::shared_ptr<PgSqlResult> PgSqlResultPtr;
+
+/// @brief Fixture for exercising basic PostgreSQL operations and data types
+///
+/// This class is intended to be used to verify basic operations and to
+/// verify that each PostgreSQL  data type currently used by Kea, can be
+/// correctly written to and read from PostgreSQL.  Rather than use tables
+/// that belong to Kea the schema proper,  it creates its own. Currently it
+/// consists of a single table, called "basics" which contains one column for
+/// each of the supported data types.
+///
+/// It creates the schema during construction, deletes it upon destruction, and
+/// provides functions for executing SQL statements, executing prepared
+/// statements, fetching all rows in the table, and deleting all the rows in
+/// the table.
+class PgSqlBasicsTest : public ::testing::Test {
+public:
+    /// @brief Column index for each column
+    enum BasicColIndex {
+        ID_COL,
+        BOOL_COL,
+        BYTEA_COL,
+        BIGINT_COL,
+        SMALLINT_COL,
+        INT_COL,
+        TEXT_COL,
+        TIMESTAMP_COL,
+        VARCHAR_COL,
+        NUM_BASIC_COLS
+    };
+
+    /// @brief Constructor
+    ///
+    /// Creates the database connection, opens the database, and destroys
+    /// the table (if present) and then recreates it.
+    PgSqlBasicsTest() : expectedColNames_(NUM_BASIC_COLS) {
+        // Create database connection parameter list
+        PgSqlConnection::ParameterMap params;
+        params["name"] = "keatest";
+        params["user"] = "keatest";
+        params["password"] = "keatest";
+
+        // Create and open the database connection
+        conn_.reset(new PgSqlConnection(params));
+        conn_->openDatabase();
+
+        // Create the list of expected column names
+        expectedColNames_[ID_COL] = "id";
+        expectedColNames_[BOOL_COL] = "bool_col";
+        expectedColNames_[BYTEA_COL] = "bytea_col";
+        expectedColNames_[BIGINT_COL] = "bigint_col";
+        expectedColNames_[SMALLINT_COL] = "smallint_col";
+        expectedColNames_[INT_COL] = "int_col";
+        expectedColNames_[TEXT_COL] = "text_col";
+        expectedColNames_[TIMESTAMP_COL] = "timestamp_col";
+        expectedColNames_[VARCHAR_COL] = "varchar_col";
+
+        destroySchema();
+        createSchema();
+    }
+
+    /// @brief Destructor
+    ///
+    /// Destroys the table. The database resources are freed and the connection
+    /// closed by the destruction of conn_.
+    virtual ~PgSqlBasicsTest () {
+        destroySchema();
+    }
+
+    /// @brief Gets the expected name of the column for a given column index
+    ///
+    /// Returns the name of column as we expect it to be when the column is
+    /// fetched from the database.
+    ///
+    /// @param col index of the desired column
+    ///
+    /// @return string containing the column name
+    ///
+    /// @throw BadValue if the index is out of range
+    const std::string& expectedColumnName(int col) {
+        if (col < 0 || col >= NUM_BASIC_COLS) {
+            isc_throw(BadValue,
+                      "definedColunName: invalid column value" << col);
+        }
+
+        return (expectedColNames_[col]);
+    }
+
+    /// @brief Creates the basics table
+    /// Asserts if the creation step fails
+    void createSchema() {
+        // One column for OID type, plus an auto-increment
+        const char* sql =
+            "CREATE TABLE basics ( "
+            "    id SERIAL PRIMARY KEY NOT NULL, "
+            "    bool_col BOOLEAN, "
+            "    bytea_col BYTEA, "
+            "    bigint_col  BIGINT, "
+            "    smallint_col  SMALLINT, "
+            "    int_col INT, "
+            "    text_col TEXT, "
+            "    timestamp_col TIMESTAMP WITH TIME ZONE, "
+            "    varchar_col VARCHAR(255) "
+            "); ";
+
+        PgSqlResult r(PQexec(*conn_, sql));
+        ASSERT_EQ(PQresultStatus(r), PGRES_COMMAND_OK)
+                 << " create basics table failed: " << PQerrorMessage(*conn_);
+    }
+
+    /// @brief Destroys the basics table
+    /// Asserts if the destruction fails
+    void destroySchema() {
+        if (conn_) {
+            PgSqlResult r(PQexec(*conn_, "DROP TABLE IF EXISTS basics;"));
+            ASSERT_EQ(PQresultStatus(r), PGRES_COMMAND_OK)
+                 << " drop basics table failed: " << PQerrorMessage(*conn_);
+        }
+    }
+
+    /// @brief Executes a SQL statement and tests for an expected outcome
+    ///
+    /// @param r pointer which will contain the result set returned by the
+    /// statment's execution.
+    /// @param sql string containing the SQL statement text.  Note that
+    /// PostgreSQL supports executing text which contains more than one SQL
+    /// statement separated by semicolons.
+    /// @param exp_outcome expected status value returned with within the
+    /// result set such as PGRES_COMMAND_OK, PGRES_TUPLES_OK.
+    /// @lineno line number from where the call was invoked
+    ///
+    /// Asserts if the result set status does not equal the expected outcome.
+    void runSql(PgSqlResultPtr& r, const std::string sql, int exp_outcome,
+                int lineno) {
+        r.reset(new PgSqlResult(PQexec(*conn_, sql.c_str())));
+        ASSERT_EQ(PQresultStatus(*r), exp_outcome)
+                  << " runSql at line: " << lineno << " failed, sql:[" << sql
+                  << "]\n reason: " << PQerrorMessage(*conn_);
+    }
+
+    /// @brief Executes a SQL statement and tests for an expected outcome
+    ///
+    /// @param r pointer which will contain the result set returned by the
+    /// statment's execution.
+    /// @param statement statement descriptor of the prepared statement
+    /// to execute.
+    /// @param bind_array bind array containing the input values to submit
+    /// along with the statement
+    /// @param exp_outcome expected status value returned with within the
+    /// result set such as PGRES_COMMAND_OK, PGRES_TUPLES_OK.
+    /// @lineno line number from where the call was invoked
+    ///
+    /// Asserts if the result set status does not equal the expected outcome.
+    void runPreparedStatement(PgSqlResultPtr& r,
+                              PgSqlTaggedStatement& statement,
+                              PsqlBindArrayPtr bind_array, int exp_outcome,
+                              int lineno) {
+        r.reset(new PgSqlResult(PQexecPrepared(*conn_, statement.name,
+                                statement.nbparams,
+                                &bind_array->values_[0],
+                                &bind_array->lengths_[0],
+                                &bind_array->formats_[0], 0)));
+        ASSERT_EQ(PQresultStatus(*r), exp_outcome)
+                  << " runPreparedStatement at line: " << lineno
+                  << " statement name:[" << statement.name
+                  << "]\n reason: " << PQerrorMessage(*conn_);
+    }
+
+    /// @brief Fetches all of the rows currently in the table
+    ///
+    /// Executes a select statement which returns all of the rows in the
+    /// basics table, in their order of insertion.   Each row contains all
+    /// of the defined columns, in the order they are defined.
+    ///
+    /// @param r pointer which will contain the result set returned by the
+    /// statment's execution.
+    /// @param exp_rows expected number of rows fetched. (This can be 0).
+    /// @lineno line number from where the call was invoked
+    ///
+    /// Asserts if the result set status does not equal the expected outcome.
+    void fetchRows(PgSqlResultPtr& r, int exp_rows, int line) {
+        std::string sql =
+            "SELECT"
+            "   id, bool_col, bytea_col, bigint_col, smallint_col, "
+            "   int_col, text_col,"
+            "   extract(epoch from timestamp_col)::bigint as timestamp_col,"
+            "   varchar_col FROM basics";
+
+        runSql(r, sql, PGRES_TUPLES_OK, line);
+        ASSERT_EQ(r->getRows(), exp_rows) << "fetch at line: " << line
+                  << " wrong row count, expected: " << exp_rows
+                  << " , have: " << r->getRows();
+
+    }
+
+    /// @brief Database connection
+    PgSqlConnectionPtr conn_;
+
+    /// @brief List of column names as we expect them to be in fetched rows
+    std::vector<std::string> expectedColNames_;
+};
+
+// Macros defined to ease passing invocation line number for output tracing
+// (Yes I could have used scoped tracing but that's so ugly in code...)
+#define RUN_SQL(a,b,c) (runSql(a,b,c, __LINE__))
+#define RUN_PREP(a,b,c,d) (runPreparedStatement(a,b,c,d, __LINE__))
+#define FETCH_ROWS(a,b) (fetchRows(a,b,__LINE__))
+#define WIPE_ROWS(a) (RUN_SQL(a, "DELETE FROM BASICS", PGRES_COMMAND_OK))
+
+/// @brief Verifies that PgResultSet row and colum meta-data is correct
+TEST_F(PgSqlBasicsTest, rowColumnBasics) {
+    // We fetch the table contents, which at this point should be no rows.
+    PgSqlResultPtr r;
+    FETCH_ROWS(r, 0);
+
+    // Column meta-data is deteremined by the select statement and is
+    // present whether or not any rows were returned.
+    EXPECT_EQ(r->getCols(), NUM_BASIC_COLS);
+
+    // Negative indexes should be out of range.  We test negative values
+    // as PostgreSQL functions accept column values as type int.
+    EXPECT_THROW(r->colCheck(-1), DbOperationError);
+
+    // Iterate over the column indexes verifying:
+    // 1. the column is valid
+    // 2. the result set column name matches the expected column name
+    for (int i = 0; i < NUM_BASIC_COLS; i++) {
+        EXPECT_NO_THROW(r->colCheck(i));
+        EXPECT_EQ(r->getColumnLabel(i), expectedColumnName(i));
+    }
+
+    // Verify above range column value is detected.
+    EXPECT_THROW(r->colCheck(NUM_BASIC_COLS), DbOperationError);
+
+    // Verify the fetching a column label for out of range columns
+    // do NOT throw.
+    std::string label;
+    ASSERT_NO_THROW(label = r->getColumnLabel(-1));
+    EXPECT_EQ(label, "Unknown column:-1");
+    ASSERT_NO_THROW(label = r->getColumnLabel(NUM_BASIC_COLS));
+    std::ostringstream os;
+    os << "Unknown column:" << NUM_BASIC_COLS;
+    EXPECT_EQ(label, os.str());
+
+    // Verify row count and checking. With an empty result set all values of
+    // row are invalid.
+    EXPECT_EQ(r->getRows(), 0);
+    EXPECT_THROW(r->rowCheck(-1), DbOperationError);
+    EXPECT_THROW(r->rowCheck(0), DbOperationError);
+    EXPECT_THROW(r->rowCheck(1), DbOperationError);
+
+    // Verify Row-column check will always fail with an empty result set.
+    EXPECT_THROW(r->rowColCheck(-1, 1), DbOperationError);
+    EXPECT_THROW(r->rowColCheck(0, 1), DbOperationError);
+    EXPECT_THROW(r->rowColCheck(1, 1), DbOperationError);
+
+    // Insert three minimal rows.  We don't really care about column content
+    // for this test.
+    int num_rows = 3;
+    for (int i = 0; i < num_rows; i++) {
+        RUN_SQL(r, "INSERT INTO basics (bool_col) VALUES ('t')",
+                PGRES_COMMAND_OK);
+    }
+
+    // Fetch the newly created rows.
+    FETCH_ROWS(r, num_rows);
+
+    // Verify we row count and checking
+    EXPECT_EQ(r->getRows(), num_rows);
+    EXPECT_THROW(r->rowCheck(-1), DbOperationError);
+
+    // Iterate over the row count, verifying that expected rows are valid
+    for (int i = 0; i < num_rows; i++) {
+        EXPECT_NO_THROW(r->rowCheck(i));
+        EXPECT_NO_THROW(r->rowColCheck(i, 0));
+    }
+
+    // Verify an above range row is detected.
+    EXPECT_THROW(r->rowCheck(num_rows), DbOperationError);
+}
+
+/// @brief Verify that we can read and write BOOL columns
+TEST_F(PgSqlBasicsTest, boolTest) {
+    // Create a prepared statement for inserting bool_col
+    const char* st_name = "bool_insert";
+    PgSqlTaggedStatement statement[] = {
+     {1, { OID_BOOL }, st_name,
+      "INSERT INTO BASICS (bool_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    bool bools[] = { true, false };
+    PsqlBindArrayPtr bind_array(new PsqlBindArray());
+    PgSqlResultPtr r;
+
+    // Insert bool rows
+    for (int i = 0; i < 2; ++i) {
+        bind_array.reset(new PsqlBindArray());
+        bind_array->add(bools[i]);
+        RUN_PREP(r,statement[0], bind_array, PGRES_COMMAND_OK);
+    }
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, 2);
+
+    // Verify the fetched bool values are what we expect.
+    bool fetched_bool;
+    int row = 0;
+    for ( ; row  < 2; ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, BOOL_COL));
+
+        // Fetch and verify the column value
+        fetched_bool = !bools[row];
+        ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, row, BOOL_COL,
+                                                      fetched_bool));
+        EXPECT_EQ(fetched_bool, bools[row]);
+    }
+
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::getColumnValue(*r, row, 1, fetched_bool),
+                 DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL boolean
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull();
+
+    // Run the insert with the bind array.
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted row.
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, 1));
+}
+
+/// @brief Verify that we can read and write BYTEA columns
+TEST_F(PgSqlBasicsTest, byteaTest) {
+    const char* st_name = "bytea_insert";
+    PgSqlTaggedStatement statement[] = {
+     {1, { OID_BYTEA }, st_name,
+      "INSERT INTO BASICS (bytea_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    const uint8_t bytes[] = {
+        0x01, 0x02, 0x03, 0x04
+    };
+    std::vector<uint8_t> vbytes(bytes, bytes + sizeof(bytes));
+
+    // Verify we can insert bytea from a vector
+    PsqlBindArrayPtr bind_array(new PsqlBindArray());
+    PgSqlResultPtr r;
+    bind_array->add(vbytes);
+    RUN_PREP(r,statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Verify we can insert bytea from a buffer.
+    bind_array.reset(new PsqlBindArray());
+    bind_array->add(bytes, sizeof(bytes));
+    RUN_PREP(r,statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted rows.
+    int num_rows = 2;
+    FETCH_ROWS(r, num_rows);
+
+    uint8_t fetched_bytes[sizeof(bytes)];
+    size_t byte_count;
+    int row = 0;
+    for ( ; row < num_rows; ++row) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, BYTEA_COL));
+
+        // Extract the data into a correctly sized buffer
+        memset(fetched_bytes, 0, sizeof(fetched_bytes));
+        ASSERT_NO_THROW(PgSqlExchange::convertFromBytea(*r, row, BYTEA_COL,
+                                                        fetched_bytes,
+                                                        sizeof(fetched_bytes),
+                                                        byte_count));
+
+        // Verify the data is correct
+        ASSERT_EQ(byte_count, sizeof(bytes));
+        for (int i = 0; i < sizeof(bytes); i++) {
+            ASSERT_EQ(bytes[i], fetched_bytes[i]);
+        }
+    }
+
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::convertFromBytea(*r, row, BYTEA_COL,
+                                                 fetched_bytes,
+                                                 sizeof(fetched_bytes),
+                                                 byte_count),
+                 DbOperationError);
+
+    // Verify that too small of a buffer throws
+    ASSERT_THROW(PgSqlExchange::convertFromBytea(*r, 0, BYTEA_COL,
+                                                 fetched_bytes,
+                                                 sizeof(fetched_bytes) - 1,
+                                                 byte_count),
+                  DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL for a bytea column
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull(PsqlBindArray::BINARY_FMT);
+    RUN_PREP(r,statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted row.
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, BYTEA_COL));
+
+    // Verify that fetching a NULL bytea, returns 0 byte count
+    ASSERT_NO_THROW(PgSqlExchange::convertFromBytea(*r, 0, BYTEA_COL,
+                                                    fetched_bytes,
+                                                    sizeof(fetched_bytes),
+                                                    byte_count));
+    EXPECT_EQ(byte_count, 0);
+}
+
+/// @brief Verify that we can read and write BIGINT columns
+TEST_F(PgSqlBasicsTest, bigIntTest) {
+    // Create a prepared statement for inserting BIGINT
+    const char* st_name = "bigint_insert";
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_INT8 }, st_name,
+          "INSERT INTO BASICS (bigint_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Build our reference list of reference values
+    std::vector<int64_t> ints;
+    ints.push_back(-1);
+    ints.push_back(0);
+    ints.push_back(0x7fffffffffffffff);
+    ints.push_back(0xffffffffffffffff);
+
+    // Insert a row for each reference value
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    for (int i = 0; i < ints.size(); ++i) {
+        bind_array.reset(new PsqlBindArray());
+        bind_array->add(ints[i]);
+        RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+    }
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, ints.size());
+
+    // Iterate over the rows, verifying each value against its reference
+    int64_t fetched_int;
+    int row = 0;
+    for ( ; row  < ints.size(); ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, BIGINT_COL));
+
+        // Fetch and verify the column value
+        fetched_int = 777;
+        ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, row, BIGINT_COL,
+                                                      fetched_int));
+        EXPECT_EQ(fetched_int, ints[row]);
+    }
 
 
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::getColumnValue(*r, row, BIGINT_COL,
+                                               fetched_int),
+                 DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL value.
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull();
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted row.
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, BIGINT_COL));
+}
+
+/// @brief Verify that we can read and write SMALLINT columns
+TEST_F(PgSqlBasicsTest, smallIntTest) {
+    // Create a prepared statement for inserting a SMALLINT
+    const char* st_name = "smallint_insert";
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_INT2 }, st_name,
+          "INSERT INTO BASICS (smallint_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Build our reference list of reference values
+    std::vector<int16_t>ints;
+    ints.push_back(-1);
+    ints.push_back(0);
+    ints.push_back(0x7fff);
+    ints.push_back(0xffff);
+
+    // Insert a row for each reference value
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    for (int i = 0; i < ints.size(); ++i) {
+        bind_array.reset(new PsqlBindArray());
+        bind_array->add(ints[i]);
+        RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+    }
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, ints.size());
+
+    // Iterate over the rows, verifying each value against its reference
+    int16_t fetched_int;
+    int row = 0;
+    for ( ; row  < ints.size(); ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, SMALLINT_COL));
+
+        // Fetch and verify the column value
+        fetched_int = 777;
+        ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, row, SMALLINT_COL,
+                                                      fetched_int));
+        EXPECT_EQ(fetched_int, ints[row]);
+    }
+
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::getColumnValue(*r, row, SMALLINT_COL,
+                                               fetched_int),
+                 DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL value.
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull();
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted row.
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, SMALLINT_COL));
+}
+
+/// @brief Verify that we can read and write INT columns
+TEST_F(PgSqlBasicsTest, intTest) {
+    // Create a prepared statement for inserting an  INT
+    const char* st_name = "int_insert";
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_INT4 }, st_name,
+          "INSERT INTO BASICS (int_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Build our reference list of reference values
+    std::vector<int32_t> ints;
+    ints.push_back(-1);
+    ints.push_back(0);
+    ints.push_back(0x7fffffff);
+    ints.push_back(0xffffffff);
+
+    // Insert a row for each reference value
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    for (int i = 0; i < ints.size(); ++i) {
+        bind_array.reset(new PsqlBindArray());
+        bind_array->add(ints[i]);
+        RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+    }
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, ints.size());
+
+    // Iterate over the rows, verifying each value against its reference
+    int32_t fetched_int;
+    int row = 0;
+    for ( ; row  < ints.size(); ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, INT_COL));
+
+        // Fetch and verify the column value
+        fetched_int = 777;
+        ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, row, INT_COL,
+                                                      fetched_int));
+        EXPECT_EQ(fetched_int, ints[row]);
+    }
+
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::getColumnValue(*r, row, INT_COL, fetched_int),
+                 DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL value.
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull();
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted rows
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, INT_COL));
+}
+
+/// @brief Verify that we can read and write TEXT columns
+TEST_F(PgSqlBasicsTest, textTest) {
+    // Create a prepared statement for inserting TEXT
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_TEXT }, "text_insert",
+          "INSERT INTO BASICS (text_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Our reference string.
+    std::string ref_string = "This is a text string";
+
+    // Insert the reference from std::string
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    bind_array.reset(new PsqlBindArray());
+    bind_array->add(ref_string);
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Insert the reference from a buffer
+    bind_array.reset(new PsqlBindArray());
+    bind_array->add(ref_string.c_str());
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, 2);
+
+    // Iterate over the rows, verifying the value against the reference
+    std::string fetched_str;
+    int row = 0;
+    for ( ; row  < 2; ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, TEXT_COL));
+
+        // Fetch and verify the column value
+        fetched_str = "";
+        ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, row, TEXT_COL,
+                                                      fetched_str));
+        EXPECT_EQ(fetched_str, ref_string);
+    }
+
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::getColumnValue(*r, row, TEXT_COL, fetched_str),
+                 DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL value.
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull();
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted row.
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, TEXT_COL));
+}
+
+/// @brief Verify that we can read and write VARCHAR columns
+TEST_F(PgSqlBasicsTest, varcharTest) {
+    // Create a prepared statement for inserting a VARCHAR
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_VARCHAR }, "varchar_insert",
+          "INSERT INTO BASICS (varchar_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Our reference string.
+    std::string ref_string = "This is a varchar string";
+
+    // Insert the reference from std::string
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    bind_array.reset(new PsqlBindArray());
+    bind_array->add(ref_string);
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Insert the reference from a buffer
+    bind_array.reset(new PsqlBindArray());
+    bind_array->add(ref_string.c_str());
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, 2);
+
+    // Iterate over the rows, verifying the value against the reference
+    std::string fetched_str;
+    int row = 0;
+    for ( ; row  < 2; ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, VARCHAR_COL));
+
+        // Fetch and verify the column value
+        fetched_str = "";
+        ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, row, VARCHAR_COL,
+                                                      fetched_str));
+        EXPECT_EQ(fetched_str, ref_string);
+    }
+
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::getColumnValue(*r, row, VARCHAR_COL,
+                                               fetched_str),
+                 DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL value.
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull();
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted rows
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, VARCHAR_COL));
+}
+
+/// @brief Verify that we can read and write TIMESTAMP columns
+TEST_F(PgSqlBasicsTest, timeStampTest) {
+    // Create a prepared statement for inserting a TIMESTAMP
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_TIMESTAMP }, "timestamp_insert",
+          "INSERT INTO BASICS (timestamp_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Build our list of reference times
+    time_t now;
+    time(&now);
+    std::vector<time_t> times;
+    times.push_back(now);
+    times.push_back(DatabaseConnection::MAX_DB_TIME);
+    // Note on a 32-bit OS this value is really -1. PosgreSQL will store it
+    // and return it intact.
+    times.push_back(0xFFFFFFFF);
+
+    // Insert a row for each reference value
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    std::string time_str;
+    for (int i = 0; i < times.size(); ++i) {
+        // Timestamps are inserted as strings so convert them first
+        ASSERT_NO_THROW(time_str =
+                        PgSqlExchange::convertToDatabaseTime(times[i]));
+
+        // Add it to the bind array and insert it
+        bind_array.reset(new PsqlBindArray());
+        bind_array->add(time_str);
+        RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+    }
+
+    // Insert a row with ref time plus one day
+    times.push_back(now + 24*3600);
+    ASSERT_NO_THROW(time_str =
+                    PgSqlExchange::convertToDatabaseTime(times[0], 24*3600));
+
+    bind_array.reset(new PsqlBindArray());
+    bind_array->add(time_str);
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, times.size());
+
+    // Iterate over the rows, verifying the value against its reference
+    std::string fetched_str;
+    int row = 0;
+    for ( ; row  < times.size(); ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, TIMESTAMP_COL));
+
+        // Fetch and verify the column value
+        fetched_str = "";
+        ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, row, TIMESTAMP_COL,
+                                                      fetched_str));
+
+        time_t fetched_time;
+        ASSERT_NO_THROW(fetched_time =
+                        PgSqlExchange::convertFromDatabaseTime(fetched_str));
+        EXPECT_EQ(fetched_time, times[row]) << " row: " << row;
+    }
+
+    // While we here, verify that bad row throws
+    ASSERT_THROW(PgSqlExchange::getColumnValue(*r, row, TIMESTAMP_COL,
+                                               fetched_str),
+                 DbOperationError);
+
+    // Clean out the table
+    WIPE_ROWS(r);
+
+    // Verify we can insert a NULL value.
+    bind_array.reset(new PsqlBindArray());
+    bind_array->addNull();
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted rows
+    FETCH_ROWS(r, 1);
+
+    // Verify the column is null.
+    ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, TIMESTAMP_COL));
+
+    // Verify exceeding max time throws
+    ASSERT_THROW(PgSqlExchange::convertToDatabaseTime(times[0],
+                                                      DatabaseConnection::
+                                                      MAX_DB_TIME),
+                 BadValue);
+}
+
+}; // namespace