From: Marcin Siodelski Date: Tue, 18 Sep 2018 06:36:53 +0000 (+0200) Subject: [#93,!35] Extended MySqlConnection with generic query functions. X-Git-Tag: 5-netconf-extend-syntax_base~35 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=479791d759ebab726b0053e3c3394889b0f99416;p=thirdparty%2Fkea.git [#93,!35] Extended MySqlConnection with generic query functions. --- diff --git a/configure.ac b/configure.ac index e87315f4c1..46e2fc941c 100644 --- a/configure.ac +++ b/configure.ac @@ -1529,6 +1529,8 @@ AC_CONFIG_FILES([Makefile src/hooks/dhcp/high_availability/tests/Makefile src/hooks/dhcp/lease_cmds/Makefile src/hooks/dhcp/lease_cmds/tests/Makefile + src/hooks/dhcp/mysql_cb/Makefile + src/hooks/dhcp/mysql_cb/tests/Makefile src/hooks/dhcp/user_chk/Makefile src/hooks/dhcp/user_chk/tests/Makefile src/hooks/dhcp/user_chk/tests/test_data_files_config.h @@ -1593,6 +1595,7 @@ AC_CONFIG_FILES([Makefile src/lib/log/tests/tempdir.h src/lib/mysql/Makefile src/lib/mysql/testutils/Makefile + src/lib/mysql/tests/Makefile src/lib/pgsql/Makefile src/lib/pgsql/tests/Makefile src/lib/pgsql/testutils/Makefile diff --git a/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc b/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc index faa84244f4..da0acce5cf 100644 --- a/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc +++ b/src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc @@ -17,128 +17,6 @@ using namespace isc::db; namespace { -class Binding; - -typedef boost::shared_ptr BindingPtr; - -class Binding { -public: - - enum_field_types getType() const { - return (bind_.buffer_type); - } - - MYSQL_BIND& getMySqlBinding() { - return (bind_); - } - - void setBufferValue(const std::string& value) { - buffer_.assign(value.begin(), value.end()); - bind_.buffer = &buffer_[0]; - bind_.buffer_length = value.size(); - } - - template - T getValue() const { - const T* value = reinterpret_cast(&buffer_[0]); - return (*value); - } - - bool amNull() const { - return (null_value_ == MLM_TRUE); - } - - static BindingPtr createString(const unsigned long length = 512) { - BindingPtr binding(new Binding(MYSQL_TYPE_STRING)); - binding->setBufferLength(length); - return (binding); - } - - static BindingPtr createString(const std::string& value) { - BindingPtr binding(new Binding(MYSQL_TYPE_STRING)); - binding->setBufferValue(value); - return (binding); - } - - static BindingPtr createTimestamp() { - BindingPtr binding(new Binding(MYSQL_TYPE_TIMESTAMP)); - binding->setBufferLength(sizeof(MYSQL_TIME)); - return (binding); - } - -private: - - Binding(enum_field_types buffer_type) - : buffer_(), length_(0), null_value_(MLM_FALSE) { - bind_.buffer_type = buffer_type; - bind_.length = &length_; - bind_.is_null = &null_value_; - } - - void setBufferLength(const unsigned long length) { - length_ = length; - buffer_.resize(length_); - bind_.buffer = &buffer_[0]; - bind_.buffer_length = length_; - } - - std::vector buffer_; - - unsigned long length_; - - my_bool null_value_; - - MYSQL_BIND bind_; -}; - -typedef std::vector BindingCollection; - -class DatabaseExchange { -public: - - typedef std::function ConsumeResultFun; - - void selectQuery(MYSQL_STMT* statement, - const BindingCollection& in_bindings, - BindingCollection& out_bindings, - ConsumeResultFun process_result) { - std::vector in_bind_vec; - for (BindingPtr in_binding : in_bindings) { - in_bind_vec.push_back(in_binding->getMySqlBinding()); - } - - int status = 0; - - if (!in_bind_vec.empty()) { - status = mysql_stmt_bind_param(statement, &in_bind_vec[0]); - } - - std::vector out_bind_vec; - for (BindingPtr out_binding : out_bindings) { - out_bind_vec.push_back(out_binding->getMySqlBinding()); - } - - if (!out_bind_vec.empty()) { - status = mysql_stmt_bind_result(statement, &out_bind_vec[0]); - } - - status = mysql_stmt_execute(statement); - - status = mysql_stmt_store_result(statement); - - MySqlFreeResult fetch_release(statement); - while ((status = mysql_stmt_fetch(statement)) == - MLM_MYSQL_FETCH_SUCCESS) { - try { - process_result(); - } catch (...) { - throw; - } - } - } - -}; - } namespace isc { @@ -252,10 +130,9 @@ MySqlConfigBackendDHCPv4::getSubnet4(const ServerSelector& selector, BindingCollection out_bindings; out_bindings.push_back(Binding::createString()); - DatabaseExchange xchg; - xchg.selectQuery(impl_->conn_.statements_[MySqlConfigBackendDHCPv4Impl::GET_SUBNET4_ID], - in_bindings, out_bindings, - [&out_bindings]() { + impl_->conn_.selectQuery(MySqlConfigBackendDHCPv4Impl::GET_SUBNET4_ID, + in_bindings, out_bindings, + [&out_bindings]() { uint32_t hostname = out_bindings[0]->getValue(); }); diff --git a/src/lib/mysql/Makefile.am b/src/lib/mysql/Makefile.am index 8852b7e813..ce76c0cb30 100644 --- a/src/lib/mysql/Makefile.am +++ b/src/lib/mysql/Makefile.am @@ -1,4 +1,4 @@ -SUBDIRS = . testutils +SUBDIRS = . testutils tests AM_CPPFLAGS = -I$(top_srcdir)/src/lib -I$(top_builddir)/src/lib AM_CPPFLAGS += $(BOOST_INCLUDES) $(MYSQL_CPPFLAGS) @@ -9,6 +9,8 @@ CLEANFILES = *.gcno *.gcda lib_LTLIBRARIES = libkea-mysql.la libkea_mysql_la_SOURCES = mysql_connection.cc mysql_connection.h +libkea_mysql_la_SOURCES += mysql_binding.cc mysql_binding.h +libkea_mysql_la_SOURCES += mysql_constants.h libkea_mysql_la_LIBADD = $(top_builddir)/src/lib/database/libkea-database.la libkea_mysql_la_LIBADD += $(top_builddir)/src/lib/cc/libkea-cc.la diff --git a/src/lib/mysql/mysql_binding.cc b/src/lib/mysql/mysql_binding.cc new file mode 100644 index 0000000000..ad07c4a97a --- /dev/null +++ b/src/lib/mysql/mysql_binding.cc @@ -0,0 +1,216 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include + +namespace isc { +namespace db { + +std::string +MySqlBinding::getString() const { + // Make sure the binding type is text. + validateAccess(); + if (length_ == 0) { + return (std::string()); + } + return (std::string(buffer_.begin(), buffer_.begin() + length_)); +} + +std::vector +MySqlBinding::getBlob() const { + // Make sure the binding type is blob. + validateAccess >(); + if (length_ == 0) { + return (std::vector()); + } + return (std::vector(buffer_.begin(), buffer_.begin() + length_)); +} + +boost::posix_time::ptime +MySqlBinding::getTimestamp() const { + // Make sure the binding type is timestamp. + validateAccess(); + // Copy the buffer contents into native timestamp structure and + // then convert it to posix time. + const MYSQL_TIME* database_time = reinterpret_cast(&buffer_[0]); + return (convertFromDatabaseTime(*database_time)); +} + +MySqlBindingPtr +MySqlBinding::createString(const unsigned long length) { + MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits::column_type, + length)); + return (binding); +} + +MySqlBindingPtr +MySqlBinding::createString(const std::string& value) { + MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits::column_type, + value.size())); + binding->setBufferValue(value.begin(), value.end()); + return (binding); +} + +MySqlBindingPtr +MySqlBinding::createBlob(const unsigned long length) { + MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits >::column_type, + length)); + return (binding); +} + +MySqlBindingPtr +MySqlBinding::createTimestamp(const boost::posix_time::ptime& timestamp) { + MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits::column_type, + MySqlBindingTraits::length)); + binding->setTimestampValue(timestamp); + return (binding); +} + +MySqlBindingPtr +MySqlBinding::createTimestamp() { + MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits::column_type, + MySqlBindingTraits::length)); + return (binding); +} + +MySqlBindingPtr +MySqlBinding::createNull() { + MySqlBindingPtr binding(new MySqlBinding(MYSQL_TYPE_NULL, 0)); + return (binding); +} + +void +MySqlBinding::convertToDatabaseTime(const time_t input_time, + MYSQL_TIME& output_time) { + + // Convert to broken-out time + struct tm time_tm; + (void) localtime_r(&input_time, &time_tm); + + // Place in output expire structure. + output_time.year = time_tm.tm_year + 1900; + output_time.month = time_tm.tm_mon + 1; // Note different base + output_time.day = time_tm.tm_mday; + output_time.hour = time_tm.tm_hour; + output_time.minute = time_tm.tm_min; + output_time.second = time_tm.tm_sec; + output_time.second_part = 0; // No fractional seconds + output_time.neg = my_bool(0); // Not negative +} + +void +MySqlBinding::convertToDatabaseTime(const time_t cltt, + const uint32_t valid_lifetime, + MYSQL_TIME& expire) { + + // Calculate expiry time. Store it in the 64-bit value so as we can detect + // overflows. + int64_t expire_time_64 = static_cast(cltt) + + static_cast(valid_lifetime); + + // Even on 64-bit systems MySQL doesn't seem to accept the timestamps + // beyond the max value of int32_t. + if (expire_time_64 > DatabaseConnection::MAX_DB_TIME) { + isc_throw(BadValue, "Time value is too large: " << expire_time_64); + } + + const time_t expire_time = static_cast(expire_time_64); + + // Convert to broken-out time + struct tm expire_tm; + (void) localtime_r(&expire_time, &expire_tm); + + // Place in output expire structure. + expire.year = expire_tm.tm_year + 1900; + expire.month = expire_tm.tm_mon + 1; // Note different base + expire.day = expire_tm.tm_mday; + expire.hour = expire_tm.tm_hour; + expire.minute = expire_tm.tm_min; + expire.second = expire_tm.tm_sec; + expire.second_part = 0; // No fractional seconds + expire.neg = my_bool(0); // Not negative +} + +void +MySqlBinding::convertFromDatabaseTime(const MYSQL_TIME& expire, + uint32_t valid_lifetime, + time_t& cltt) { + // Copy across fields from MYSQL_TIME structure. + struct tm expire_tm; + memset(&expire_tm, 0, sizeof(expire_tm)); + + expire_tm.tm_year = expire.year - 1900; + expire_tm.tm_mon = expire.month - 1; + expire_tm.tm_mday = expire.day; + expire_tm.tm_hour = expire.hour; + expire_tm.tm_min = expire.minute; + expire_tm.tm_sec = expire.second; + expire_tm.tm_isdst = -1; // Let the system work out about DST + + // Convert to local time + cltt = mktime(&expire_tm) - valid_lifetime; +} + +boost::posix_time::ptime +MySqlBinding::convertFromDatabaseTime(const MYSQL_TIME& database_time) { + // Copy across fields from MYSQL_TIME structure. + struct tm converted_tm; + memset(&converted_tm, 0, sizeof(converted_tm)); + + converted_tm.tm_year = database_time.year - 1900; + converted_tm.tm_mon = database_time.month - 1; + converted_tm.tm_mday = database_time.day; + converted_tm.tm_hour = database_time.hour; + converted_tm.tm_min = database_time.minute; + converted_tm.tm_sec = database_time.second; + converted_tm.tm_isdst = -1; // Let the system work out about DST + + // Convert to local time + return (boost::posix_time::ptime_from_tm(converted_tm)); +} + +MySqlBinding::MySqlBinding(enum_field_types buffer_type, + const size_t length) + : buffer_(length), length_(length), + null_value_(buffer_type == MYSQL_TYPE_NULL) { + memset(&bind_, 0, sizeof(MYSQL_BIND)); + bind_.buffer_type = buffer_type; + + if (buffer_type != MYSQL_TYPE_NULL) { + bind_.buffer = &buffer_[0]; + bind_.buffer_length = length_; + bind_.length = &length_; + bind_.is_null = &null_value_; + } +} + +void +MySqlBinding::setBufferLength(const unsigned long length) { + length_ = length; + buffer_.resize(length_); + bind_.buffer = &buffer_[0]; + bind_.buffer_length = length_; +} + +void +MySqlBinding::setTimestampValue(const boost::posix_time::ptime& timestamp) { + // Convert timestamp to tm structure. + tm td_tm = to_tm(timestamp); + // Convert tm value to time_t. + time_t tt = mktime(&td_tm); + // Convert time_t to database time. + MYSQL_TIME database_time; + convertToDatabaseTime(tt, database_time); + // Copy database time into the buffer. + memcpy(static_cast(&buffer_[0]), reinterpret_cast(&database_time), + sizeof(MYSQL_TIME)); + bind_.buffer = &buffer_[0]; +} + +} // end of namespace isc::db +} // end of namespace isc diff --git a/src/lib/mysql/mysql_binding.h b/src/lib/mysql/mysql_binding.h new file mode 100644 index 0000000000..450d63ddd3 --- /dev/null +++ b/src/lib/mysql/mysql_binding.h @@ -0,0 +1,479 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef MYSQL_BINDING_H +#define MYSQL_BINDING_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace isc { +namespace db { + +/// @brief Trait class for column types supported in MySQL. +/// +/// This class is used to map C++ types to MySQL column types +/// defined in MySQL C API and their sizes. Specializations of +/// this class provide such mapping. The default is a BLOB type +/// which can be used for various input types. +template +struct MySqlBindingTraits { + /// @brief Column type represented in MySQL C API. + static const enum_field_types column_type = MYSQL_TYPE_BLOB; + /// @brief Length of data in this column. + /// + /// The value of 0 is used for variable size columns. + static const size_t length = 0; + /// @brief Boolean value indicating if the numeric value is + /// unsigned. + static const bool am_unsigned = false; +}; + +/// @brief Specialization for MySQL TEXT type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_STRING; + static const size_t length = 0; + static const bool am_unsigned = false; +}; + +/// @brief Specialization for MySQL TIMESTAMP type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_TIMESTAMP; + static const size_t length = sizeof(MYSQL_TIME); + static const bool am_unsignged = false; +}; + +/// @brief Specialization for MySQL TINYINT type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_TINY; + static const size_t length = 1; + static const bool am_unsigned = false; +}; + +/// @brief Specialization for MySQL TINYINT UNSIGNED type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_TINY; + static const size_t length = 1; + static const bool am_unsigned = true; +}; + +/// @brief Speclialization for MySQL SMALLINT type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_SHORT; + static const size_t length = 2; + static const bool am_unsigned = false; +}; + +/// @brief Specialization for MySQL SMALLINT UNSIGNED type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_SHORT; + static const size_t length = 2; + static const bool am_unsigned = true; +}; + +/// @brief Specialization for MySQL INT type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_LONG; + static const size_t length = 4; + static const bool am_unsigned = false; +}; + +/// @brief Specialization for MySQL INT UNSIGNED type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_LONG; + static const size_t length = 4; + static const bool am_unsigned = true; +}; + +/// @brief Specialization for MySQL BIGINT type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_LONGLONG; + static const size_t length = 8; + static const bool am_unsigned = false; +}; + +/// @brief Specialization for MySQL BIGINT UNSIGNED type. +template<> +struct MySqlBindingTraits { + static const enum_field_types column_type = MYSQL_TYPE_LONGLONG; + static const size_t length = 8; + static const bool am_unsigned = true; +}; + +/// @brief Forward declaration of @c MySqlBinding class. +class MySqlBinding; + +/// @brief Shared pointer to the @c Binding class. +typedef boost::shared_ptr MySqlBindingPtr; + +/// @brief MySQL binding used in prepared statements. +/// +/// Kea uses prepared statements to execute queries in a database. +/// Prepared statements include placeholders for the input parameters. +/// These parameters are passed to the prepared statements via a +/// collection of @c MYSQL_BIND structures. The same structures are +/// used to receive the values from the database as a result of +/// SELECT statements. +/// +/// The @c MYSQL_BIND structure contains information about the +/// data type and length. It also contains pointer to the buffer +/// actually holding the data to be passed to the database, a +/// flag indicating if the value is null etc. +/// +/// The @c MySqlBinding is a C++ wrapper around this structure which +/// is meant to ease management of the MySQL bindings. The major +/// benefit is that the @c MySqlBinding class owns the buffer, +/// holding the data as well as other variables which are assigned +/// to the @c MYSQL_BIND structure. It also automatically detects +/// the appropriate @c enum_field_types value based on the C++ +/// type used in the binding. +class MySqlBinding { +public: + + /// @brief Returns MySQL column type for the binding. + /// + /// @return column type, e.g. MYSQL_TYPE_STRING. + enum_field_types getType() const { + return (bind_.buffer_type); + } + + /// @brief Returns reference to the native binding. + /// + /// The returned reference is only valid for the lifetime of the + /// object. Make sure that the object is not destroyed as long + /// as the binding is required. In particular, do not destroy this + /// object before database query is complete. + /// + /// @return Reference to native MySQL binding. + MYSQL_BIND& getMySqlBinding() { + return (bind_); + } + + /// @brief Returns value held in the binding as string. + /// + /// Call @c MySqlBinding::amNull to verify that the value is not + /// null prior to calling this method. + /// + /// @throw InvalidOperation if the value is NULL or the binding + /// type is not @c MYSQL_TYPE_STRING. + /// + /// @return String value. + std::string getString() const; + + /// @brief Returns value held in the binding as blob. + /// + /// Call @c MySqlBinding::amNull to verify that the value is not + /// null prior to calling this method. + /// + /// @throw InvalidOperation if the value is NULL or the binding + /// type is not @c MYSQL_TYPE_BLOB. + /// + /// @return Blob in a vactor. + std::vector getBlob() const; + + /// @brief Returns numeric value held in the binding. + /// + /// Call @c MySqlBinding::amNull to verify that the value is not + /// null prior to calling this method. + /// + /// @tparam Numeric type corresponding to the binding type, e.g. + /// @c uint8_t, @c uint16_t etc. + /// + /// @throw InvalidOperation if the value is NULL or the binding + /// type does not match the template parameter. + /// + /// @return Numeric value of a specified type. + template + T getInteger() const { + // Make sure that the binding type is numeric. + validateAccess(); + + // Convert the buffer to a numeric type and then return a copy. + const T* value = reinterpret_cast(&buffer_[0]); + return (*value); + } + + /// @brief Returns timestamp value held in the binding. + /// + /// Call @c MySqlBinding::amNull to verify that the value is not + /// null prior to calling this method. + /// + /// @throw InvalidOperation if the value is NULL or the binding + /// type is not @c MYSQL_TYPE_TIMESTAMP. + /// + /// @return Timestamp converted to posix time. + boost::posix_time::ptime getTimestamp() const; + + /// @brief Checks if the bound value is NULL. + /// + /// @return true if the value in the binding is NULL, false otherwise. + bool amNull() const { + return (null_value_ == MLM_TRUE); + } + + /// @brief Creates binding of text type for receiving data. + /// + /// @param length Length of the buffer into which received data will + /// be stored. + /// + /// @return Pointer to the created binding. + static MySqlBindingPtr createString(const unsigned long length); + + /// @brief Creates binding of text type for sending data. + /// + /// @param value String value to be sent to the database. + /// + /// @return Pointer to the created binding. + static MySqlBindingPtr createString(const std::string& value); + + /// @brief Creates binding of blob type for receiving data. + /// + /// @param length Length of the buffer into which received data will + /// be stored. + /// + /// @return Pointer to the created binding. + static MySqlBindingPtr createBlob(const unsigned long length); + + /// @brief Creates binding of blob type for sending data. + /// + /// @tparam Iterator Type of the iterator. + /// + /// @param begin Iterator pointing to the beginning of the input + /// buffer holding the data to be sent to the database. + /// @param end Iterator pointing to the end of the input buffer + /// holding the data to be sent to the database. + /// + /// @return Pointer to the created binding. + template + static MySqlBindingPtr createBlob(Iterator begin, Iterator end) { + MySqlBindingPtr binding(new MySqlBinding(MYSQL_TYPE_BLOB, + std::distance(begin, end))); + binding->setBufferValue(begin, end); + return (binding); + } + + /// @brief Creates binding of numeric type for receiving data. + /// + /// @tparam Numeric type corresponding to the binding type, e.g. + /// @c uint8_t, @c uint16_t etc. + /// + /// @return Pointer to the created binding. + template + static MySqlBindingPtr createInteger() { + MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits::column_type, + MySqlBindingTraits::length)); + binding->setValue(0); + return (binding); + } + + /// @brief Creates binding of numeric type for sending data. + /// + /// @tparam Numeric type corresponding to the binding type, e.g. + /// @c uint8_t, @c uint16_t etc. + /// + /// @param value Numeric value to be sent to the database. + /// + /// @return Pointer to the created binding. + template + static MySqlBindingPtr createInteger(T value) { + MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits::column_type, + MySqlBindingTraits::length)); + binding->setValue(value); + return (binding); + } + + /// @brief Creates binding of timestamp type for receiving data. + /// + /// @return Pointer to the created binding. + static MySqlBindingPtr createTimestamp(); + + /// @brief Creates binding of timestamp type for sending data. + /// + /// @param timestamp Timestamp value to be sent to the database. + /// + /// @return Pointer to the created binding. + static MySqlBindingPtr createTimestamp(const boost::posix_time::ptime& timestamp); + + /// @brief Creates binding encapsulating a NULL value. + /// + /// This method is used to create a binding encapsulating a NULL + /// value, which can be used to assign NULL to any type of column. + /// + /// @return Pointer to the created binding. + static MySqlBindingPtr createNull(); + + /// @brief Converts time_t value to database time. + /// + /// @param input_time A time_t value representing time. + /// @param output_time Reference to MYSQL_TIME object where converted time + /// will be put. + static void convertToDatabaseTime(const time_t input_time, + MYSQL_TIME& output_time); + + /// @brief Converts Lease Time to Database Times + /// + /// Within the DHCP servers, times are stored as client last transmit time + /// and valid lifetime. In the database, the information is stored as + /// valid lifetime and "expire" (time of expiry of the lease). They are + /// related by the equation: + /// + /// - expire = client last transmit time + valid lifetime + /// + /// This method converts from the times in the lease object into times + /// able to be added to the database. + /// + /// @param cltt Client last transmit time + /// @param valid_lifetime Valid lifetime + /// @param expire Reference to MYSQL_TIME object where the expiry time of + /// the DHCP lease will be put. + /// + /// @throw isc::BadValue if the sum of the calculated expiration time is + /// greater than the value of @c LeaseMgr::MAX_DB_TIME. + static void convertToDatabaseTime(const time_t cltt, + const uint32_t valid_lifetime, + MYSQL_TIME& expire); + + /// @brief Converts Database Time to Lease Times + /// + /// Within the database, time is stored as "expire" (time of expiry of the + /// lease) and valid lifetime. In the DHCP server, the information is + /// stored client last transmit time and valid lifetime. These are related + /// by the equation: + /// + /// - client last transmit time = expire - valid_lifetime + /// + /// This method converts from the times in the database into times + /// able to be inserted into the lease object. + /// + /// @param expire Reference to MYSQL_TIME object from where the expiry + /// time of the lease is taken. + /// @param valid_lifetime lifetime of the lease. + /// @param cltt Reference to location where client last transmit time + /// is put. + static void convertFromDatabaseTime(const MYSQL_TIME& expire, + uint32_t valid_lifetime, + time_t& cltt); + + /// @brief Converts database time to posix time. + /// + /// @param database_time Reference to MYSQL_TIME object where database + /// time is stored. + /// + /// @return Database time converted to posix time. + static boost::posix_time::ptime + convertFromDatabaseTime(const MYSQL_TIME& database_time); + +private: + + /// @brief Private constructor. + /// + /// This constructor is private because MySQL bindings should only be + /// created using static factory functions. + /// + /// @param buffer_type MySQL buffer type as defined in MySQL C API. + /// @param length Buffer length. + MySqlBinding(enum_field_types buffer_type, const size_t length); + + /// @brief Assigns new value to a buffer. + /// + /// @tparam Iterator Type of the iterators marking beginning and end + /// of the range to be assigned to the buffer. + /// + /// @param begin Iterator pointing to the beginning of the assigned + /// range. + /// @param end Iterator pointing to the end of the assigned range. + template + void setBufferValue(Iterator begin, Iterator end) { + buffer_.assign(begin, end); + bind_.buffer = &buffer_[0]; + bind_.buffer_length = std::distance(begin, end); + } + + /// @brief Resizes the buffer and assigns new length to the binding. + /// + /// @param length New buffer length to be used. + void setBufferLength(const unsigned long length); + + /// @brief Copies numeric value to a buffer and modifies "unsigned" flag + /// accoriding to the numeric type used. + /// + /// @tparam T Type of the numeric value. + /// + /// @param value Value to be copied to the buffer. + template + void setValue(T value) { + memcpy(static_cast(&buffer_[0]), reinterpret_cast(&value), + sizeof(value)); + bind_.buffer = &buffer_[0]; + bind_.is_unsigned = (MySqlBindingTraits::am_unsigned ? MLM_TRUE : MLM_FALSE); + } + + /// @brief Converts timestamp to database time value and copies it to + /// the buffer. + /// + /// @param timestamp Timestamp value as posix time. + void setTimestampValue(const boost::posix_time::ptime& timestamp); + + /// @brief Checks if the data accessor called is matching the type + /// of the data held in the binding. + /// + /// @tparam Data type requested, e.g. @c std::string. + /// + /// @throw InvalidOperation if the requested data type is not matching + /// the type of the binding, e.g. called @c getString but the binding + /// type is @c MYSQL_TYPE_LONG. + template + void validateAccess() const { + // Can't retrieve null value. + if (amNull()) { + isc_throw(InvalidOperation, "retrieved value is null"); + } + // The type of the accessor must match the stored data type. + if (MySqlBindingTraits::column_type != getType()) { + isc_throw(InvalidOperation, "mismatched column type"); + } + } + + /// @brief Data buffer (both input and output). + std::vector buffer_; + + /// @brief Buffer length. + unsigned long length_; + + /// @brief Flag indicating whether the value of the binding is NULL. + my_bool null_value_; + + /// @brief Native MySQL binding wrapped by this class. + MYSQL_BIND bind_; +}; + +/// @brief Collection of bindings. +typedef std::vector BindingCollection; + + +} // end of namespace isc::db +} // end of namespace isc + +#endif diff --git a/src/lib/mysql/mysql_connection.cc b/src/lib/mysql/mysql_connection.cc index 350506563f..e51a61f13c 100644 --- a/src/lib/mysql/mysql_connection.cc +++ b/src/lib/mysql/mysql_connection.cc @@ -4,7 +4,6 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. - #include #include @@ -24,11 +23,6 @@ using namespace std; namespace isc { namespace db { -const my_bool MLM_FALSE = 0; -const my_bool MLM_TRUE = 1; -const int MLM_MYSQL_FETCH_SUCCESS = 0; -const int MLM_MYSQL_FETCH_FAILURE = 1; - /// @todo: Migrate this default value to src/bin/dhcpX/simple_parserX.cc const int MYSQL_DEFAULT_CONNECTION_TIMEOUT = 5; // seconds @@ -298,73 +292,20 @@ MySqlConnection::~MySqlConnection() { void MySqlConnection::convertToDatabaseTime(const time_t input_time, MYSQL_TIME& output_time) { - - // Convert to broken-out time - struct tm time_tm; - (void) localtime_r(&input_time, &time_tm); - - // Place in output expire structure. - output_time.year = time_tm.tm_year + 1900; - output_time.month = time_tm.tm_mon + 1; // Note different base - output_time.day = time_tm.tm_mday; - output_time.hour = time_tm.tm_hour; - output_time.minute = time_tm.tm_min; - output_time.second = time_tm.tm_sec; - output_time.second_part = 0; // No fractional seconds - output_time.neg = my_bool(0); // Not negative + MySqlBinding::convertToDatabaseTime(input_time, output_time); } void MySqlConnection::convertToDatabaseTime(const time_t cltt, const uint32_t valid_lifetime, MYSQL_TIME& expire) { - - // Calculate expiry time. Store it in the 64-bit value so as we can detect - // overflows. - int64_t expire_time_64 = static_cast(cltt) + - static_cast(valid_lifetime); - - // Even on 64-bit systems MySQL doesn't seem to accept the timestamps - // beyond the max value of int32_t. - if (expire_time_64 > DatabaseConnection::MAX_DB_TIME) { - isc_throw(BadValue, "Time value is too large: " << expire_time_64); - } - - const time_t expire_time = static_cast(expire_time_64); - - // Convert to broken-out time - struct tm expire_tm; - (void) localtime_r(&expire_time, &expire_tm); - - // Place in output expire structure. - expire.year = expire_tm.tm_year + 1900; - expire.month = expire_tm.tm_mon + 1; // Note different base - expire.day = expire_tm.tm_mday; - expire.hour = expire_tm.tm_hour; - expire.minute = expire_tm.tm_min; - expire.second = expire_tm.tm_sec; - expire.second_part = 0; // No fractional seconds - expire.neg = my_bool(0); // Not negative + MySqlBinding::convertToDatabaseTime(cltt, valid_lifetime, expire); } void MySqlConnection::convertFromDatabaseTime(const MYSQL_TIME& expire, - uint32_t valid_lifetime, time_t& cltt) { - - // Copy across fields from MYSQL_TIME structure. - struct tm expire_tm; - memset(&expire_tm, 0, sizeof(expire_tm)); - - expire_tm.tm_year = expire.year - 1900; - expire_tm.tm_mon = expire.month - 1; - expire_tm.tm_mday = expire.day; - expire_tm.tm_hour = expire.hour; - expire_tm.tm_min = expire.minute; - expire_tm.tm_sec = expire.second; - expire_tm.tm_isdst = -1; // Let the system work out about DST - - // Convert to local time - cltt = mktime(&expire_tm) - valid_lifetime; + uint32_t valid_lifetime, time_t& cltt) { + MySqlBinding::convertFromDatabaseTime(expire, valid_lifetime, cltt); } void diff --git a/src/lib/mysql/mysql_connection.h b/src/lib/mysql/mysql_connection.h index e77a9d1e84..16f0458123 100644 --- a/src/lib/mysql/mysql_connection.h +++ b/src/lib/mysql/mysql_connection.h @@ -8,42 +8,22 @@ #define MYSQL_CONNECTION_H #include +#include #include #include +#include +#include #include #include #include #include +#include #include #include namespace isc { namespace db { -/// @name MySQL constants. -/// -//@{ - -/// @brief MySQL false value. -extern const my_bool MLM_FALSE; - -/// @brief MySQL true value. -extern const my_bool MLM_TRUE; - -/// @brief MySQL fetch success code. -extern const int MLM_MYSQL_FETCH_SUCCESS; - -/// @brief MySQL fetch failure code. -extern const int MLM_MYSQL_FETCH_FAILURE; - -//@} - -/// @name Current database schema version values. -//@{ -const uint32_t MYSQL_SCHEMA_VERSION_MAJOR = 7; -const uint32_t MYSQL_SCHEMA_VERSION_MINOR = 0; - -//@} /// @brief Fetch and Release MySQL Results /// @@ -211,6 +191,9 @@ private: class MySqlConnection : public db::DatabaseConnection { public: + /// @brief Function invoked to process fetched row. + typedef std::function ConsumeResultFun; + /// @brief Constructor /// /// Initialize MySqlConnection object with parameters needed for connection. @@ -329,6 +312,171 @@ public: /// @brief Starts Transaction void startTransaction(); + /// @brief Executes SELECT query using prepared statement. + /// + /// The statement index must point to an existing prepared statement + /// associated with the connection. The @c in_bindings size must match + /// the number of placeholders in the prepared statement. The size of + /// the @c out_bindings must match the number of selected columns. The + /// output bindings must be created and must encapsulate values of + /// the appropriate type, e.g. string, uint32_t etc. + /// + /// This method executes prepared statement using provided bindings and + /// calls @c process_result function for each returned row. The + /// @c process_result function is implemented by the caller and should + /// gather and store each returned row in an external data structure prior + /// to returning because the values in the @c out_bindings will be + /// overwritten by the values of the next returned row when this function + /// is called again. + /// + /// @tparam StatementIndex Type of the statement index enum. + /// + /// @param index Index of the query to be executed. + /// @param in_bindings Input bindings holding values to substitue placeholders + /// in the query. + /// @param [out] out_bindings Output bindings where retrieved data will be + /// stored. + /// @param process_result Pointer to the function to be invoked for each + /// retrieved row. This function consumes the retrieved data from the + /// output bindings. + template + void selectQuery(const StatementIndex& index, + const BindingCollection& in_bindings, + BindingCollection& out_bindings, + ConsumeResultFun process_result) { + // Extract native input bindings. + std::vector in_bind_vec; + for (MySqlBindingPtr in_binding : in_bindings) { + in_bind_vec.push_back(in_binding->getMySqlBinding()); + } + + int status = 0; + if (!in_bind_vec.empty()) { + // Bind parameters to the prepared statement. + status = mysql_stmt_bind_param(statements_[index], &in_bind_vec[0]); + } + + // Bind variables that will receive results as well. + std::vector out_bind_vec; + for (MySqlBindingPtr out_binding : out_bindings) { + out_bind_vec.push_back(out_binding->getMySqlBinding()); + } + if (!out_bind_vec.empty()) { + status = mysql_stmt_bind_result(statements_[index], &out_bind_vec[0]); + } + + // Execute query. + status = mysql_stmt_execute(statements_[index]); + checkError(status, index, "unable to execute"); + + status = mysql_stmt_store_result(statements_[index]); + checkError(status, index, "unable to set up for storing all results"); + + // Fetch results. + MySqlFreeResult fetch_release(statements_[index]); + while ((status = mysql_stmt_fetch(statements_[index])) == + MLM_MYSQL_FETCH_SUCCESS) { + try { + // For each returned row call user function which should + // consume the row and copy the data to a safe place. + process_result(out_bindings); + + } catch (const std::exception& ex) { + // Rethrow the exception with a bit more data. + isc_throw(BadValue, ex.what() << ". Statement is <" << + text_statements_[index] << ">"); + } + } + + // How did the fetch end? + // If mysql_stmt_fetch return value is equal to 1 an error occurred. + if (status == MLM_MYSQL_FETCH_FAILURE) { + // Error - unable to fetch results + checkError(status, index, "unable to fetch results"); + + } else if (status == MYSQL_DATA_TRUNCATED) { + // Data truncated - throw an exception indicating what was at fault + isc_throw(DataTruncated, text_statements_[index] + << " returned truncated data"); + } + } + + /// @brief Executes INSERT prepared statement. + /// + /// The statement index must point to an existing prepared statement + /// associated with the connection. The @c in_bindings size must match + /// the number of placeholders in the prepared statement. + /// + /// This method executes prepared statement using provided bindings to + /// insert data into the database. + /// + /// @tparam StatementIndex Type of the statement index enum. + /// + /// @param index Index of the query to be executed. + /// @param in_bindings Input bindings holding values to substitue placeholders + /// in the query. + template + void insertQuery(const StatementIndex& index, + const BindingCollection& in_bindings) { + std::vector in_bind_vec; + for (MySqlBindingPtr in_binding : in_bindings) { + in_bind_vec.push_back(in_binding->getMySqlBinding()); + } + + // Bind the parameters to the statement + int status = mysql_stmt_bind_param(statements_[index], &in_bind_vec[0]); + checkError(status, index, "unable to bind parameters"); + + // Execute the statement + status = mysql_stmt_execute(statements_[index]); + + if (status != 0) { + // Failure: check for the special case of duplicate entry. + if (mysql_errno(mysql_) == ER_DUP_ENTRY) { + isc_throw(DuplicateEntry, "Database duplicate entry error"); + } + checkError(status, index, "unable to execute"); + } + } + + /// @brief Executes UPDATE or DELETE prepared statement and returns + /// the number of affected rows. + /// + /// The statement index must point to an existing prepared statement + /// associated with the connection. The @c in_bindings size must match + /// the number of placeholders in the prepared statement. + /// + /// @tparam StatementIndex Type of the statement index enum. + /// + /// @param index Index of the query to be executed. + /// @param in_bindings Input bindings holding values to substitue placeholders + /// in the query. + /// + /// @return Number of affected rows. + template + uint64_t updateDeleteQuery(const StatementIndex& index, + const BindingCollection& in_bindings) { + std::vector in_bind_vec; + for (MySqlBindingPtr in_binding : in_bindings) { + in_bind_vec.push_back(in_binding->getMySqlBinding()); + } + + // Bind the parameters to the statement + int status = mysql_stmt_bind_param(statements_[index], &in_bind_vec[0]); + checkError(status, index, "unable to bind parameters"); + + // Execute the statement + status = mysql_stmt_execute(statements_[index]); + + if (status != 0) { + checkError(status, index, "unable to execute"); + } + + // Let's return how many rows were affected. + return (static_cast(mysql_stmt_affected_rows(statements_[index]))); + } + + /// @brief Commit Transactions /// /// Commits all pending database operations. On databases that don't diff --git a/src/lib/mysql/mysql_constants.h b/src/lib/mysql/mysql_constants.h new file mode 100644 index 0000000000..71d3a2fe9a --- /dev/null +++ b/src/lib/mysql/mysql_constants.h @@ -0,0 +1,44 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef MYSQL_CONSTANTS_H +#define MYSQL_CONSTANTS_H + +#include + +namespace isc { +namespace db { + +/// @name MySQL constants. +/// +//@{ + +/// @brief MySQL false value. +const my_bool MLM_FALSE = 0; + +/// @brief MySQL true value. +const my_bool MLM_TRUE = 1; + +/// @brief MySQL fetch success code. +const int MLM_MYSQL_FETCH_SUCCESS = 0; + +/// @brief MySQL fetch failure code. +const int MLM_MYSQL_FETCH_FAILURE = 0; + +//@} + +/// @name Current database schema version values. +//@{ +const uint32_t MYSQL_SCHEMA_VERSION_MAJOR = 7; +const uint32_t MYSQL_SCHEMA_VERSION_MINOR = 0; + +//@} + + +} // end of namespace isc::db +} // end of namespace isc + +#endif diff --git a/src/lib/mysql/tests/.gitignore b/src/lib/mysql/tests/.gitignore new file mode 100644 index 0000000000..b5459bd823 --- /dev/null +++ b/src/lib/mysql/tests/.gitignore @@ -0,0 +1 @@ +/libmysql_unittests \ No newline at end of file diff --git a/src/lib/mysql/tests/Makefile.am b/src/lib/mysql/tests/Makefile.am new file mode 100644 index 0000000000..14d5f16f5c --- /dev/null +++ b/src/lib/mysql/tests/Makefile.am @@ -0,0 +1,40 @@ +SUBDIRS = . + +AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib +AM_CPPFLAGS += $(BOOST_INCLUDES) $(MYSQL_CPPFLAGS) + +AM_CXXFLAGS = $(KEA_CXXFLAGS) + +if USE_STATIC_LINK +AM_LDFLAGS = -static +endif + +CLEANFILES = *.gcno *.gcda + +TESTS_ENVIRONMENT = \ + $(LIBTOOL) --mode=execute $(VALGRIND_COMMAND) + +TESTS = +if HAVE_GTEST +TESTS += libmysql_unittests + +libmysql_unittests_SOURCES = mysql_connection_unittest.cc +libmysql_unittests_SOURCES += run_unittests.cc + +libmysql_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES) +libmysql_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS) $(MYSQL_LIBS) + +libmysql_unittests_LDADD = $(top_builddir)/src/lib/mysql/testutils/libmysqltest.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/mysql/libkea-mysql.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/database/libkea-database.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/cc/libkea-cc.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/asiolink/libkea-asiolink.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/log/libkea-log.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/util/threads/libkea-threads.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/util/libkea-util.la +libmysql_unittests_LDADD += $(top_builddir)/src/lib/exceptions/libkea-exceptions.la +libmysql_unittests_LDADD += $(LOG4CPLUS_LIBS) $(BOOST_LIBS) $(GTEST_LDADD) + +endif + +noinst_PROGRAMS = $(TESTS) diff --git a/src/lib/mysql/tests/mysql_connection_unittest.cc b/src/lib/mysql/tests/mysql_connection_unittest.cc new file mode 100644 index 0000000000..f8455923a5 --- /dev/null +++ b/src/lib/mysql/tests/mysql_connection_unittest.cc @@ -0,0 +1,377 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include +#include +#include +#include + +using namespace isc::db; +using namespace isc::db::test; + +namespace { + +/// @brief Test fixture class for @c MySqlConnection class. +class MySqlConnectionTest : public ::testing::Test { +public: + + /// @brief Indexes of prepared statements used within the tests. + enum StatementIndex { + GET_BY_INT_VALUE, + DELETE_BY_INT_VALUE, + INSERT_VALUE, + NUM_STATEMENTS + }; + + /// @brief Array of tagged MySQL statements. + typedef std::array + TaggedStatementArray; + + /// @brief Prepared MySQL statements used in the tests. + TaggedStatementArray tagged_statements = { { + { GET_BY_INT_VALUE, + "SELECT tinyint_value, int_value, bigint_value, string_value," + " blob_value, timestamp_value" + " FROM mysql_connection_test WHERE int_value = ?" }, + + { DELETE_BY_INT_VALUE, + "DELETE FROM mysql_connection_test WHERE int_value = ?" }, + + { INSERT_VALUE, + "INSERT INTO mysql_connection_test (tinyint_value, int_value," + "bigint_value, string_value, blob_value, timestamp_value)" + " VALUES (?, ?, ?, ?, ?, ?)" } + } + }; + + + /// @brief Constructor. + /// + /// Re-creates database schema, opens new database connection and creates + /// prepared statements used in tests. Created schema contains a test + /// table @c mysql_connection_test which includes 6 columns of various + /// types. + MySqlConnectionTest() + : conn_(DatabaseConnection::parse(validMySQLConnectionString())) { + + try { + // Open new connection. + conn_.openDatabase(); + my_bool result = mysql_autocommit(conn_.mysql_, 1); + if (result != 0) { + isc_throw(DbOperationError, "failed to set autocommit option " + "for test MySQL connection"); + } + + // Create mysql_connection_test table. + createTestTable(); + + // Created prepared statements for basic queries to test table. + conn_.prepareStatements(tagged_statements.begin(), + tagged_statements.end()); + + } catch (...) { + std::cerr << "*** ERROR: unable to open database. The test\n" + "*** environment is broken and must be fixed before\n" + "*** the MySQL tests will run correctly.\n" + "*** The reason for the problem is described in the\n" + "*** accompanying exception output.\n"; + throw; + } + } + + /// @brief Destructor + /// + /// Removes test table from the database. + virtual ~MySqlConnectionTest() { + conn_.rollback(); + dropTestTable(); + } + + /// @brief Creates test table @c mysql_connection_test. + /// + /// The new table contains 6 columns of various data types. All of + /// the columns accept null values. + void createTestTable() { + runQuery("CREATE TABLE IF NOT EXISTS mysql_connection_test (" + "tinyint_value TINYINT NULL," + "int_value INT NULL," + "bigint_value BIGINT NULL," + "string_value TEXT NULL," + "blob_value BLOB NULL," + "timestamp_value TIMESTAMP NULL" + ")"); + } + + /// @brief Drops test table. + void dropTestTable() { + runQuery("DROP TABLE IF EXISTS mysql_connection_test"); + } + + /// @brief Runs MySQL query on the opened connection. + /// + /// @param sql Query in the textual form. + void runQuery(const std::string& sql) { + MYSQL_STMT *stmt = mysql_stmt_init(conn_.mysql_); + if (stmt == NULL) { + isc_throw(DbOperationError, "unable to allocate MySQL prepared " + "statement structure, reason: " << mysql_error(conn_.mysql_)); + } + + int status = mysql_stmt_prepare(stmt, sql.c_str(), sql.length()); + if (status != 0) { + isc_throw(DbOperationError, "unable to prepare MySQL statement <" + << sql << ">, reason: " << mysql_errno(conn_.mysql_)); + } + + // Execute the prepared statement. + if (mysql_stmt_execute(stmt) != 0) { + isc_throw(DbOperationError, "cannot execute MySQL query <" + << sql << ">, reason: " << mysql_errno(conn_.mysql_)); + } + + // Discard the statement and its resources + mysql_stmt_close(stmt); + } + + + /// @brief Tests inserting and retrieving data from the database. + /// + /// In this test data carried in the bindings is inserted into the database. + /// Then this data is retrieved from the database and compared with the + /// orginal. + /// + /// @param in_bindings Collection of bindings encapsulating the data to + /// be inserted into the database and then retrieved. + void testInsertSelect(const BindingCollection& in_bindings) { + // Expecting 6 bindings because we have 6 columns in our table. + ASSERT_EQ(6, in_bindings.size()); + // We are going to select by int_value so this value must not be null. + ASSERT_FALSE(in_bindings[1]->amNull()); + + // Store data in the database. + ASSERT_NO_THROW(conn_.insertQuery(MySqlConnectionTest::INSERT_VALUE, + in_bindings)); + + // Create input binding for select query. + BindingCollection bindings = + { MySqlBinding::createInteger(in_bindings[1]->getInteger()) }; + + // Also, create output (placeholder) bindings for receiving data. + BindingCollection out_bindings = { + MySqlBinding::createInteger(), + MySqlBinding::createInteger(), + MySqlBinding::createInteger(), + MySqlBinding::createString(512), + MySqlBinding::createBlob(512), + MySqlBinding::createTimestamp() + }; + + // Execute select statement. We expect one row to be returned. For this + // returned row the lambda provided as 4th argument should be executed. + ASSERT_NO_THROW(conn_.selectQuery(MySqlConnectionTest::GET_BY_INT_VALUE, + bindings, out_bindings, + [&](BindingCollection& out_bindings) { + + // Compare received data with input data assuming they are both non-null. + + if (!out_bindings[0]->amNull() && !in_bindings[0]->amNull()) { + EXPECT_EQ(static_cast(in_bindings[0]->getInteger()), + static_cast(out_bindings[0]->getInteger())); + } + + if (!out_bindings[1]->amNull() && !in_bindings[1]->amNull()) { + EXPECT_EQ(in_bindings[1]->getInteger(), + out_bindings[1]->getInteger()); + } + + if (!out_bindings[2]->amNull() && !in_bindings[2]->amNull()) { + EXPECT_EQ(in_bindings[2]->getInteger(), + out_bindings[2]->getInteger()); + } + + if (!out_bindings[3]->amNull() && !in_bindings[3]->amNull()) { + EXPECT_EQ(in_bindings[3]->getString(), + out_bindings[3]->getString()); + } + + if (!out_bindings[4]->amNull() && !in_bindings[4]->amNull()) { + EXPECT_EQ(in_bindings[4]->getBlob(), + out_bindings[4]->getBlob()); + } + + if (!out_bindings[5]->amNull() && !in_bindings[5]->amNull()) { + EXPECT_TRUE(in_bindings[5]->getTimestamp() == + out_bindings[5]->getTimestamp()); + } + })); + + // Make sure that null values were returned for columns for which null + // was set. + ASSERT_EQ(in_bindings.size(), out_bindings.size()); + for (auto i = 0; i < in_bindings.size(); ++i) { + EXPECT_EQ(in_bindings[i]->amNull(), out_bindings[i]->amNull()) + << "null value test failed for binding #" << i; + } + } + + /// @brief Test MySQL connection. + MySqlConnection conn_; + +}; + +// Test that non-null values of various types can be inserted and retrieved +// from the dataabse. +TEST_F(MySqlConnectionTest, select) { + std::string blob = "myblob"; + BindingCollection in_bindings = { + MySqlBinding::createInteger(123), + MySqlBinding::createInteger(1024), + MySqlBinding::createInteger(-4096), + MySqlBinding::createString("shellfish"), + MySqlBinding::createBlob(blob.begin(), blob.end()), + MySqlBinding::createTimestamp(boost::posix_time::microsec_clock::universal_time()) + }; + + testInsertSelect(in_bindings); +} + +// Test that null value can be inserted to a column having numeric type and +// retrieved. +TEST_F(MySqlConnectionTest, selectNullInteger) { + std::string blob = "myblob"; + BindingCollection in_bindings = { + MySqlBinding::createNull(), + MySqlBinding::createInteger(1024), + MySqlBinding::createInteger(-4096), + MySqlBinding::createString("shellfish"), + MySqlBinding::createBlob(blob.begin(), blob.end()), + MySqlBinding::createTimestamp(boost::posix_time::microsec_clock::universal_time()) + }; + + testInsertSelect(in_bindings); +} + +// Test that null value can be inserted to a column having string type and +// retrieved. +TEST_F(MySqlConnectionTest, selectNullString) { + std::string blob = "myblob"; + + BindingCollection in_bindings = { + MySqlBinding::createInteger(123), + MySqlBinding::createInteger(1024), + MySqlBinding::createInteger(-4096), + MySqlBinding::createNull(), + MySqlBinding::createBlob(blob.begin(), blob.end()), + MySqlBinding::createTimestamp(boost::posix_time::microsec_clock::universal_time()) + }; + + testInsertSelect(in_bindings); +} + +// Test that null value can be inserted to a column having blob type and +// retrieved. +TEST_F(MySqlConnectionTest, selectNullBlob) { + BindingCollection in_bindings = { + MySqlBinding::createInteger(123), + MySqlBinding::createInteger(1024), + MySqlBinding::createInteger(-4096), + MySqlBinding::createString("shellfish"), + MySqlBinding::createNull(), + MySqlBinding::createTimestamp(boost::posix_time::microsec_clock::universal_time()) + }; + + testInsertSelect(in_bindings); +} + +// Test that null value can be inserted to a column having timestamp type and +// retrieved. +TEST_F(MySqlConnectionTest, selectNullTimestamp) { + std::string blob = "myblob"; + BindingCollection in_bindings = { + MySqlBinding::createInteger(123), + MySqlBinding::createInteger(1024), + MySqlBinding::createInteger(-4096), + MySqlBinding::createString("shellfish"), + MySqlBinding::createBlob(blob.begin(), blob.end()), + MySqlBinding::createNull() + }; + + testInsertSelect(in_bindings); +} + +// Test that empty string and empty blob can be inserted to a database. +TEST_F(MySqlConnectionTest, selectEmptyStringBlob) { + std::string blob = ""; + BindingCollection in_bindings = { + MySqlBinding::createInteger(123), + MySqlBinding::createInteger(1024), + MySqlBinding::createInteger(-4096), + MySqlBinding::createString(""), + MySqlBinding::createBlob(blob.begin(), blob.end()), + MySqlBinding::createTimestamp(boost::posix_time::microsec_clock::universal_time()) + }; + + testInsertSelect(in_bindings); +} + +// Test that a row can be deleted from the database. +TEST_F(MySqlConnectionTest, deleteByValue) { + // Insert a row with numeric values. + BindingCollection in_bindings = { + MySqlBinding::createInteger(123), + MySqlBinding::createInteger(1024), + MySqlBinding::createInteger(-4096), + MySqlBinding::createNull(), + MySqlBinding::createNull(), + MySqlBinding::createNull() + }; + ASSERT_NO_THROW(conn_.insertQuery(MySqlConnectionTest::INSERT_VALUE, + in_bindings)); + + // This variable will be checked to see if the row has been deleted + // from the database. + bool deleted = false; + + // Execute delete query but use int_value of non existing row. + // The row should not be deleted. + in_bindings = { MySqlBinding::createInteger(1) }; + ASSERT_NO_THROW(deleted = conn_.updateDeleteQuery(MySqlConnectionTest::DELETE_BY_INT_VALUE, + in_bindings)); + ASSERT_FALSE(deleted); + + // This time use the correct value. + in_bindings = { MySqlBinding::createInteger(1024) }; + ASSERT_NO_THROW(deleted = conn_.updateDeleteQuery(MySqlConnectionTest::DELETE_BY_INT_VALUE, + in_bindings)); + // The row should have been deleted. + ASSERT_TRUE(deleted); + + // Let's confirm that it has been deleted by issuing a select query. + BindingCollection out_bindings = { + MySqlBinding::createInteger(), + MySqlBinding::createInteger(), + MySqlBinding::createInteger(), + MySqlBinding::createString(512), + MySqlBinding::createBlob(512), + MySqlBinding::createTimestamp() + }; + + ASSERT_NO_THROW(conn_.selectQuery(MySqlConnectionTest::GET_BY_INT_VALUE, + in_bindings, out_bindings, + [&deleted](BindingCollection& out_bindings) { + // This will be executed if the row is returned as a result of + // select query. We expect that this is not executed. + deleted = false; + })); + // Make sure that select query returned nothing. + EXPECT_TRUE(deleted); +} + +} diff --git a/src/lib/mysql/tests/run_unittests.cc b/src/lib/mysql/tests/run_unittests.cc new file mode 100644 index 0000000000..4e83d4bd6c --- /dev/null +++ b/src/lib/mysql/tests/run_unittests.cc @@ -0,0 +1,20 @@ +// Copyright (C) 2018 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include + +int +main(int argc, char* argv[]) { + ::testing::InitGoogleTest(&argc, argv); + isc::log::initLogger(); + + int result = RUN_ALL_TESTS(); + + return (result); +}