From: Thomas Markwalder Date: Thu, 14 Jul 2016 11:34:06 +0000 (-0400) Subject: [4277] Addressed bulk of review comments X-Git-Tag: trac4551_base~12^2~4 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=15b51b6229b550a3c0e672a79f832dba4a5ced58;p=thirdparty%2Fkea.git [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 --- diff --git a/src/lib/dhcpsrv/pgsql_connection.cc b/src/lib/dhcpsrv/pgsql_connection.cc index 7af6576689..9d722a458a 100644 --- a/src/lib/dhcpsrv/pgsql_connection.cc +++ b/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; +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) : conn_(conn), committed_(false) { conn_.startTransaction(); diff --git a/src/lib/dhcpsrv/pgsql_connection.h b/src/lib/dhcpsrv/pgsql_connection.h index 664b8fdb3b..d75e818275 100755 --- a/src/lib/dhcpsrv/pgsql_connection.h +++ b/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 /// associated prepared statement. struct PgSqlTaggedStatement { - /// Number of parameters for a given query int nbparams; @@ -48,16 +47,16 @@ struct PgSqlTaggedStatement { /// @brief Constants for PostgreSQL data types /// This are defined by PostreSQL in , 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_BOOL = 16; const size_t OID_BYTEA = 17; 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_INT4 = 23; // 4 byte int const size_t OID_TEXT = 25; -const size_t OID_TIMESTAMP = 1114; 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 /// 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 /// /// Frees the result set - ~PgSqlResult() { - if (result_) { - PQclear(result_); - } - } + ~PgSqlResult(); /// @brief Returns the number of rows in the result set. int getRows() const { @@ -117,35 +105,34 @@ public: /// /// @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 /// /// @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 /// /// @param row 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 - 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:" + /// + /// @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 /// @@ -275,7 +262,7 @@ public: /// @brief Commits transaction. /// /// Commits all changes made during the transaction by executing the - /// SQL statement: "COMMIT"> + /// SQL statement: "COMMIT" /// /// @throw DbOperationError if statement execution fails void commit(); @@ -410,8 +397,6 @@ public: }; - - }; // end of isc::dhcp namespace }; // end of isc namespace diff --git a/src/lib/dhcpsrv/pgsql_exchange.cc b/src/lib/dhcpsrv/pgsql_exchange.cc index 00f7813acc..dc38e98638 100644 --- a/src/lib/dhcpsrv/pgsql_exchange.cc +++ b/src/lib/dhcpsrv/pgsql_exchange.cc @@ -21,6 +21,10 @@ const char* PsqlBindArray::TRUE_STR = "TRUE"; const char* PsqlBindArray::FALSE_STR = "FALSE"; void PsqlBindArray::add(const char* value) { + if (!value) { + isc_throw(BadValue, "PsqlBindArray::add - char* value cannot be NULL"); + } + values_.push_back(value); lengths_.push_back(strlen(value)); formats_.push_back(TEXT_FMT); @@ -39,6 +43,10 @@ void PsqlBindArray::add(const std::vector& data) { } 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(&(data[0]))); lengths_.push_back(len); formats_.push_back(BINARY_FMT); @@ -70,7 +78,11 @@ void PsqlBindArray::addNull(const int 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) { bound_strs_.push_back(ConstStringPtr(new std::string(str))); PsqlBindArray::add((bound_strs_.back())->c_str()); @@ -246,15 +258,7 @@ PgSqlExchange::convertFromBytea(const PgSqlResult& r, const int row, std::string 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 @@ -264,7 +268,7 @@ PgSqlExchange::dumpRow(const PgSqlResult& r, int row) { int columns = r.getCols(); for (int col = 0; col < columns; ++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); stream << col << " " << name << " : " ; diff --git a/src/lib/dhcpsrv/pgsql_exchange.h b/src/lib/dhcpsrv/pgsql_exchange.h index 00cb6f6e3d..5658b8bc32 100644 --- a/src/lib/dhcpsrv/pgsql_exchange.h +++ b/src/lib/dhcpsrv/pgsql_exchange.h @@ -82,6 +82,7 @@ struct PsqlBindArray { /// remains in scope until the bind array has been discarded. /// /// @param value char array containing the null-terminated text to add. + /// @throw DbOperationError if value is NULL. void add(const char* value); /// @brief Adds an string value to the bind array @@ -113,6 +114,7 @@ struct PsqlBindArray { /// /// @param data buffer of binary data. /// @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); /// @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. /// /// 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, /// without the caller maintaining the string values. /// @@ -262,7 +264,15 @@ public: static const char* getRawColumnValue(const PgSqlResult& r, const int row, 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:". 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); /// @brief Fetches text column value as a string @@ -319,6 +329,8 @@ public: /// @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 True if the column values in the row is NULL, false otherwise. static bool isColumnNull(const PgSqlResult& r, const int row, const size_t col); @@ -372,6 +384,8 @@ public: /// /// @param r the result set containing the query results /// @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); protected: diff --git a/src/lib/dhcpsrv/pgsql_host_data_source.cc b/src/lib/dhcpsrv/pgsql_host_data_source.cc index 0cc63e1ea8..ee0d9c3ed7 100644 --- a/src/lib/dhcpsrv/pgsql_host_data_source.cc +++ b/src/lib/dhcpsrv/pgsql_host_data_source.cc @@ -34,6 +34,8 @@ using namespace std; namespace { /// @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; /// @brief Numeric value representing last supported identifier. @@ -106,7 +108,7 @@ public: /// @brief Reinitializes state information /// /// 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. virtual void clear() { host_.reset(); @@ -375,13 +377,30 @@ private: /// @brief Creates instance of the currently processed option. /// /// 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 /// option. If the current value is greater than the id of the recently /// processed option it is assumed that the processed row holds new /// option information. In such case the option instance is created and /// 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 /// option instances should be inserted. /// @param r result set containing one or more rows from a dhcp @@ -421,8 +440,6 @@ private: sizeof(value), value_len); // 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; PgSqlExchange::getColumnValue(r, row, formatted_value_index_, formatted_value); @@ -596,7 +613,7 @@ public: /// @brief Clears state information /// /// 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. virtual void clear() { PgSqlHostExchange::clear(); @@ -611,10 +628,11 @@ public: /// @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. virtual void processRowData(ConstHostCollection& hosts, @@ -685,7 +703,7 @@ private: /// host information, DHCPv4 options, DHCPv6 options and IPv6 reservations. /// /// 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 /// (DHCPv6 options and reservations), or entire information about the host /// (DHCPv4 options, DHCPv6 options and reservations). The following are the @@ -727,7 +745,7 @@ public: /// @brief Reinitializes state information /// /// 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. void clear() { PgSqlHostWithOptionsExchange::clear(); @@ -921,7 +939,7 @@ public: bind_array->add(resv.getPrefixLen()); // 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; bind_array->add(type); @@ -1089,8 +1107,8 @@ public: enum StatementIndex { INSERT_HOST, // Insert new host to collection 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_ADDR, // Gets hosts by IPv4 address 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. /// 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. /// /// @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 /// retrieve hosts from the database. PgSqlTaggedStatement tagged_statements[] = { + // PgSqlHostDataSourceImpl::INSERT_HOST // Inserts a host into the 'hosts' table. Returns the inserted host id. - {8, // PgSqlHostDataSourceImpl::INSERT_HOST, + {8, { OID_BYTEA, OID_INT2, OID_INT4, OID_INT4, OID_INT8, 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" }, + //PgSqlHostDataSourceImpl::INSERT_V6_RESRV // Inserts a single IPv6 reservation into 'reservations' table. - {5, //PgSqlHostDataSourceImpl::INSERT_V6_RESRV, + {5, { OID_VARCHAR, OID_INT2, OID_INT4, OID_INT4, OID_INT4 }, "insert_v6_resrv", "INSERT INTO ipv6_reservations(address, prefix_len, type, " @@ -1266,9 +1286,10 @@ PgSqlTaggedStatement tagged_statements[] = { "VALUES ($1, $2, $3, $4, $5)" }, + // PgSqlHostDataSourceImpl::INSERT_V4_HOST_OPTION // Inserts a single DHCPv4 option into 'dhcp4_options' table. // 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_VARCHAR, OID_BOOL, OID_INT8}, "insert_v4_host_option", @@ -1277,9 +1298,10 @@ PgSqlTaggedStatement tagged_statements[] = { "VALUES ($1, $2, $3, $4, $5, $6, 3)" }, + // PgSqlHostDataSourceImpl::INSERT_V6_HOST_OPTION // Inserts a single DHCPv6 option into 'dhcp6_options' table. // 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_VARCHAR, OID_BOOL, OID_INT8}, "insert_v6_host_option", @@ -1288,11 +1310,12 @@ PgSqlTaggedStatement tagged_statements[] = { "VALUES ($1, $2, $3, $4, $5, $6, 3)" }, + // PgSqlHostDataSourceImpl::GET_HOST_DHCPID // Retrieves host information, IPv6 reservations and both DHCPv4 and // DHCPv6 options associated with the host. The LEFT JOIN clause is used // to retrieve information from 4 different tables using a single query. // Hence, this query returns multiple rows for a single host. - {2, // PgSqlHostDataSourceImpl::GET_HOST_DHCPID, + {2, { OID_BYTEA, OID_INT2 }, "get_host_dhcpid", "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" }, + // PgSqlHostDataSourceImpl::GET_HOST_ADDR // Retrieves host information along with the DHCPv4 options associated with // it. Left joining the dhcp4_options table results in multiple rows being // returned for the same host. The host is retrieved by IPv4 address. - { 1, // PgSqlHostDataSourceImpl::GET_HOST_ADDR, + {1, { OID_INT8 }, "get_host_addr", "SELECT h.host_id, h.dhcp_identifier, h.dhcp_identifier_type, " " 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" }, + //PgSqlHostDataSourceImpl::GET_HOST_SUBID4_DHCPID // Retrieves host information and DHCPv4 options using subnet identifier // and client's identifier. Left joining the dhcp4_options table results in // multiple rows being returned for the same host. - { 3, //PgSqlHostDataSourceImpl::GET_HOST_SUBID4_DHCPID, + {3, { OID_INT4, OID_INT2, OID_BYTEA }, "get_host_subid4_dhcpid", "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" }, + //PgSqlHostDataSourceImpl::GET_HOST_SUBID6_DHCPID // Retrieves host information, IPv6 reservations and DHCPv6 options // associated with a host. The number of rows returned is a multiplication // of number of IPv6 reservations and DHCPv6 options. - {3, //PgSqlHostDataSourceImpl::GET_HOST_SUBID6_DHCPID, + {3, { OID_INT4, OID_INT2, OID_BYTEA }, "get_host_subid6_dhcpid", "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" }, + //PgSqlHostDataSourceImpl::GET_HOST_SUBID_ADDR // Retrieves host information and DHCPv4 options for the host using subnet // identifier and IPv4 reservation. Left joining the dhcp4_options table // results in multiple rows being returned for the host. The number of // rows depends on the number of options defined for the host. - { 2, //PgSqlHostDataSourceImpl::GET_HOST_SUBID_ADDR, + {2, { OID_INT4, OID_INT8 }, "get_host_subid_addr", "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" }, + // PgSqlHostDataSourceImpl::GET_HOST_PREFIX // Retrieves host information, IPv6 reservations and DHCPv6 options // associated with a host using prefix and prefix length. This query // returns host information for a single host. However, multiple rows // are returned due to left joining IPv6 reservations and DHCPv6 options. // The number of rows returned is multiplication of number of existing // IPv6 reservations and DHCPv6 options. - {2, // PgSqlHostDataSourceImpl::GET_HOST_PREFIX, + {2, { OID_VARCHAR, OID_INT2 }, "get_host_prefix", "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" }, + //PgSqlHostDataSourceImpl::GET_VERSION // Retrieves MySQL schema version. - { 0, //PgSqlHostDataSourceImpl::GET_VERSION, + {0, { OID_NONE }, "get_version", "SELECT version, minor FROM schema_version" @@ -1460,13 +1489,13 @@ PgSqlHostDataSourceImpl::addStatement(StatementIndex stindex, int s = PQresultStatus(r); 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)) { 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]); } @@ -1748,6 +1777,11 @@ PgSqlHostDataSource::get4(const SubnetID& subnet_id, ConstHostPtr PgSqlHostDataSource::get4(const SubnetID& subnet_id, 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 PsqlBindArrayPtr bind_array(new PsqlBindArray()); diff --git a/src/lib/dhcpsrv/pgsql_host_data_source.h b/src/lib/dhcpsrv/pgsql_host_data_source.h index 3f0de33f18..e4703e2580 100644 --- a/src/lib/dhcpsrv/pgsql_host_data_source.h +++ b/src/lib/dhcpsrv/pgsql_host_data_source.h @@ -22,6 +22,16 @@ class PgSqlHostDataSourceImpl; /// This class implements the @ref isc::dhcp::BaseHostDataSource interface to /// the PostgreSQL database. Use of this backend presupposes that a PostgreSQL /// 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 { public: @@ -50,6 +60,8 @@ public: PgSqlHostDataSource(const DatabaseConnection::ParameterMap& parameters); /// @brief Virtual destructor. + /// Frees database resources and closes the database connection through + /// the destruction of member impl_. virtual ~PgSqlHostDataSource(); /// @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 /// 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 address reserved IPv4 address. /// /// @return Const @c Host object using a specified IPv4 address. + /// @throw BadValue is given an IPv6 address virtual ConstHostPtr get4(const SubnetID& subnet_id, const asiolink::IOAddress& address) const; @@ -198,30 +208,51 @@ public: /// @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. + /// @throw DuplicateEntry or DbOperationError dependent on the constraint + /// violation virtual void add(const HostPtr& host); /// @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. virtual std::string getType() const { 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; /// @brief Returns description of the backend. @@ -244,7 +275,7 @@ public: private: /// @brief Pointer to the implementation of the @ref PgSqlHostDataSource. - PgSqlHostDataSourceImpl* impl_; + PgSqlHostDataSourceImpl* impl_; }; } diff --git a/src/lib/dhcpsrv/pgsql_lease_mgr.cc b/src/lib/dhcpsrv/pgsql_lease_mgr.cc index 693fb95db8..6e8cdc15c0 100644 --- a/src/lib/dhcpsrv/pgsql_lease_mgr.cc +++ b/src/lib/dhcpsrv/pgsql_lease_mgr.cc @@ -26,7 +26,8 @@ using namespace std; 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 /// that the order columns appear in statement body must match the order they diff --git a/src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc b/src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc index ba672fe2c4..c708cb86e0 100755 --- a/src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc +++ b/src/lib/dhcpsrv/tests/pgsql_exchange_unittest.cc @@ -6,69 +6,21 @@ #include +#include #include #include #include +#include +#include + 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(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 /// the bind array. TEST(PsqlBindArray, addDataTest) { @@ -138,5 +90,840 @@ TEST(PsqlBindArray, addDataTest) { EXPECT_EQ(expected, b.toText()); } -}; // namespace +/// @brief Defines a pointer to a PgSqlConnection +typedef boost::shared_ptr PgSqlConnectionPtr; +/// @brief Defines a pointer to a PgSqlResult +typedef boost::shared_ptr 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 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 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 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::vectorints; + 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 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 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