From: Thomas Markwalder Date: Tue, 7 Dec 2021 16:16:00 +0000 (-0500) Subject: [#2183] Added support for optionals and ptimes X-Git-Tag: Kea-2.1.2~175 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=ca7f1e44189aea54824f0edbec1dca3e0bed97be;p=thirdparty%2Fkea.git [#2183] Added support for optionals and ptimes 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 --- diff --git a/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc b/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc index 607151ab2e..75fe15da41 100644 --- a/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc +++ b/src/lib/dhcpsrv/tests/pgsql_host_data_source_unittest.cc @@ -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(); diff --git a/src/lib/pgsql/pgsql_exchange.cc b/src/lib/pgsql/pgsql_exchange.cc index 17d5bb48d7..939cf0fd92 100644 --- a/src/lib/pgsql/pgsql_exchange.cc +++ b/src/lib/pgsql/pgsql_exchange.cc @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -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& 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("")); +void +PsqlBindArray::addOptionalBool(const util::Optional& value) { + if (value.unspecified()) { + addNull(); + } else { + add(value); } +} + +void +PsqlBindArray::addOptionalIPv4Address(const util::Optional& 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(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(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) { diff --git a/src/lib/pgsql/pgsql_exchange.h b/src/lib/pgsql/pgsql_exchange.h index 6a794699f4..b44f8a49f6 100644 --- a/src/lib/pgsql/pgsql_exchange.h +++ b/src/lib/pgsql/pgsql_exchange.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -305,7 +306,17 @@ struct PsqlBindArray { /// @param triple Triplet instance from which to get the value. void addMax(const isc::util::Triplet& 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& value); + + /// @brief Adds an @c Optional boolean to the bind array. + /// + /// @param value Optional boolean value to add + void addOptionalBool(const util::Optional& 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& 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 diff --git a/src/lib/pgsql/tests/pgsql_basics.h b/src/lib/pgsql/tests/pgsql_basics.h index e3ca3f6671..8e3fa6fa65 100644 --- a/src/lib/pgsql/tests/pgsql_basics.h +++ b/src/lib/pgsql/tests/pgsql_basics.h @@ -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 diff --git a/src/lib/pgsql/tests/pgsql_exchange_unittest.cc b/src/lib/pgsql/tests/pgsql_exchange_unittest.cc index 61c5aaa98d..80d694323c 100644 --- a/src/lib/pgsql/tests/pgsql_exchange_unittest.cc +++ b/src/lib/pgsql/tests/pgsql_exchange_unittest.cc @@ -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 empty; Triplet 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 empty; + Optional 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 empty; + Optional am_false(false); + Optional 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 empty; Optional 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 empty; + Optional not_empty(asiolink::IOAddress("192.16.1.1")); + + // Verify we cannot add a v6 address. + Optional 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