]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#2183] Added support for optionals and ptimes
authorThomas Markwalder <tmark@isc.org>
Tue, 7 Dec 2021 16:16:00 +0000 (11:16 -0500)
committerThomas Markwalder <tmark@isc.org>
Tue, 21 Dec 2021 18:30:14 +0000 (13:30 -0500)
src/lib/pgsql/pgsql_exchange.*
    New functions:
    PsqlBindArray::addOptionalString()
    PsqlBindArray::addOptionalBool()
    PsqlBindArray::addOptionalIPv4Address()
    PsqlBindArray::addTimestamp() - adds from ptime
    PsqlBindArray::addTimestamp() - add current time
    PgSqlExchange::convertFromDatabaseTime() - convert to ptime

src/lib/pgsql/tests/pgsql_exchange_unittest.cc
    TEST(PsqlBindArray, addOptionalString)
    TEST(PsqlBindArray, addOptionalBool)
    TEST(PsqlBindArray, addOptionalIPv4Address)
    TEST_F(PgSqlBasicsTest, ptimeTimestamp) - new tests

src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc
src/lib/pgsql/pgsql_exchange.cc
src/lib/pgsql/pgsql_exchange.h
src/lib/pgsql/tests/pgsql_basics.h
src/lib/pgsql/tests/pgsql_exchange_unittest.cc

index 607151ab2eb2b6a1d6b373629cf176a42f04c368..75fe15da418724ad910cd26231ac5b841ea6a9d0 100644 (file)
@@ -1458,7 +1458,12 @@ PgSQLHostMgrTest::SetUp() {
 
 void
 PgSQLHostMgrTest::TearDown() {
-    HostMgr::instance().getHostDataSource()->rollback();
+    try {
+        HostMgr::instance().getHostDataSource()->rollback();
+    } catch(...) {
+        // we don't care if we aren't in a transaction.
+    }
+
     HostMgr::delBackend("postgresql");
     // If data wipe enabled, delete transient data otherwise destroy the schema
     db::test::destroyPgSQLSchema();
index 17d5bb48d735d956b2f43e8f2654a37a28b5b337..939cf0fd92f23befe5bfdf0fe9b7c9154854683c 100644 (file)
@@ -9,6 +9,7 @@
 #include <pgsql/pgsql_exchange.h>
 #include <exceptions/exceptions.h>
 
+#include <boost/date_time/posix_time/posix_time.hpp>
 #include <boost/lexical_cast.hpp>
 
 #include <iomanip>
@@ -120,40 +121,79 @@ void PsqlBindArray::addTempString(const std::string& str) {
     PsqlBindArray::add((bound_strs_.back())->c_str());
 }
 
-std::string PsqlBindArray::toText() const {
-    std::ostringstream stream;
-    for (int i = 0; i < values_.size(); ++i) {
-        stream << i << " : ";
-        stream << toText(i) << std::endl;
+void
+PsqlBindArray::addOptionalString(const util::Optional<std::string>& value) {
+    if (value.unspecified()) {
+        addNull();
+    } else {
+        add(value);
     }
-
-    return (stream.str());
 }
 
-std::string
-PsqlBindArray::toText(size_t index) const {
-    if (index >= values_.size()) {
-        // We don't throw to keep this exception safe for logging.
-        return(std::string("<index-out-of-range>"));
+void
+PsqlBindArray::addOptionalBool(const util::Optional<bool>& value) {
+    if (value.unspecified()) {
+        addNull();
+    } else {
+        add(value);
     }
+}
+
+void
+PsqlBindArray::addOptionalIPv4Address(const util::Optional<isc::asiolink::IOAddress>& value) {
+    // If the value is unspecified it doesn't matter what the value is.
+    if (value.unspecified()) {
+        addNull();
+    } else {
+        // Make sure it is an IPv4 address.
+        if (!value.get().isV4()) {
+            isc_throw(BadValue, "unable to add address to PsqlBindAray '"
+                  << value.get().toText() << "' is not an IPv4 address");
+        }
 
-    if (lengths_[index] == 0) {
-        return(std::string("empty"));
+        add((value.get().toUint32()));
     }
+}
+
+void
+PsqlBindArray::addTimestamp(const boost::posix_time::ptime& timestamp) {
+    time_t input_time = boost::posix_time::to_time_t(timestamp);
+    // Converts to timestamp to local date/time string.
+    addTempString(PgSqlExchange::convertToDatabaseTime(input_time));
+}
+
+void
+PsqlBindArray::addTimestamp() {
+    time_t now;
+    time(&now);
+    addTempString(PgSqlExchange::convertToDatabaseTime(now));
+}
 
+std::string
+PsqlBindArray::toText() const {
     std::ostringstream stream;
-    if (formats_[index] == TEXT_FMT) {
-        stream << "\"" << values_[index] << "\"";
-    } else {
-        const char *data = values_[index];
-        stream << "0x";
-        for (int x = 0; x < lengths_[index]; ++x) {
-                stream << std::setfill('0') << std::setw(2)
-                       << std::setbase(16)
-                       << static_cast<unsigned int>(data[x]);
+
+    for (int i = 0; i < values_.size(); ++i) {
+        stream << i << " : ";
+
+        if (lengths_[i] == 0) {
+            stream << "empty" << std::endl;
+            continue;
         }
 
-        stream << std::setbase(10);
+        if (formats_[i] == TEXT_FMT) {
+            stream << "\"" << values_[i] << "\"" << std::endl;
+        } else {
+            const char *data = values_[i];
+            stream << "0x";
+            for (int x = 0; x < lengths_[i]; ++x) {
+                    stream << std::setfill('0') << std::setw(2)
+                           << std::setbase(16)
+                           << static_cast<unsigned int>(data[x]);
+            }
+
+            stream << std::endl << std::setbase(10);
+        }
     }
 
     return (stream.str());
@@ -177,6 +217,8 @@ PgSqlExchange::convertToDatabaseTime(const time_t input_time) {
     struct tm tinfo;
     char buffer[20];
     localtime_r(&input_time, &tinfo);
+    // PostgreSQL will assume the value is already in local time since we
+    // do not specify timezone in the string.
     strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &tinfo);
     return (std::string(buffer));
 }
@@ -214,6 +256,13 @@ PgSqlExchange::convertFromDatabaseTime(const std::string& db_time_val) {
     return (new_time);
 }
 
+void
+PgSqlExchange::convertFromDatabaseTime(const std::string& db_time_val,
+                                       boost::posix_time::ptime& conv_time) {
+    time_t tmp_time = convertFromDatabaseTime(db_time_val);
+    conv_time = boost::posix_time::from_time_t(tmp_time);
+}
+
 const char*
 PgSqlExchange::getRawColumnValue(const PgSqlResult& r, const int row,
                                  const size_t col) {
index 6a794699f4a93c00f883d2669654671a5793ac1e..b44f8a49f6cc0d1aef91c9df88d1496247fc419c 100644 (file)
@@ -10,6 +10,7 @@
 #include <asiolink/io_address.h>
 #include <database/database_connection.h>
 #include <util/triplet.h>
+#include <util/boost_time_utils.h>
 #include <exceptions/exceptions.h>
 
 #include <boost/lexical_cast.hpp>
@@ -305,7 +306,17 @@ struct PsqlBindArray {
     /// @param triple Triplet instance from which to get the value.
     void addMax(const isc::util::Triplet<uint32_t>& triplet);
 
-    /// @brief Adds an @c Optional of integer type to 0the bind array.
+    /// @brief Adds an @c Optional string to the bind array.
+    ///
+    /// @param value Optional string value to add
+    void addOptionalString(const util::Optional<std::string>& value);
+
+    /// @brief Adds an @c Optional boolean to the bind array.
+    ///
+    /// @param value Optional boolean value to add
+    void addOptionalBool(const util::Optional<bool>& value);
+
+    /// @brief Adds an @c Optional of integer type to the bind array.
     ///
     /// @tparam T Numeric type corresponding to the binding type, e.g.
     /// @c uint8_t, @c uint16_t etc.
@@ -319,14 +330,28 @@ struct PsqlBindArray {
         }
     }
 
+    /// @brief Adds an @c Optional IPv4 address to the bind array.
+    ///
+    /// @param value Optional boolean value to add
+    /// @throw BadValue if the address is not a IPv4 address.
+    void addOptionalIPv4Address(const util::Optional<isc::asiolink::IOAddress>& value);
+
+    /// @brief Adds a timestamp from a ptime to the bind array.
+    ///
+    /// Precision is seconds.
+    ///
+    /// @param timestamp Timestamp value to be sent to the database.
+    void addTimestamp(const boost::posix_time::ptime& timestamp);
+
+    /// @brief Adds a timestamp of the current time to the bind array.
+    ///
+    /// Precision is seconds.
+    void addTimestamp();
+
     /// @brief Dumps the contents of the array to a string.
     /// @return std::string containing the dump
     std::string toText() const;
 
-    /// @brief Dumps the contents of an array element's value to a string.
-    /// @return std::string containing the dump
-    std::string toText(size_t index) const;
-
     // --- the following methods are mostly useful for testing -----
 
     /// @brief Determines if specified value is null
@@ -432,6 +457,18 @@ public:
     /// @return Converted timestamp as time_t value.
     static time_t convertFromDatabaseTime(const std::string& db_time_val);
 
+    /// @brief Converts time stamp from the database to a boost::posix::ptime
+    ///
+    /// We're fetching timestamps as an integer string of seconds since the
+    /// epoch.  This method converts such a string int a time_t.
+    ///
+    /// @param db_time_val timestamp to be converted.  This value
+    /// is expected to be the number of seconds since the epoch
+    /// expressed as base-10 integer string.
+    /// @param[out] conv_time resulting time as a ptime (UTC)
+    static void convertFromDatabaseTime(const std::string& db_time_val,
+                                        boost::posix_time::ptime& conv_time);
+
     /// @brief Gets a pointer to the raw column value in a result set row
     ///
     /// Given a result set, row, and column return a const char* pointer to
index e3ca3f6671ec3a8471faa20ce7af6fb540871405..8e3fa6fa651f87e59106f03c02ed28237212ea97 100644 (file)
@@ -207,7 +207,8 @@ public:
             "   id, bool_col, bytea_col, bigint_col, smallint_col, "
             "   int_col, text_col,"
             "   extract(epoch from timestamp_col)::bigint as timestamp_col,"
-            "   varchar_col FROM basics";
+            "   varchar_col"
+            " FROM basics";
 
         runSql(r, sql, PGRES_TUPLES_OK, line);
         ASSERT_EQ(r->getRows(), exp_rows) << "fetch at line: " << line
index 61c5aaa98d2083a579c4eb0f3ceb0b10422d7ca0..80d694323c4c5ea49fcd571901c3a48e4dab6165 100644 (file)
@@ -23,6 +23,8 @@ using namespace isc;
 using namespace isc::db;
 using namespace isc::db::test;
 using namespace isc::util;
+using namespace boost::posix_time;
+using namespace boost::gregorian;
 
 namespace {
 
@@ -111,7 +113,7 @@ TEST(PsqlBindArray, addTriplet) {
         Triplet<uint32_t> empty;
         Triplet<uint32_t> not_empty(1,2,3);
 
-        // Add an unspecified triplet value.
+        // Add triplets to the array.
         b.add(empty);
         b.add(not_empty);
         b.addMin(empty);
@@ -122,6 +124,8 @@ TEST(PsqlBindArray, addTriplet) {
 
     // We've left bind scope, everything should be intact.
     EXPECT_EQ(6, b.size());
+
+    // Verify contents are correct.
     std::string expected =
         "0 : empty\n"
         "1 : \"2\"\n"
@@ -133,6 +137,67 @@ TEST(PsqlBindArray, addTriplet) {
     EXPECT_EQ(expected, b.toText());
 }
 
+/// @brief Verifies the ability to add Optional strings to
+/// the bind array.
+TEST(PsqlBindArray, addOptionalString) {
+
+    PsqlBindArray b;
+
+    // Add all the items within a different scope. Everything should
+    // still be valid once we exit this scope.
+    {
+        Optional<std::string> empty;
+        Optional<std::string> not_empty("whoopee!");
+
+        // Add strings to the array.
+        b.addOptionalString(empty);
+        b.addOptionalString(not_empty);
+    }
+
+    // We've left bind scope, everything should be intact.
+    EXPECT_EQ(2, b.size());
+
+    // Verify contents are correct.
+    std::string expected =
+        "0 : empty\n"
+        "1 : \"whoopee!\"\n";
+
+    EXPECT_EQ(expected, b.toText());
+}
+
+
+/// @brief Verifies the ability to add Optional booleans to
+/// the bind array.
+TEST(PsqlBindArray, addOptionalBool) {
+
+    PsqlBindArray b;
+
+    // Add all the items within a different scope. Everything should
+    // still be valid once we exit this scope.
+    {
+        Optional<bool> empty;
+        Optional<bool> am_false(false);
+        Optional<bool> am_true(true);
+
+        // Add booleans to the array.
+        b.addOptionalBool(empty);
+        b.addOptionalBool(am_false);
+        b.addOptionalBool(am_true);
+    }
+
+    // We've left bind scope, everything should be intact.
+    EXPECT_EQ(3, b.size());
+
+    // Verify contents are correct.
+    std::string expected =
+        "0 : empty\n"
+        "1 : \"0\"\n"
+        "2 : \"1\"\n";
+
+    EXPECT_EQ(expected, b.toText());
+}
+
+
 /// @brief Verifies the ability to add OptionalIntegers to
 /// the bind array.
 TEST(PsqlBindArray, addOptionalInteger) {
@@ -145,15 +210,15 @@ TEST(PsqlBindArray, addOptionalInteger) {
         Optional<uint32_t> empty;
         Optional<uint32_t> not_empty(123);
 
-        ASSERT_TRUE(empty.unspecified());
-
-        // Add an unspecified triplet value.
+        // Add the integers to the array..
         b.addOptionalInteger(empty);
         b.addOptionalInteger(not_empty);
     }
 
     // We've left bind scope, everything should be intact.
     EXPECT_EQ(2, b.size());
+
+    // Verify contents are correct.
     std::string expected =
         "0 : empty\n"
         "1 : \"123\"\n";
@@ -161,6 +226,39 @@ TEST(PsqlBindArray, addOptionalInteger) {
     EXPECT_EQ(expected, b.toText());
 }
 
+/// @brief Verifies the ability to add Optional IPv4 addresses to
+/// the bind array.
+TEST(PsqlBindArray, addOptionalIPv4Address) {
+
+    PsqlBindArray b;
+
+    // Add all the items within a different scope. Everything should
+    // still be valid once we exit this scope.
+    {
+        Optional<asiolink::IOAddress> empty;
+        Optional<asiolink::IOAddress> not_empty(asiolink::IOAddress("192.16.1.1"));
+
+        // Verify we cannot add a v6 address.
+        Optional<asiolink::IOAddress> not_v4(asiolink::IOAddress("3001::1"));
+        ASSERT_THROW_MSG(b.addOptionalIPv4Address(not_v4), BadValue, 
+                         "unable to add address to PsqlBindAray"
+                         " '3001::1' is not an IPv4 address");
+
+        // Add the addresses to the array..
+        b.addOptionalInteger(empty);
+        b.addOptionalInteger(not_empty);
+    }
+
+    // We've left bind scope, everything should be intact.
+    EXPECT_EQ(2, b.size());
+
+    // Verify contents are correct.
+    std::string expected =
+        "0 : empty\n"
+        "1 : \"192.16.1.1\"\n";
+
+    EXPECT_EQ(expected, b.toText());
+}
 
 /// @brief Verifies that PgResultSet row and column meta-data is correct
 TEST_F(PgSqlBasicsTest, rowColumnBasics) {
@@ -807,4 +905,43 @@ TEST_F(PgSqlBasicsTest, timeStampTest) {
                                                       MAX_DB_TIME), BadValue);
 }
 
+/// @brief Verify that we can read and write ptime using TIMESTAMP columns.
+TEST_F(PgSqlBasicsTest, ptimeTimestamp) {
+    // Create a prepared statement for inserting a TIMESTAMP
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_TIMESTAMP }, "timestamp_insert",
+          "INSERT INTO BASICS (timestamp_col) values ($1)" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    time_duration duration = hours(10) + minutes(14) + seconds(15);
+
+    // US National Ice Cream day
+    ptime nice_day(date(2021, Jul, 18), duration);
+
+    // Add timestamp with default/fractional seconds.
+    PsqlBindArrayPtr bind_array(new PsqlBindArray());
+    bind_array->addTimestamp(nice_day);
+    std::cout << "bind array: " << bind_array->toText() << std::endl;
+
+    PgSqlResultPtr r;
+    RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+
+    // Fetch the newly inserted row.
+    FETCH_ROWS(r, 1);
+
+    // Fetch the timestamp column
+    ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, 0, TIMESTAMP_COL));
+    std::string timestamp_str = "";
+    ASSERT_NO_THROW(PgSqlExchange::getColumnValue(*r, 0, TIMESTAMP_COL,
+                                                  timestamp_str));
+
+    // Convert fetched values into a ptime.
+    ptime fetched_time;
+    ASSERT_NO_THROW(PgSqlExchange::convertFromDatabaseTime(timestamp_str, fetched_time));
+
+    ASSERT_EQ(fetched_time, nice_day);
+}
+
 }; // namespace