From: Thomas Markwalder Date: Fri, 28 Jan 2022 12:45:31 +0000 (-0500) Subject: [#95] Postgresql v4 backend now supports global parameters X-Git-Tag: Kea-2.1.3~36 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=31f764eacc79865e589227cefee40fe3ccd59351;p=thirdparty%2Fkea.git [#95] Postgresql v4 backend now supports global parameters Added support for global paramters and new convenience class, PgSqlResultRowWorker src/hooks/dhcp/pgsql_cb/pgsql_cb_dhcp4.cc PgSqlConfigBackendDHCPv4Impl::getGlobalParameter4() PgSqlConfigBackendDHCPv4Impl::createUpdateGlobalParameter4() - implemented src/hooks/dhcp/pgsql_cb/pgsql_cb_impl.cc PgSqlConfigBackendImpl::getRecentAuditEntries() PgSqlConfigBackendImpl::getServers() - now uses PgSqlResultRowWorker PgSqlConfigBackendImpl::getGlobalParameters() - implemented src/hooks/dhcp/pgsql_cb/tests/pgsql_cb_dhcp4_unittest.cc TEST_F(PgSqlConfigBackendDHCPv4Test, getAndDeleteAllServersTest) TEST_F(PgSqlConfigBackendDHCPv4Test, createUpdateDeleteGlobalParameter4Test) TEST_F(PgSqlConfigBackendDHCPv4Test, globalParameters4WithServerTagsTest) TEST_F(PgSqlConfigBackendDHCPv4Test, getAllGlobalParameters4Test) TEST_F(PgSqlConfigBackendDHCPv4Test, getModifiedGlobalParameters4Test) TEST_F(PgSqlConfigBackendDHCPv4Test, nullKeyErrorTest) - new tests src/lib/dhcpsrv/testutils/generic_cb_dhcp4_unittest.cc GenericConfigBackendDHCPv4Test::nullKeyErrorTest() - only checks exception type, since messages between backends are different src/lib/pgsql/pgsql_exchange.* PgSqlExchange::convertFromBytea() - new function outputs a vector PgSqlExchange::getTripletValue() PgSqlExchange::getTripletValue() - new functions PgSqlResultRowWorker - new convenience class for accessing columns in a result set row src/lib/pgsql/tests/pgsql_basics.cc Added new columns to basics table src/lib/pgsql/tests/pgsql_exchange_unittest.cc TEST_F(PgSqlBasicsTest, tripleTest) TEST_F(PgSqlBasicsTest, resultRowWorker) - new tests --- diff --git a/src/hooks/dhcp/pgsql_cb/pgsql_cb_dhcp4.cc b/src/hooks/dhcp/pgsql_cb/pgsql_cb_dhcp4.cc index 515e7e19e2..f1a8cec7a5 100644 --- a/src/hooks/dhcp/pgsql_cb/pgsql_cb_dhcp4.cc +++ b/src/hooks/dhcp/pgsql_cb/pgsql_cb_dhcp4.cc @@ -196,19 +196,76 @@ public: /// /// @return Pointer to the retrieved value or null if such parameter /// doesn't exist. - StampedValuePtr getGlobalParameter4(const ServerSelector& /* server_selector */, - const std::string& /* name */) { - isc_throw(NotImplemented, NOT_IMPL_STR); + StampedValuePtr getGlobalParameter4(const ServerSelector& server_selector, + const std::string& name) { + StampedValueCollection parameters; + + auto tags = server_selector.getTags(); + for (auto tag : tags) { + PsqlBindArray in_bindings; + in_bindings.addTempString(tag.get()); + in_bindings.add(name); + + getGlobalParameters(GET_GLOBAL_PARAMETER4, in_bindings, parameters); + } + + return (parameters.empty() ? StampedValuePtr() : *parameters.begin()); } /// @brief Sends query to insert or update global parameter. /// /// @param server_selector Server selector. /// @param value StampedValue describing the parameter to create/update. - void createUpdateGlobalParameter4(const db::ServerSelector& /* server_selector */, - const StampedValuePtr& /* value */) { - isc_throw(NotImplemented, NOT_IMPL_STR); - } + void createUpdateGlobalParameter4(const db::ServerSelector& server_selector, + const StampedValuePtr& value) { + if (server_selector.amUnassigned()) { + isc_throw(NotImplemented, "managing configuration for no particular server" + " (unassigned) is unsupported at the moment"); + } + + auto tag = getServerTag(server_selector, "creating or updating global parameter"); + + PsqlBindArray in_bindings; + in_bindings.addTempString(value->getName()); + in_bindings.addTempString(value->getValue()); + in_bindings.add(value->getType()), + in_bindings.addTimestamp(value->getModificationTime()), + in_bindings.addTempString(tag); + in_bindings.addTempString(value->getName()); + + PgSqlTransaction transaction(conn_); + + // Create scoped audit revision. As long as this instance exists + // no new audit revisions are created in any subsequent calls. + ScopedAuditRevision audit_revision(this, + PgSqlConfigBackendDHCPv4Impl::CREATE_AUDIT_REVISION, + server_selector, "global parameter set", + false); + + // Try to update the existing row. + if (updateDeleteQuery(PgSqlConfigBackendDHCPv4Impl::UPDATE_GLOBAL_PARAMETER4, + in_bindings) == 0) { + + // No such parameter found, so let's insert it. We have to adjust the + // bindings collection to match the prepared statement for insert. + in_bindings.popBack(); + in_bindings.popBack(); + + insertQuery(PgSqlConfigBackendDHCPv4Impl::INSERT_GLOBAL_PARAMETER4, + in_bindings); + + // Successfully inserted global parameter. Now, we have to associate it + // with the server tag. + PsqlBindArray attach_bindings; + uint64_t pid = getLastInsertId4("dhcp4_global_parameter", "id"); + attach_bindings.add(pid); // id of newly inserted global. + attach_bindings.add(value->getModificationTime()); + attachElementToServers(PgSqlConfigBackendDHCPv4Impl::INSERT_GLOBAL_PARAMETER4_SERVER, + server_selector, attach_bindings); + } + + transaction.commit(); + } /// @brief Sends query to the database to retrieve multiple subnets. /// diff --git a/src/hooks/dhcp/pgsql_cb/pgsql_cb_impl.cc b/src/hooks/dhcp/pgsql_cb/pgsql_cb_impl.cc index 734204d87d..262d2fd800 100644 --- a/src/hooks/dhcp/pgsql_cb/pgsql_cb_impl.cc +++ b/src/hooks/dhcp/pgsql_cb/pgsql_cb_impl.cc @@ -172,33 +172,28 @@ PgSqlConfigBackendImpl::getRecentAuditEntries(const int index, selectQuery(index, in_bindings, [&audit_entries] (PgSqlResult& r, int row) { // Extract the column values for r[row]. + // Create a worker for the row. + PgSqlResultRowWorker worker(r, row); // Get the object type. Column 0 is the entry ID which // we don't need here. - std::string object_type; - PgSqlExchange::getColumnValue(r, row, 1, object_type); + std::string object_type = worker.getString(1); // Get the object ID. - uint64_t object_id; - PgSqlExchange::getColumnValue(r, row, 2, object_id); + uint64_t object_id = worker.getBigInt(2); // Get the modification type. - uint8_t mod_typ_int; - PgSqlExchange::getColumnValue(r, row, 3, mod_typ_int); AuditEntry::ModificationType mod_type = - static_cast(mod_typ_int); + static_cast(worker.getSmallInt(3)); // Get the modification time. - boost::posix_time::ptime mod_time; - PgSqlExchange::getColumnValue(r, row, 4, mod_time); + boost::posix_time::ptime mod_time = worker.getTimestamp(4); // Get the revision ID. - uint64_t revision_id; - PgSqlExchange::getColumnValue(r, row, 5, revision_id); + uint64_t revision_id = worker.getBigInt(5);; // Get the revision log message. - std::string log_message; - PgSqlExchange::getColumnValue(r, row, 6, log_message); + std::string log_message = worker.getString(6); // Create new audit entry and add it to the collection of received // entries. @@ -256,10 +251,86 @@ PgSqlConfigBackendImpl::getLastInsertId(const int index, const std::string& tabl } void -PgSqlConfigBackendImpl::getGlobalParameters(const int /* index */, - const PsqlBindArray& /* in_bindings */, - StampedValueCollection& /* parameters */) { - isc_throw(NotImplemented, NOT_IMPL_STR); +PgSqlConfigBackendImpl::getGlobalParameters(const int index, + const PsqlBindArray& in_bindings, + StampedValueCollection& parameters) { + // The following parameters from the dhcp[46]_global_parameter table are + // returned per row: + // - id + // - parameter name + // - parameter value + // - parameter type + // - modification timestamp + + StampedValuePtr last_param; + StampedValueCollection local_parameters; + selectQuery(index, in_bindings, + [&local_parameters, &last_param](PgSqlResult& r, int row) { + // Extract the column values for r[row]. + // Create a worker for the row. + PgSqlResultRowWorker worker(r, row); + + // Get parameter ID. + uint64_t id = worker.getBigInt(0); + + // If we're starting or if this is new parameter being processed... + if (!last_param || (last_param->getId() != id)) { + // Create the parameter instance. + + // Get parameter name. + std::string name = worker.getString(1); + if (!name.empty()) { + // Fetch the value. + std::string value = worker.getString(2); + + // Fetch the type. + Element::types ptype = static_cast(worker.getSmallInt(3)); + + // Create the parameter. + last_param = StampedValue::create(name, value, ptype); + + // Set the id. + last_param->setId(id); + + // Get and set the modification time. + boost::posix_time::ptime mod_time = worker.getTimestamp(4); + last_param->setModificationTime(mod_time); + + // server_tag + std::string server_tag_str = worker.getString(5); + last_param->setServerTag(server_tag_str); + + // If we're fetching parameters for a given server (explicit server + // tag is provided), it takes precedence over the same parameter + // specified for all servers. Therefore, we check if the given + // parameter already exists and belongs to 'all'. + ServerTag last_param_server_tag(server_tag_str); + auto& index = local_parameters.get(); + auto existing = index.find(name); + if (existing != index.end()) { + // This parameter was already fetched. Let's check if we should + // replace it or not. + if (!last_param_server_tag.amAll() && (*existing)->hasAllServerTag()) { + // Replace parameter specified for 'all' with the one associated + // with the particular server tag. + local_parameters.replace(existing, last_param); + return; + } + } + + // If there is no such parameter yet or the existing parameter + // belongs to a different server and the inserted parameter is + // not for all servers. + if ((existing == index.end()) || + (!(*existing)->hasServerTag(last_param_server_tag) && + !last_param_server_tag.amAll())) { + local_parameters.insert(last_param); + } + } + } + }); + + parameters.insert(local_parameters.begin(), local_parameters.end()); } OptionDefinitionPtr @@ -485,22 +556,20 @@ PgSqlConfigBackendImpl::getServers(const int index, selectQuery(index, in_bindings, [&servers, &last_server](PgSqlResult& r, int row) { // Extract the column values for r[row]. + // Create a worker for the row. + PgSqlResultRowWorker worker(r, row); // Get the server ID. - uint64_t id; - PgSqlExchange::getColumnValue(r, row, 0, id); + uint64_t id = worker.getBigInt(0); // Get the server tag. - std::string tag; - PgSqlExchange::getColumnValue(r, row, 1, tag); + std::string tag = worker.getString(1); // Get the description. - std::string description; - PgSqlExchange::getColumnValue(r, row, 2, description); + std::string description = worker.getString(2); // Get the modification time. - boost::posix_time::ptime mod_time; - PgSqlExchange::getColumnValue(r, row, 3, mod_time); + boost::posix_time::ptime mod_time = worker.getTimestamp(3); if (!last_server || (last_server->getId() != id)) { // Create the server instance. diff --git a/src/hooks/dhcp/pgsql_cb/tests/pgsql_cb_dhcp4_unittest.cc b/src/hooks/dhcp/pgsql_cb/tests/pgsql_cb_dhcp4_unittest.cc index 5c1d842911..3bfe3ebd24 100644 --- a/src/hooks/dhcp/pgsql_cb/tests/pgsql_cb_dhcp4_unittest.cc +++ b/src/hooks/dhcp/pgsql_cb/tests/pgsql_cb_dhcp4_unittest.cc @@ -133,6 +133,29 @@ TEST_F(PgSqlConfigBackendDHCPv4Test, createUpdateDeleteServer) { createUpdateDeleteServerTest(); } +TEST_F(PgSqlConfigBackendDHCPv4Test, getAndDeleteAllServersTest) { + getAndDeleteAllServersTest(); +} + +TEST_F(PgSqlConfigBackendDHCPv4Test, createUpdateDeleteGlobalParameter4Test) { + createUpdateDeleteGlobalParameter4Test(); +} + +TEST_F(PgSqlConfigBackendDHCPv4Test, globalParameters4WithServerTagsTest) { + globalParameters4WithServerTagsTest(); +} + +TEST_F(PgSqlConfigBackendDHCPv4Test, getAllGlobalParameters4Test) { + getAllGlobalParameters4Test(); +} + +TEST_F(PgSqlConfigBackendDHCPv4Test, getModifiedGlobalParameters4Test) { + getModifiedGlobalParameters4Test(); +} + +TEST_F(PgSqlConfigBackendDHCPv4Test, nullKeyErrorTest) { + nullKeyErrorTest(); +} /// @brief Test fixture for verifying database connection loss-recovery /// behavior. diff --git a/src/lib/dhcpsrv/testutils/generic_cb_dhcp4_unittest.cc b/src/lib/dhcpsrv/testutils/generic_cb_dhcp4_unittest.cc index ca3a8a4bb7..10f59c6ff4 100644 --- a/src/lib/dhcpsrv/testutils/generic_cb_dhcp4_unittest.cc +++ b/src/lib/dhcpsrv/testutils/generic_cb_dhcp4_unittest.cc @@ -1014,18 +1014,8 @@ GenericConfigBackendDHCPv4Test::nullKeyErrorTest() { // Create a global parameter (it should work with any object type). StampedValuePtr global_parameter = StampedValue::create("global", "value"); - // Try to insert it and associate with non-existing server. - std::string msg; - try { - cbptr_->createUpdateGlobalParameter4(ServerSelector::ONE("server1"), - global_parameter); - msg = "got no exception"; - } catch (const NullKeyError& ex) { - msg = ex.what(); - } catch (const std::exception&) { - msg = "got another exception"; - } - EXPECT_EQ("server 'server1' does not exist", msg); + ASSERT_THROW (cbptr_->createUpdateGlobalParameter4(ServerSelector::ONE("server1"), + global_parameter), NullKeyError); } void diff --git a/src/lib/pgsql/pgsql_exchange.cc b/src/lib/pgsql/pgsql_exchange.cc index f2c25b4b65..c20af57a2b 100644 --- a/src/lib/pgsql/pgsql_exchange.cc +++ b/src/lib/pgsql/pgsql_exchange.cc @@ -521,6 +521,70 @@ PgSqlExchange::convertFromBytea(const PgSqlResult& r, const int row, PQfreemem(bytes); } +void +PgSqlExchange::convertFromBytea(const PgSqlResult& r, const int row, const size_t col, + std::vector& value) { + // Returns converted bytes in a dynamically allocated buffer, and + // sets bytes_converted. + size_t bytes_converted = 0; + unsigned char* bytes = PQunescapeBytea((const unsigned char*) + (getRawColumnValue(r, row, col)), + &bytes_converted); + + // Unlikely it couldn't allocate it but you never know. + if (!bytes) { + isc_throw (DbOperationError, "PQunescapeBytea failed for:" + << getColumnLabel(r, col) << " row:" << row); + } + + // Copy from the allocated buffer to caller's buffer the free up + // the allocated buffer. + if (bytes_converted) { + value.assign(bytes, bytes + bytes_converted); + } else { + value.clear(); + } + + // Free the PostgreSQL buffer. + PQfreemem(bytes); +} + +Triplet +PgSqlExchange::getTripletValue(const PgSqlResult& r, const int row, + const size_t col) { + uint32_t col_value; + if (isColumnNull(r, row, col)) { + return (Triplet()); + } + + getColumnValue(r, row, col, col_value); + return (Triplet(col_value)); +} + +Triplet +PgSqlExchange::getTripletValue(const PgSqlResult& r, const int row, + const size_t def_col, const size_t min_col, + const size_t max_col) { + if (isColumnNull(r, row, def_col)) { + return (Triplet()); + } + + uint32_t value; + getColumnValue(r, row, def_col, value); + + uint32_t min_value = value; + if (!isColumnNull(r, row, min_col)) { + getColumnValue(r, row, min_col, min_value); + } + + uint32_t max_value = value; + if (!isColumnNull(r, row, max_col)) { + getColumnValue(r, row, max_col, max_value); + } + + return (Triplet(min_value, value, max_value)); +} + std::string PgSqlExchange::getColumnLabel(const PgSqlResult& r, const size_t column) { return (r.getColumnLabel(column)); @@ -559,5 +623,108 @@ PgSqlExchange::dumpRow(const PgSqlResult& r, int row) { return (stream.str()); } +PgSqlResultRowWorker::PgSqlResultRowWorker(const PgSqlResult& r, const int row) + : r_(r), row_(row) { + // Validate the desired row. + r.rowCheck(row); +} + +bool +PgSqlResultRowWorker::isColumnNull(const size_t col) { + return (PgSqlExchange::isColumnNull(r_, row_, col)); +} + +std::string +PgSqlResultRowWorker::getString(const size_t col) { + std::string tmp; + PgSqlExchange::getColumnValue(r_, row_, col, tmp); + return (tmp); +} + +bool +PgSqlResultRowWorker::getBool(const size_t col) { + bool tmp; + PgSqlExchange::getColumnValue(r_, row_, col, tmp); + return (tmp); +} + +double +PgSqlResultRowWorker::getDouble(const size_t col) { + double tmp; + PgSqlExchange::getColumnValue(r_, row_, col, tmp); + return (tmp); +} + +const char* +PgSqlResultRowWorker::getRawColumnValue(const size_t col) { + return(PgSqlExchange::getRawColumnValue(r_, row_, col)); +} + +uint64_t +PgSqlResultRowWorker::getBigInt(const size_t col) { + uint64_t value; + PgSqlExchange::getColumnValue(r_, row_, col, value); + return (value); +} + +uint32_t +PgSqlResultRowWorker::getInt(const size_t col) { + uint32_t value; + PgSqlExchange::getColumnValue(r_, row_, col, value); + return (value); +} + +uint16_t +PgSqlResultRowWorker::getSmallInt(const size_t col) { + uint16_t value; + PgSqlExchange::getColumnValue(r_, row_, col, value); + return (value); +} + +void +PgSqlResultRowWorker::getBytes(const size_t col, std::vector& value) { + PgSqlExchange::convertFromBytea(r_, row_, col, value); +} + +isc::asiolink::IOAddress +PgSqlResultRowWorker::getInet4(const size_t col) { + return(PgSqlExchange::getInetValue4(r_, row_, col)); +} + +isc::asiolink::IOAddress +PgSqlResultRowWorker::getInet6(const size_t col) { + return(PgSqlExchange::getInetValue6(r_, row_, col)); +} + +boost::posix_time::ptime +PgSqlResultRowWorker::getTimestamp(const size_t col) { + boost::posix_time::ptime value; + getColumnValue(col, value); + return (value); +}; + +data::ElementPtr +PgSqlResultRowWorker::getJSON(const size_t col) { + data::ElementPtr value; + getColumnValue(col, value); + return(value); +} + +isc::util::Triplet +PgSqlResultRowWorker::getTriplet(const size_t col) { + return(PgSqlExchange::getTripletValue(r_, row_, col)); +} + +isc::util::Triplet +PgSqlResultRowWorker::getTriplet(const size_t def_col, const size_t min_col, + const size_t max_col) { + return(PgSqlExchange::getTripletValue(r_, row_, def_col, min_col, max_col)); +} + +std::string +PgSqlResultRowWorker::dumpRow() { + return(PgSqlExchange::dumpRow(r_, row_)); +} + } // end of isc::db namespace } // end of isc namespace diff --git a/src/lib/pgsql/pgsql_exchange.h b/src/lib/pgsql/pgsql_exchange.h index 37c7479044..90999ca88b 100644 --- a/src/lib/pgsql/pgsql_exchange.h +++ b/src/lib/pgsql/pgsql_exchange.h @@ -756,6 +756,59 @@ public: const size_t buffer_size, size_t &bytes_converted); + /// @brief Converts a column in a row in a result set to a binary bytes + /// + /// Method is used to convert columns stored as BYTEA into a vectory of + /// binary bytes, (uint8_t). It uses PQunescapeBytea to do the conversion. + /// + /// @param r the result set containing the query results + /// @param row the row number within the result set + /// @param col the column number within the row + /// @param[out] value vector to receive the converted bytes + /// value + /// + /// @throw DbOperationError if the value cannot be fetched or is + /// invalid. + static void convertFromBytea(const PgSqlResult& r, const int row, + const size_t col, std::vector& value); + + /// @brief Fetches a uint32_t value into a Triplet using a single + /// column value + /// + /// @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 If the column + /// is null, the Triplet is returned as unspecified. + /// @param[out] value Triplet to receive the column value + /// + /// @throw DbOperationError if the value cannot be fetched or is + /// invalid. + static isc::util::Triplet getTripletValue(const PgSqlResult& r, + const int row, + const size_t col); + + /// @brief Fetches a uint32_t value into a Triplet using a three + /// column values: default, minimum, and maximum + /// + /// @param r the result set containing the query results + /// @param row the row number within the result set + /// @param def_col the column number within the row that contains the + /// default value. If this column is null, the Triplet is returned + /// as unspecified. + /// @param min_col the column number within the row that contains the + /// minium value. + /// @param max_col the column number within the row that contains the + /// maximum value. + /// @param[out] value Triplet to receive the column value + /// + /// @throw DbOperationError if the value cannot be fetched or is + /// invalid. + static isc::util::Triplet getTripletValue(const PgSqlResult& r, + const int row, + const size_t def_col, + const size_t min_col, + const size_t max_col); + /// @brief Diagnostic tool which dumps the Result row contents as a string /// /// @param r the result set containing the query results @@ -770,6 +823,164 @@ protected: std::vector columns_; }; +/// @brief Convenience class which facilitates fetching column values +/// from a result set row. +class PgSqlResultRowWorker { +public: + /// @brief Constructor + /// + /// @param r result set containing the fetched rows of data. + /// @param row zero-based index of the desired row, (e.g. + /// 0 .. n - 1 where n = number of rows in r) + /// @throw DbOperationError if row value is invalid. + PgSqlResultRowWorker(const PgSqlResult& r, const int row); + + /// @brief Indicates whether or not the given column value is null. + /// + /// @param col the column number within the row + /// + /// @return true if the value is null, false otherwise. + bool isColumnNull(const size_t col); + + /// @brief Fetches the column value as a string. + /// + /// @param col the column number within the row + /// + /// @return std::string containing the column value. + std::string getString(const size_t col); + + /// @brief Fetches the boolean value at the given column. + /// + /// @param col the column number within the row + /// + /// @return bool containing the column value. + bool getBool(const size_t col); + + /// @brief Fetches the floating point value at the given column. + /// + /// @param col the column number within the row + /// + /// @return double containing the column value. + double getDouble(const size_t col); + + /// @brief Gets a pointer to the raw column value in a result set row + /// + /// Given a column return a const char* pointer to the data value in + /// the result set row. The pointer is valid as long as the underlying + /// result set has not been freed. It may point to text or binary + /// data depending on how query was structured. You should not attempt + /// to free this pointer. + /// + /// @param col the column number within the row + /// + /// @return a const char* pointer to the column's raw data + const char* getRawColumnValue(const size_t col); + + /// @brief Fetches the uint64_t value at the given column. + /// + /// @param col the column number within the row + /// + /// @return uint64_t containing the column value + uint64_t getBigInt(const size_t col); + + /// @brief Fetches the uint32_t value at the given column. + /// + /// @param col the column number within the row + /// + /// @return uint32_t containing the column value + uint32_t getInt(const size_t col); + + /// @brief Fetches the uint16_t value at the given column. + /// + /// @param col the column number within the row + /// + /// @return uint16_t containing the column value + uint16_t getSmallInt(const size_t col); + + /// @brief Fetches binary data at the given column into a vector. + /// + /// @param col the column number within the row + /// @param[out] value vector to receive the fetched data. + void getBytes(const size_t col, std::vector& value); + + /// @brief Fetches the v4 IP address at the given column. + /// + /// This is used to fetch values from inet type columns. + /// @param col the column number within the row + /// + /// @return isc::asiolink::IOAddress containing the IPv4 address. + isc::asiolink::IOAddress getInet4(const size_t col); + + /// @brief Fetches the v6 IP address at the given column. + /// + /// This is used to fetch values from inet type columns. + /// @param col the column number within the row + /// + /// @return isc::asiolink::IOAddress containing the IPv6 address. + isc::asiolink::IOAddress getInet6(const size_t col); + + /// @brief Fetches a text column as the given value type + /// + /// Uses boost::lexicalcast to convert the text column value into + /// a value of type T. + /// + /// @param col the column number within the row + /// @param[out] value parameter to receive the converted value + template + void getColumnValue(const size_t col, T& value) { + PgSqlExchange::getColumnValue(r_, row_, col, value); + } + + /// @brief Fetches a timestamp column as a ptime. + /// + /// @param col the column number within the row + /// @param[out] value ptime parameter to receive the converted timestamp + boost::posix_time::ptime getTimestamp(const size_t col); + + /// @brief Fetches a JSON column as an ElementPtr. + /// + /// @param col the column number within the row + /// @param[out] value ElementPtr parameter to receive the column value + data::ElementPtr getJSON(const size_t col); + + /// @brief Fetches a uint32_t value into a Triplet using a single + /// column value + /// + /// @param col the column number within the row If the column + /// is null, the Triplet is returned as unspecified. + /// @param[out] value Triplet to receive the column value + isc::util::Triplet getTriplet(const size_t col); + + /// @brief Fetches a uint32_t value into a Triplet using a three + /// column values: default, minimum, and maximum + /// + /// @param def_col the column number within the row that contains the + /// default value. If this column is null, the Triplet is returned + /// as unspecified. + /// @param min_col the column number within the row that contains the + /// minium value. + /// @param max_col the column number within the row that contains the + /// maximum value. + /// @param[out] value Triplet to receive the column value + isc::util::Triplet getTriplet(const size_t def_col, + const size_t min_col, + const size_t max_col); + + /// @brief Diagnostic tool which dumps the Result row contents as a string + /// + /// @return A string depiction of the row contents. + std::string dumpRow(); + +private: + /// @brief Result set containing the row. + const PgSqlResult& r_; + /// @brief Index of the desired row. + size_t row_; +}; + +/// @brief Pointer to a result row worker. +typedef boost::shared_ptr PgSqlResultRowWorkerPtr; + } // end of isc::db namespace } // end of isc namespace diff --git a/src/lib/pgsql/tests/pgsql_basics.cc b/src/lib/pgsql/tests/pgsql_basics.cc index c0dcb0d8f7..1c4850adc9 100644 --- a/src/lib/pgsql/tests/pgsql_basics.cc +++ b/src/lib/pgsql/tests/pgsql_basics.cc @@ -42,9 +42,12 @@ PgSqlBasicsTest::PgSqlBasicsTest() : expected_col_names_(NUM_BASIC_COLS) { expected_col_names_[TEXT_COL] = "text_col"; expected_col_names_[TIMESTAMP_COL] = "timestamp_col"; expected_col_names_[VARCHAR_COL] = "varchar_col"; - expected_col_names_[INET_COL] = "inet_col"; + expected_col_names_[INET4_COL] = "inet4_col"; expected_col_names_[FLOAT_COL] = "float_col"; expected_col_names_[JSON_COL] = "json_col"; + expected_col_names_[MIN_INT_COL] = "min_int_col"; + expected_col_names_[MAX_INT_COL] = "max_int_col"; + expected_col_names_[INET6_COL] = "inet6_col"; destroySchema(); createSchema(); @@ -78,9 +81,12 @@ PgSqlBasicsTest::createSchema() { " text_col TEXT, " " timestamp_col TIMESTAMP WITH TIME ZONE, " " varchar_col VARCHAR(255), " - " inet_col INET, " + " inet4_col INET, " " float_col FLOAT, " - " json_col JSON " + " json_col JSON," + " min_int_col INT, " + " max_int_col INT, " + " inet6_col INET " "); "; PgSqlResult r(PQexec(*conn_, sql)); @@ -129,7 +135,8 @@ PgSqlBasicsTest::fetchRows(PgSqlResultPtr& r, int exp_rows, int line) { " id, bool_col, bytea_col, bigint_col, smallint_col, " " int_col, text_col," " extract(epoch from timestamp_col)::bigint as timestamp_col," - " varchar_col, inet_col, float_col, json_col" + " varchar_col, inet4_col, float_col, json_col," + " min_int_col, max_int_col, inet6_col" " FROM basics"; runSql(r, sql, PGRES_TUPLES_OK, line); diff --git a/src/lib/pgsql/tests/pgsql_basics.h b/src/lib/pgsql/tests/pgsql_basics.h index 3366a1604f..02c5184f93 100644 --- a/src/lib/pgsql/tests/pgsql_basics.h +++ b/src/lib/pgsql/tests/pgsql_basics.h @@ -51,9 +51,12 @@ public: TEXT_COL, TIMESTAMP_COL, VARCHAR_COL, - INET_COL, + INET4_COL, FLOAT_COL, JSON_COL, + MIN_INT_COL, + MAX_INT_COL, + INET6_COL, NUM_BASIC_COLS }; diff --git a/src/lib/pgsql/tests/pgsql_exchange_unittest.cc b/src/lib/pgsql/tests/pgsql_exchange_unittest.cc index d8e8eb7fa4..5166c0de51 100644 --- a/src/lib/pgsql/tests/pgsql_exchange_unittest.cc +++ b/src/lib/pgsql/tests/pgsql_exchange_unittest.cc @@ -1093,12 +1093,12 @@ TEST(PsqlBindArray, popBackTest) { /// using INET columns. TEST_F(PgSqlBasicsTest, inetTest4) { // Create a prepared statement for inserting a SMALLINT - const char* st_name = "inet_insert"; + const char* st_name = "inet4_insert"; PgSqlTaggedStatement statement[] = { { 1, { OID_TEXT }, st_name, - "INSERT INTO BASICS (inet_col) values (cast($1 as inet))" }, + "INSERT INTO BASICS (inet4_col) values (cast($1 as inet))" }, { 1, { OID_TEXT }, "check_where", - "select * from BASICS where inet_col = cast($1 as inet)" } + "select * from BASICS where inet4_col = cast($1 as inet)" } }; ASSERT_NO_THROW(conn_->prepareStatement(statement[0])); @@ -1127,10 +1127,10 @@ TEST_F(PgSqlBasicsTest, inetTest4) { asiolink::IOAddress fetched_inet("0.0.0.0"); for ( ; row < inets.size(); ++row ) { // Verify the column is not null. - ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, INET_COL)); + ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, INET4_COL)); // Fetch and verify the column value - ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue4(*r, row, INET_COL)); + ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue4(*r, row, INET4_COL)); EXPECT_EQ(fetched_inet, inets[row]); } @@ -1140,7 +1140,7 @@ TEST_F(PgSqlBasicsTest, inetTest4) { bind_array->addInet4(inets[1]); RUN_PREP(r, statement[1], bind_array, PGRES_TUPLES_OK); ASSERT_EQ(r->getRows(),1); - ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue4(*r, 0, INET_COL)); + ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue4(*r, 0, INET4_COL)); EXPECT_EQ(fetched_inet, inets[1]); // Clean out the table @@ -1155,17 +1155,17 @@ TEST_F(PgSqlBasicsTest, inetTest4) { FETCH_ROWS(r, 1); // Verify the column is null. - ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, INET_COL)); + ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, INET4_COL)); } /// @brief Verify that we can read and write IPv6 addresses /// using INET columns. TEST_F(PgSqlBasicsTest, inetTest6) { // Create a prepared statement for inserting a SMALLINT - const char* st_name = "inet_insert"; + const char* st_name = "inet6_insert"; PgSqlTaggedStatement statement[] = { { 1, { OID_TEXT }, st_name, - "INSERT INTO BASICS (inet_col) values (cast($1 as inet))" } + "INSERT INTO BASICS (inet6_col) values (cast($1 as inet))" } }; ASSERT_NO_THROW(conn_->prepareStatement(statement[0])); @@ -1191,11 +1191,11 @@ TEST_F(PgSqlBasicsTest, inetTest6) { int row = 0; for ( ; row < inets.size(); ++row ) { // Verify the column is not null. - ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, INET_COL)); + ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, INET6_COL)); // Fetch and verify the column value asiolink::IOAddress fetched_inet("::"); - ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue6(*r, row, INET_COL)); + ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue6(*r, row, INET6_COL)); EXPECT_EQ(fetched_inet, inets[row]); } @@ -1211,7 +1211,7 @@ TEST_F(PgSqlBasicsTest, inetTest6) { FETCH_ROWS(r, 1); // Verify the column is null. - ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, INET_COL)); + ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, INET6_COL)); } /// @brief Verify that we can read and write floats @@ -1324,4 +1324,207 @@ TEST_F(PgSqlBasicsTest, jsonTest) { ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, JSON_COL)); } +/// @brief Verify that we can read and write integer Triplets. +TEST_F(PgSqlBasicsTest, tripleTest) { + // Create a prepared statement for inserting a SMALLINT + const char* st_name = "triplets_insert"; + PgSqlTaggedStatement statement[] = { + { 3, { OID_INT4, OID_INT4, OID_INT4 }, st_name, + "INSERT INTO BASICS (int_col, min_int_col, max_int_col) values ($1, $2, $3)" } + }; + + ASSERT_NO_THROW_LOG(conn_->prepareStatement(statement[0])); + + // Build our reference list of reference values + std::vector> triplets; + triplets.push_back(Triplet()); // def column is null + triplets.push_back(Triplet(10)); // only default column + triplets.push_back(Triplet(5,10,15)); // all three columns + triplets.push_back(Triplet(10,10,15)); // min column is null + triplets.push_back(Triplet(5,10,10)); // max column is null + + // Insert a row for each reference value + PsqlBindArrayPtr bind_array; + PgSqlResultPtr r; + for (auto triplet : triplets) { + bind_array.reset(new PsqlBindArray()); + bind_array->add(triplet); + bind_array->addMin(triplet); + bind_array->addMax(triplet); + RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK); + } + + // Fetch the newly inserted rows. + FETCH_ROWS(r, triplets.size()); + + // Iterate over the rows, verifying each value against its reference + int row = 0; + for (auto expected : triplets) { + Triplet fetched; + // First we test making a triplet only with default value column. + ASSERT_NO_THROW_LOG(fetched = PgSqlExchange::getTripletValue(*r, row, INT_COL)); + if (expected.unspecified()) { + EXPECT_TRUE(fetched.unspecified()); + } else { + EXPECT_FALSE(fetched.unspecified()); + EXPECT_EQ(fetched.get(), expected.get()); + } + + // Now test making a triplet with all three columns. + ASSERT_NO_THROW_LOG( + fetched = PgSqlExchange::getTripletValue(*r, row, + INT_COL, MIN_INT_COL, MAX_INT_COL)); + if (expected.unspecified()) { + EXPECT_TRUE(fetched.unspecified()); + } else { + EXPECT_FALSE(fetched.unspecified()); + EXPECT_EQ(fetched.get(), expected.get()); + EXPECT_EQ(fetched.getMin(), expected.getMin()); + EXPECT_EQ(fetched.getMax(), expected.getMax()); + } + + ++row; + } + + // Clean out the table + WIPE_ROWS(r); +} + +/// @brief Verify PgResultRowWorker operations. +TEST_F(PgSqlBasicsTest, resultRowWorker) { + // Create a prepared statement for inserting a SMALLINT + const char* st_name = "row_insert"; + PgSqlTaggedStatement statement[] = { + { 14, + { + OID_BOOL, + OID_BYTEA, + OID_INT8, + OID_INT2, + OID_INT4, + OID_TEXT, + OID_TIMESTAMP, + OID_TEXT, + OID_TEXT, + OID_TEXT, + OID_TEXT, + OID_INT4, + OID_INT4, + }, st_name, + "INSERT INTO BASICS (" + " bool_col, " + " bytea_col, " + " bigint_col, " + " smallint_col, " + " int_col, " + " text_col, " + " timestamp_col, " + " varchar_col, " + " inet4_col, " + " float_col, " + " json_col, " + " min_int_col, " + " max_int_col, " + " inet6_col) " + " VALUES ( $1, $2, $3, $4, $5, $6, $7, $8, cast($9 as inet), " + " cast($10 as float), cast($11 as json), $12, $13, $14)" + } + }; + + ASSERT_NO_THROW_LOG(conn_->prepareStatement(statement[0])); + + // Create a bind array of input values. + PsqlBindArrayPtr b(new PsqlBindArray()); + + bool exp_bool(true); + b->add(exp_bool); + + std::vector exp_bytes({ 0x01, 0x02, 0x03, 0x04}); + b->add(exp_bytes); + + uint64_t exp_bigint = 9876; + b->add(exp_bigint); + + uint16_t exp_smallint = 12; + b->add(exp_smallint); + + uint32_t exp_int = 345; + b->add(exp_int); + + std::string exp_text = "just some string"; + b->add(exp_text); + + time_duration duration = hours(7) + minutes(45) + seconds(9); + ptime exp_timestamp(date(2021, Jul, 18), duration); + b->addTimestamp(exp_timestamp); + + std::string exp_varchar = "really just a string"; + b->add(exp_varchar); + + asiolink::IOAddress exp_inet4("192.168.1.35"); + b->addInet4(exp_inet4); + + double exp_double(2.5); + b->add(exp_double); + + ElementPtr exp_elems = Element::fromJSON("{ \"foo\": \"bar\" }"); + b->add(exp_elems); + + uint32_t exp_min = 100; + b->add(exp_min); + + uint32_t exp_max = 500; + b->add(exp_max); + + asiolink::IOAddress exp_inet6("3001::77"); + b->addInet6(exp_inet6); + + PgSqlResultPtr r; + RUN_PREP(r, statement[0], b, PGRES_COMMAND_OK); + + // Fetch the newly inserted row. + FETCH_ROWS(r, 1); + + // Create a row worker. + PgSqlResultRowWorkerPtr worker; + + // Creating the row worker for the first (and only) row should succeed. + ASSERT_NO_THROW_LOG(worker.reset(new PgSqlResultRowWorker(*r, 0))); + + // Now let's test all the getters. + EXPECT_EQ(exp_bool, worker->getBool(BOOL_COL)); + + std::vector fetched_bytes; + ASSERT_NO_THROW_LOG(worker->getBytes(BYTEA_COL, fetched_bytes)); + EXPECT_EQ(exp_bytes, fetched_bytes); + + EXPECT_EQ(exp_bigint, worker->getBigInt(BIGINT_COL)); + EXPECT_EQ(exp_smallint, worker->getSmallInt(SMALLINT_COL)); + EXPECT_EQ(exp_int, worker->getInt(INT_COL)); + EXPECT_EQ(exp_text, worker->getString(TEXT_COL)); + EXPECT_EQ(exp_timestamp, worker->getTimestamp(TIMESTAMP_COL)); + EXPECT_EQ(exp_varchar, worker->getString(VARCHAR_COL)); + EXPECT_EQ(exp_inet4, worker->getInet4(INET4_COL)); + EXPECT_EQ(exp_double, worker->getDouble(FLOAT_COL)); + EXPECT_EQ(*exp_elems, *(worker->getJSON(JSON_COL))); + EXPECT_EQ(exp_min, worker->getInt(MIN_INT_COL)); + EXPECT_EQ(exp_max, worker->getInt(MAX_INT_COL)); + EXPECT_EQ(exp_inet6, worker->getInet6(INET6_COL)); + + // Get a triplet using int_col as the sole value. + Tripletfetched_triplet = worker->getTriplet(5); + EXPECT_EQ(exp_int, fetched_triplet.get()); + + // Get a triplet using int_col, min_col, and max_col values. + fetched_triplet = worker->getTriplet(INT_COL, MIN_INT_COL, MAX_INT_COL); + EXPECT_EQ(exp_int, fetched_triplet.get()); + EXPECT_EQ(exp_min, fetched_triplet.getMin()); + EXPECT_EQ(exp_max, fetched_triplet.getMax()); + + // Attempting to access an invalid row should throw. + ASSERT_THROW_MSG(worker.reset(new PgSqlResultRowWorker(*r, 1)), + DbOperationError, "row: 1, out of range: 0..1"); +} + + }; // namespace