]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#93,!35] Extended MySqlConnection with generic query functions.
authorMarcin Siodelski <marcin@isc.org>
Tue, 18 Sep 2018 06:36:53 +0000 (08:36 +0200)
committerMarcin Siodelski <marcin@isc.org>
Mon, 8 Oct 2018 14:39:22 +0000 (16:39 +0200)
12 files changed:
configure.ac
src/hooks/dhcp/mysql_cb/mysql_cb_dhcp4.cc
src/lib/mysql/Makefile.am
src/lib/mysql/mysql_binding.cc [new file with mode: 0644]
src/lib/mysql/mysql_binding.h [new file with mode: 0644]
src/lib/mysql/mysql_connection.cc
src/lib/mysql/mysql_connection.h
src/lib/mysql/mysql_constants.h [new file with mode: 0644]
src/lib/mysql/tests/.gitignore [new file with mode: 0644]
src/lib/mysql/tests/Makefile.am [new file with mode: 0644]
src/lib/mysql/tests/mysql_connection_unittest.cc [new file with mode: 0644]
src/lib/mysql/tests/run_unittests.cc [new file with mode: 0644]

index e87315f4c17525ab94f976afb5c3a6578667d31f..46e2fc941cd8b09c2295e05d7b0fa287aa7be0c9 100644 (file)
@@ -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
index faa84244f459cd5d116600e01969726787c7d3b4..da0acce5cf381184486eb26d9cc13ef22680a2af 100644 (file)
@@ -17,128 +17,6 @@ using namespace isc::db;
 
 namespace {
 
-class Binding;
-
-typedef boost::shared_ptr<Binding> 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<typename T>
-    T getValue() const {
-        const T* value = reinterpret_cast<const T*>(&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<uint8_t> buffer_;
-
-    unsigned long length_;
-
-    my_bool null_value_;
-
-    MYSQL_BIND bind_;
-};
-
-typedef std::vector<BindingPtr> BindingCollection;
-
-class DatabaseExchange {
-public:
-
-    typedef std::function<void()> ConsumeResultFun;
-
-    void selectQuery(MYSQL_STMT* statement,
-                     const BindingCollection& in_bindings,
-                     BindingCollection& out_bindings,
-                     ConsumeResultFun process_result) {
-        std::vector<MYSQL_BIND> 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<MYSQL_BIND> 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<uint32_t>();
     });
 
index 8852b7e8134f033de9aa14dc9d848c455a97def4..ce76c0cb3054beb2d4079eda1c8ec2958494263f 100644 (file)
@@ -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 (file)
index 0000000..ad07c4a
--- /dev/null
@@ -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 <config.h>
+
+#include <mysql/mysql_binding.h>
+
+namespace isc {
+namespace db {
+
+std::string
+MySqlBinding::getString() const {
+    // Make sure the binding type is text.
+    validateAccess<std::string>();
+    if (length_ == 0) {
+        return (std::string());
+    }
+    return (std::string(buffer_.begin(), buffer_.begin() + length_));
+}
+
+std::vector<uint8_t>
+MySqlBinding::getBlob() const {
+    // Make sure the binding type is blob.
+    validateAccess<std::vector<uint8_t> >();
+    if (length_ == 0) {
+        return (std::vector<uint8_t>());
+    }
+    return (std::vector<uint8_t>(buffer_.begin(), buffer_.begin() + length_));
+}
+
+boost::posix_time::ptime
+MySqlBinding::getTimestamp() const {
+    // Make sure the binding type is timestamp.
+    validateAccess<boost::posix_time::ptime>();
+    // Copy the buffer contents into native timestamp structure and
+    // then convert it to posix time.
+    const MYSQL_TIME* database_time = reinterpret_cast<const MYSQL_TIME*>(&buffer_[0]);
+    return (convertFromDatabaseTime(*database_time));
+}
+
+MySqlBindingPtr
+MySqlBinding::createString(const unsigned long length) {
+    MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<std::string>::column_type,
+                                             length));
+    return (binding);
+}
+
+MySqlBindingPtr
+MySqlBinding::createString(const std::string& value) {
+    MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<std::string>::column_type,
+                                             value.size()));
+    binding->setBufferValue(value.begin(), value.end());
+    return (binding);
+}
+
+MySqlBindingPtr
+MySqlBinding::createBlob(const unsigned long length) {
+    MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<std::vector<uint8_t> >::column_type,
+                                   length));
+    return (binding);
+}
+
+MySqlBindingPtr
+MySqlBinding::createTimestamp(const boost::posix_time::ptime& timestamp) {
+    MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<boost::posix_time::ptime>::column_type,
+                                   MySqlBindingTraits<boost::posix_time::ptime>::length));
+    binding->setTimestampValue(timestamp);
+    return (binding);
+}
+
+MySqlBindingPtr
+MySqlBinding::createTimestamp() {
+    MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<boost::posix_time::ptime>::column_type,
+                                             MySqlBindingTraits<boost::posix_time::ptime>::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<int64_t>(cltt) +
+        static_cast<int64_t>(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<const time_t>(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<void*>(&buffer_[0]), reinterpret_cast<char*>(&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 (file)
index 0000000..450d63d
--- /dev/null
@@ -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 <database/database_connection.h>
+#include <exceptions/exceptions.h>
+#include <boost/date_time/posix_time/conversion.hpp>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <boost/shared_ptr.hpp>
+#include <mysql/mysql_constants.h>
+#include <mysql.h>
+#include <mysqld_error.h>
+#include <cstdint>
+#include <iterator>
+#include <string>
+#include <vector>
+
+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<typename T>
+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<std::string> {
+    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<boost::posix_time::ptime> {
+    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<int8_t> {
+    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<uint8_t> {
+    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<int16_t> {
+    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<uint16_t> {
+    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<int32_t> {
+    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<uint32_t> {
+    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<int64_t> {
+    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<uint64_t> {
+    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<MySqlBinding> 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<uint8_t> 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<typename T>
+    T getInteger() const {
+        // Make sure that the binding type is numeric.
+        validateAccess<T>();
+
+        // Convert the buffer to a numeric type and then return a copy.
+        const T* value = reinterpret_cast<const T*>(&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<typename Iterator>
+    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<typename T>
+    static MySqlBindingPtr createInteger() {
+        MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<T>::column_type,
+                                                 MySqlBindingTraits<T>::length));
+        binding->setValue<T>(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<typename T>
+    static MySqlBindingPtr createInteger(T value) {
+        MySqlBindingPtr binding(new MySqlBinding(MySqlBindingTraits<T>::column_type,
+                                                 MySqlBindingTraits<T>::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<typename Iterator>
+    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<typename T>
+    void setValue(T value) {
+        memcpy(static_cast<void*>(&buffer_[0]), reinterpret_cast<char*>(&value),
+               sizeof(value));
+        bind_.buffer = &buffer_[0];
+        bind_.is_unsigned = (MySqlBindingTraits<T>::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<typename T>
+    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<T>::column_type != getType()) {
+            isc_throw(InvalidOperation, "mismatched column type");
+        }
+    }
+
+    /// @brief Data buffer (both input and output).
+    std::vector<uint8_t> 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<MySqlBindingPtr> BindingCollection;
+
+
+} // end of namespace isc::db
+} // end of namespace isc
+
+#endif
index 350506563f2e7fd07e44690a081d5dcd7a3b8494..e51a61f13c069f5a871e3d873ea3f77ff68b35bd 100644 (file)
@@ -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 <config.h>
 
 #include <database/db_log.h>
@@ -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<int64_t>(cltt) +
-        static_cast<int64_t>(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<const time_t>(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
index e77a9d1e84b39c91016c133b9a68eb81f9e453c6..16f0458123c6035dbbe81b341f29ce6d24fde1ec 100644 (file)
@@ -8,42 +8,22 @@
 #define MYSQL_CONNECTION_H
 
 #include <database/database_connection.h>
+#include <database/db_exceptions.h>
 #include <database/db_log.h>
 #include <exceptions/exceptions.h>
+#include <mysql/mysql_binding.h>
+#include <mysql/mysql_constants.h>
 #include <boost/scoped_ptr.hpp>
 #include <mysql.h>
 #include <mysqld_error.h>
 #include <errmsg.h>
+#include <functional>
 #include <vector>
 #include <stdint.h>
 
 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<void(BindingCollection&)> 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<typename StatementIndex>
+    void selectQuery(const StatementIndex& index,
+                     const BindingCollection& in_bindings,
+                     BindingCollection& out_bindings,
+                     ConsumeResultFun process_result) {
+        // Extract native input bindings.
+        std::vector<MYSQL_BIND> 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<MYSQL_BIND> 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<typename StatementIndex>
+    void insertQuery(const StatementIndex& index,
+                     const BindingCollection& in_bindings) {
+        std::vector<MYSQL_BIND> 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<typename StatementIndex>
+    uint64_t updateDeleteQuery(const StatementIndex& index,
+                               const BindingCollection& in_bindings) {
+        std::vector<MYSQL_BIND> 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<uint64_t>(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 (file)
index 0000000..71d3a2f
--- /dev/null
@@ -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 <mysql.h>
+
+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 (file)
index 0000000..b5459bd
--- /dev/null
@@ -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 (file)
index 0000000..14d5f16
--- /dev/null
@@ -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 (file)
index 0000000..f845592
--- /dev/null
@@ -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 <config.h>
+
+#include <mysql/mysql_connection.h>
+#include <mysql/testutils/mysql_schema.h>
+#include <boost/date_time/posix_time/posix_time.hpp>
+#include <gtest/gtest.h>
+#include <array>
+
+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<TaggedStatement, NUM_STATEMENTS>
+    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<uint32_t>(in_bindings[1]->getInteger<uint32_t>()) };
+
+        // Also, create output (placeholder) bindings for receiving data.
+        BindingCollection out_bindings = {
+            MySqlBinding::createInteger<uint8_t>(),
+            MySqlBinding::createInteger<uint32_t>(),
+            MySqlBinding::createInteger<int64_t>(),
+            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<int>(in_bindings[0]->getInteger<uint8_t>()),
+                          static_cast<int>(out_bindings[0]->getInteger<uint8_t>()));
+            }
+
+            if (!out_bindings[1]->amNull() && !in_bindings[1]->amNull()) {
+                EXPECT_EQ(in_bindings[1]->getInteger<uint32_t>(),
+                          out_bindings[1]->getInteger<uint32_t>());
+            }
+
+            if (!out_bindings[2]->amNull() && !in_bindings[2]->amNull()) {
+                EXPECT_EQ(in_bindings[2]->getInteger<int64_t>(),
+                          out_bindings[2]->getInteger<int64_t>());
+            }
+
+            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<uint8_t>(123),
+        MySqlBinding::createInteger<uint32_t>(1024),
+        MySqlBinding::createInteger<int64_t>(-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<uint32_t>(1024),
+        MySqlBinding::createInteger<int64_t>(-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<uint8_t>(123),
+        MySqlBinding::createInteger<uint32_t>(1024),
+        MySqlBinding::createInteger<int64_t>(-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<uint8_t>(123),
+        MySqlBinding::createInteger<uint32_t>(1024),
+        MySqlBinding::createInteger<int64_t>(-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<uint8_t>(123),
+        MySqlBinding::createInteger<uint32_t>(1024),
+        MySqlBinding::createInteger<int64_t>(-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<uint8_t>(123),
+        MySqlBinding::createInteger<uint32_t>(1024),
+        MySqlBinding::createInteger<int64_t>(-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<uint8_t>(123),
+        MySqlBinding::createInteger<uint32_t>(1024),
+        MySqlBinding::createInteger<int64_t>(-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<uint32_t>(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<uint32_t>(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<uint8_t>(),
+        MySqlBinding::createInteger<uint32_t>(),
+        MySqlBinding::createInteger<int64_t>(),
+        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 (file)
index 0000000..4e83d4b
--- /dev/null
@@ -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 <config.h>
+
+#include <log/logger_support.h>
+#include <gtest/gtest.h>
+
+int
+main(int argc, char* argv[]) {
+    ::testing::InitGoogleTest(&argc, argv);
+    isc::log::initLogger();
+
+    int result = RUN_ALL_TESTS();
+
+    return (result);
+}