From: Thomas Markwalder Date: Fri, 17 Dec 2021 18:12:31 +0000 (-0500) Subject: [#2381] Added support for JSON columns X-Git-Tag: Kea-2.1.2~168 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b37b7fa1654ebfc3df85cf56a443d47a2039efa1;p=thirdparty%2Fkea.git [#2381] Added support for JSON columns src/lib/pgsql/pgsql_exchange.* PsqlBindArray::add(const ElementPtr& value) PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row, const size_t col, ElementPtr& value) src/lib/pgsql/tests/pgsql_basics.h src/lib/pgsql/tests/pgsql_exchange_unittest.cc TEST_F(PgSqlBasicsTest, jsonTest) - new test --- diff --git a/src/lib/pgsql/pgsql_exchange.cc b/src/lib/pgsql/pgsql_exchange.cc index 8f62158dea..e3db14022b 100644 --- a/src/lib/pgsql/pgsql_exchange.cc +++ b/src/lib/pgsql/pgsql_exchange.cc @@ -17,6 +17,7 @@ #include using namespace isc::util; +using namespace isc::data; namespace isc { namespace db { @@ -217,6 +218,17 @@ PsqlBindArray::addTimestamp() { addTempString(PgSqlExchange::convertToDatabaseTime(now)); } +void +PsqlBindArray::add(const ElementPtr& value) { + if (!value) { + addNull(); + return; + } + + std::ostringstream ss; + value->toJSON(ss); + addTempString(ss.str()); +} std::string PsqlBindArray::toText() const { @@ -375,6 +387,19 @@ PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row, PgSqlExchange::convertFromDatabaseTime(db_time_val, value); } +void +PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row, + const size_t col, ElementPtr& value) { + const char* data = getRawColumnValue(r, row, col); + try { + value = Element::fromJSON(data); + } catch (const std::exception& ex) { + isc_throw(DbOperationError, "Cannot convert data: " << data + << " for: " << getColumnLabel(r, col) << " row:" << row + << " : " << ex.what()); + } +} + isc::asiolink::IOAddress PgSqlExchange::getInetValue4(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 53650c3645..af1dde0954 100644 --- a/src/lib/pgsql/pgsql_exchange.h +++ b/src/lib/pgsql/pgsql_exchange.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -406,6 +407,15 @@ struct PsqlBindArray { /// Precision is seconds. void addTimestamp(); + /// @brief Adds an ElementPtr to the bind array + /// + /// Adds a TEXT_FMT value to the end of the bind array containing + /// the JSON text output by given ElementPtr::toJSON(). + /// + /// @param value ElementPtr containing Element tree to add. + /// @throw DbOperationError if value is NULL. + void add(const data::ElementPtr& value); + /// @brief Dumps the contents of the array to a string. /// @return std::string containing the dump std::string toText() const; @@ -685,6 +695,17 @@ public: static void getColumnValue(const PgSqlResult& r, const int row, const size_t col, boost::posix_time::ptime& value); + /// @brief Fetches a JSON column as an ElementPtr. + /// + /// @param r the result set containing the query results + /// @param row the row number within the result set + /// @param col the column number within the row + /// + /// @throw DbOperationError if the value cannot be fetched or is + /// invalid. + static void getColumnValue(const PgSqlResult& r, const int row, + const size_t col, data::ElementPtr& value); + /// @brief Converts a column in a row in a result set to a binary bytes /// /// Method is used to convert columns stored as BYTEA into a buffer of diff --git a/src/lib/pgsql/tests/pgsql_basics.h b/src/lib/pgsql/tests/pgsql_basics.h index 1e0efc28ff..47ed578ee5 100644 --- a/src/lib/pgsql/tests/pgsql_basics.h +++ b/src/lib/pgsql/tests/pgsql_basics.h @@ -52,6 +52,7 @@ public: VARCHAR_COL, INET_COL, FLOAT_COL, + JSON_COL, NUM_BASIC_COLS }; /// @brief Constructor @@ -81,6 +82,7 @@ public: expectedColNames_[VARCHAR_COL] = "varchar_col"; expectedColNames_[INET_COL] = "inet_col"; expectedColNames_[FLOAT_COL] = "float_col"; + expectedColNames_[JSON_COL] = "json_col"; destroySchema(); createSchema(); @@ -129,7 +131,8 @@ public: " timestamp_col TIMESTAMP WITH TIME ZONE, " " varchar_col VARCHAR(255), " " inet_col INET, " - " float_col FLOAT " + " float_col FLOAT, " + " json_col JSON " "); "; PgSqlResult r(PQexec(*conn_, sql)); @@ -213,7 +216,7 @@ 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, inet_col, float_col" + " varchar_col, inet_col, float_col, json_col" " FROM basics"; runSql(r, sql, PGRES_TUPLES_OK, line); diff --git a/src/lib/pgsql/tests/pgsql_exchange_unittest.cc b/src/lib/pgsql/tests/pgsql_exchange_unittest.cc index 906b9c8d8c..f3e98c3abb 100644 --- a/src/lib/pgsql/tests/pgsql_exchange_unittest.cc +++ b/src/lib/pgsql/tests/pgsql_exchange_unittest.cc @@ -20,6 +20,7 @@ #include using namespace isc; +using namespace isc::data; using namespace isc::db; using namespace isc::db::test; using namespace isc::util; @@ -81,6 +82,22 @@ TEST(PsqlBindArray, addDataTest) { // Add an empty string. b.add(std::string("")); + + // Add a v4 address. + asiolink::IOAddress addr4("192.168.1.1"); + b.addInet4(addr4); + + // Add a v6 address. + asiolink::IOAddress addr6("3001::1"); + b.addInet6(addr6); + + // Add a double. Not sure how portably reliable this test will be. + double dbl = 2.0; + b.add(dbl); + + // Add a JSON. + ElementPtr elems = Element::fromJSON("{ \"foo\": \"bar\" }"); + b.add(elems); } // We've left bind scope, everything should be intact. @@ -96,7 +113,11 @@ TEST(PsqlBindArray, addDataTest) { "8 : \"3221360418\"\n" "9 : \"3001::1\"\n" "10 : 0x0102030405060708090a\n" - "11 : empty\n"; + "11 : empty\n" + "12 : \"192.168.1.1\"\n" + "13 : \"3001::1\"\n" + "14 : \"2\"\n" + "15 : \"{ \"foo\": \"bar\" }\"\n"; EXPECT_EQ(expected, b.toText()); } @@ -165,7 +186,6 @@ TEST(PsqlBindArray, addOptionalString) { EXPECT_EQ(expected, b.toText()); } - /// @brief Verifies the ability to add Optional booleans to /// the bind array. TEST(PsqlBindArray, addOptionalBool) { @@ -1202,4 +1222,59 @@ TEST_F(PgSqlBasicsTest, floatTest) { ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, FLOAT_COL)); } +/// @brief Verify that we can read and write JSON columns. +TEST_F(PgSqlBasicsTest, jsonTest) { + // Create a prepared statement for inserting a SMALLINT + const char* st_name = "json_insert"; + PgSqlTaggedStatement statement[] = { + { 1, { OID_TEXT }, st_name, + "INSERT INTO BASICS (json_col) values (cast($1 as json))" } + }; + + ASSERT_NO_THROW_LOG(conn_->prepareStatement(statement[0])); + + // Build our reference list of reference values + std::vectorelem_ptrs; + ASSERT_NO_THROW_LOG(elem_ptrs.push_back(Element::fromJSON("{ \"one\":1 }"))); + ASSERT_NO_THROW_LOG(elem_ptrs.push_back(Element::fromJSON("{ \"two\":\"two\" }"))); + + // Insert a row for each reference value + PsqlBindArrayPtr bind_array; + PgSqlResultPtr r; + for (int i = 0; i < elem_ptrs.size(); ++i) { + bind_array.reset(new PsqlBindArray()); + bind_array->add(elem_ptrs[i]); + RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK); + } + + // Fetch the newly inserted rows. + FETCH_ROWS(r, elem_ptrs.size()); + + // Iterate over the rows, verifying each value against its reference + int row = 0; + for ( ; row < elem_ptrs.size(); ++row ) { + // Verify the column is not null. + ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, JSON_COL)); + + // Fetch and verify the column value + ElementPtr fetched_ptr; + ASSERT_NO_THROW_LOG(PgSqlExchange::getColumnValue(*r, row, JSON_COL, fetched_ptr)); + EXPECT_TRUE(fetched_ptr->equals(*(elem_ptrs[row]))); + } + + // Clean out the table + WIPE_ROWS(r); + + // Verify we can insert a NULL value. + bind_array.reset(new PsqlBindArray()); + bind_array->addNull(); + RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK); + + // Fetch the newly inserted row. + FETCH_ROWS(r, 1); + + // Verify the column is null. + ASSERT_TRUE(PgSqlExchange::isColumnNull(*r, 0, JSON_COL)); +} + }; // namespace