]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#2183] Support postgresql inet data type
authorThomas Markwalder <tmark@isc.org>
Tue, 14 Dec 2021 19:09:18 +0000 (14:09 -0500)
committerThomas Markwalder <tmark@isc.org>
Tue, 21 Dec 2021 18:30:14 +0000 (13:30 -0500)
src/lib/pgsql/pgsql_exchange.*
    Added functions to support PostgreSQL inet data types:
    PsqlBindArray::addInet4()
    PsqlBindArray::addOptionalInet4()
    PsqlBindArray::addInet6()
    PsqlBindArray::addOptionalInet6()
    PgSqlExchange::getInetValue4()
    PgSqlExchange::getInetValue6()

src/lib/pgsql/tests/pgsql_basics.h
src/lib/pgsql/tests/pgsql_exchange_unittest.cc
    Updated tests

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 87b2aad2d18155eee499ed96602baa49ace8390b..8f62158dea0c11087bc0dac298123964dc6b6b6b 100644 (file)
@@ -43,8 +43,8 @@ void PsqlBindArray::add(const std::string& value) {
 }
 
 void PsqlBindArray::insert(const char* value, size_t index) {
-    if (index >= values_.size()) {
-        isc_throw(OutOfRange, "PsqlBindArray::insert - index: " << index 
+    if (index && index >= values_.size()) {
+        isc_throw(OutOfRange, "PsqlBindArray::insert - index: " << index
                   << ", is larger than the array size: " << values_.size());
     }
 
@@ -55,7 +55,7 @@ void PsqlBindArray::insert(const char* value, size_t index) {
 
 void PsqlBindArray::insert(const std::string& value, size_t index) {
     if (index >= values_.size()) {
-        isc_throw(OutOfRange, "PsqlBindArray::insert - index: " << index 
+        isc_throw(OutOfRange, "PsqlBindArray::insert - index: " << index
                   << ", is larger than the array size: " << values_.size());
     }
 
@@ -162,18 +162,44 @@ PsqlBindArray::addOptionalBool(const util::Optional<bool>& value) {
 }
 
 void
-PsqlBindArray::addOptionalIPv4Address(const util::Optional<isc::asiolink::IOAddress>& value) {
+PsqlBindArray::addInet4(const isc::asiolink::IOAddress& value) {
+    if (!value.isV4()) {
+        isc_throw(BadValue, "unable to add address to PsqlBindAray '"
+                  << value.toText() << "' is not an IPv4 address");
+    }
+
+    // inet columns are inserted as string addresses.
+    addTempString(value.toText());
+}
+
+void
+PsqlBindArray::addOptionalInet4(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");
-        }
+        addInet4(value);
+    }
+}
+
+void
+PsqlBindArray::addInet6(const isc::asiolink::IOAddress& value) {
+    if (!value.isV6()) {
+        isc_throw(BadValue, "unable to add address to PsqlBindAray '"
+                  << value.toText() << "' is not an IPv6 address");
+    }
 
-        add((value.get().toUint32()));
+    // inet columns are inserted as string addresses.
+    addTempString(value.toText());
+}
+
+void
+PsqlBindArray::addOptionalInet6(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 {
+        addInet6(value);
     }
 }
 
@@ -191,6 +217,7 @@ PsqlBindArray::addTimestamp() {
     addTempString(PgSqlExchange::convertToDatabaseTime(now));
 }
 
+
 std::string
 PsqlBindArray::toText() const {
     std::ostringstream stream;
@@ -348,6 +375,43 @@ PgSqlExchange::getColumnValue(const PgSqlResult& r, const int row,
     PgSqlExchange::convertFromDatabaseTime(db_time_val, value);
 }
 
+isc::asiolink::IOAddress
+PgSqlExchange::getInetValue4(const PgSqlResult& r, const int row,
+                            const size_t col) {
+    const char* data = getRawColumnValue(r, row, col);
+    try {
+        asiolink::IOAddress addr(data);
+        if (!addr.isV4()) {
+            isc_throw(BadValue, "not a v4 address");
+        }
+
+        return(addr);
+    } catch (const std::exception& ex) {
+        isc_throw(DbOperationError, "Cannot convert data: " << data
+                  << " for: " << getColumnLabel(r, col) << " row:" << row
+                  << " : " << ex.what());
+    }
+}
+
+
+isc::asiolink::IOAddress
+PgSqlExchange::getInetValue6(const PgSqlResult& r, const int row,
+                            const size_t col) {
+    const char* data = getRawColumnValue(r, row, col);
+    try {
+        asiolink::IOAddress addr(data);
+        if (!addr.isV6()) {
+            isc_throw(BadValue, "not a v6 address");
+        }
+
+        return(addr);
+    } catch (const std::exception& ex) {
+        isc_throw(DbOperationError, "Cannot convert data: " << data
+                  << " for: " << getColumnLabel(r, col) << " row:" << row
+                  << " : " << ex.what());
+    }
+}
+
 isc::asiolink::IOAddress
 PgSqlExchange::getIPv6Value(const PgSqlResult& r, const int row,
                             const size_t col) {
index b6dffeb0b3bf64d5343da4ae86aaff10b7879613..53650c364583bbbfe95c5aee5dc0c108cda567fc 100644 (file)
@@ -359,11 +359,40 @@ struct PsqlBindArray {
         }
     }
 
+    /// @brief Adds an IPv4 address to the bind array.
+    ///
+    /// This is used for inet type columns.
+    ///
+    /// @param value Optional boolean value to add
+    /// @throw BadValue if the address is not a IPv4 address.
+    void addInet4(const isc::asiolink::IOAddress& value);
+
+    /// @brief Adds an IPv6 address to the bind array.
+    ///
+    /// This is used for inet type columns.
+    ///
+    /// @param value Optional boolean value to add
+    /// @throw BadValue if the address is not a IPv6 address.
+    void addInet6(const isc::asiolink::IOAddress& value);
+
     /// @brief Adds an @c Optional IPv4 address to the bind array.
     ///
+    /// This is used for inet type columns.
+    ///
     /// @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);
+    void addOptionalInet4(const util::Optional<isc::asiolink::IOAddress>& value);
+
+    /// @brief Adds an @c Optional IPv6 address to the bind array.
+    ///
+    /// This is used for inet type columns which expect
+    /// v4 addresses to be inserted in string form:
+    /// '3001::1'
+    ///
+    /// @param value Optional boolean value to add
+    /// @throw BadValue if the address is not a IPv6 address.
+    void addOptionalInet6(const util::Optional<isc::asiolink::IOAddress>& value);
+
 
     /// @brief Adds a timestamp from a ptime to the bind array.
     ///
@@ -564,8 +593,40 @@ public:
     static void getColumnValue(const PgSqlResult& r, const int row,
                                const size_t col, uint8_t &value);
 
+    /// @brief Converts a column in a row in a result set into IPv4 address.
+    ///
+    /// This is used to fetch values from inet type columns.
+    ///
+    /// @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
+    ///
+    /// @return isc::asiolink::IOAddress containing the IPv4 address.
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    static isc::asiolink::IOAddress getInetValue4(const PgSqlResult& r,
+                                                  const int row,
+                                                  const size_t col);
+
+    /// @brief Converts a column in a row in a result set into IPv6 address.
+    ///
+    /// This is used to fetch values from inet type columns.
+    ///
+    /// @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
+    ///
+    /// @return isc::asiolink::IOAddress containing the IPv6 address.
+    /// @throw  DbOperationError if the value cannot be fetched or is
+    /// invalid.
+    static isc::asiolink::IOAddress getInetValue6(const PgSqlResult& r,
+                                                  const int row,
+                                                  const size_t col);
+
     /// @brief Converts a column in a row in a result set into IPv6 address.
     ///
+    /// This used for IPv6 columns stored as varchar.
+    ///
     /// @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
index 8e3fa6fa651f87e59106f03c02ed28237212ea97..3ea6a9d5b33276f30543b5181a0c668bdb13afca 100644 (file)
@@ -50,6 +50,7 @@ public:
         TEXT_COL,
         TIMESTAMP_COL,
         VARCHAR_COL,
+        INET_COL,
         NUM_BASIC_COLS
     };
     /// @brief Constructor
@@ -77,6 +78,7 @@ public:
         expectedColNames_[TEXT_COL] = "text_col";
         expectedColNames_[TIMESTAMP_COL] = "timestamp_col";
         expectedColNames_[VARCHAR_COL] = "varchar_col";
+        expectedColNames_[INET_COL] = "inet_col";
 
         destroySchema();
         createSchema();
@@ -123,7 +125,8 @@ public:
             "    int_col INT, "
             "    text_col TEXT, "
             "    timestamp_col TIMESTAMP WITH TIME ZONE, "
-            "    varchar_col VARCHAR(255) "
+            "    varchar_col VARCHAR(255), "
+            "    inet_col INET "
             "); ";
 
         PgSqlResult r(PQexec(*conn_, sql));
@@ -207,7 +210,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"
+            "   varchar_col, inet_col"
             " FROM basics";
 
         runSql(r, sql, PGRES_TUPLES_OK, line);
index 06371d0cf531bba97bdc30205fb89051875c8026..96775de8eaa5b9b08dbda29bbb8416a3862451c6 100644 (file)
@@ -228,7 +228,7 @@ TEST(PsqlBindArray, addOptionalInteger) {
 
 /// @brief Verifies the ability to add Optional IPv4 addresses to
 /// the bind array.
-TEST(PsqlBindArray, addOptionalIPv4Address) {
+TEST(PsqlBindArray, addOptionalInet4) {
 
     PsqlBindArray b;
 
@@ -240,13 +240,13 @@ TEST(PsqlBindArray, addOptionalIPv4Address) {
 
         // Verify we cannot add a v6 address.
         Optional<asiolink::IOAddress> not_v4(asiolink::IOAddress("3001::1"));
-        ASSERT_THROW_MSG(b.addOptionalIPv4Address(not_v4), BadValue,
+        ASSERT_THROW_MSG(b.addOptionalInet4(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);
+        // Add addresses to bind array.
+        b.addOptionalInet4(empty);
+        b.addOptionalInet4(not_empty);
     }
 
     // We've left bind scope, everything should be intact.
@@ -260,6 +260,40 @@ TEST(PsqlBindArray, addOptionalIPv4Address) {
     EXPECT_EQ(expected, b.toText());
 }
 
+/// @brief Verifies the ability to add Optional IPv6 addresses to
+/// the bind array.
+TEST(PsqlBindArray, addOptionalInet6) {
+
+    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("3001::1"));
+
+        // Verify we cannot add a v6 address.
+        Optional<asiolink::IOAddress> not_v6(asiolink::IOAddress("192.168.1.1"));
+        ASSERT_THROW_MSG(b.addOptionalInet6(not_v6), BadValue,
+                         "unable to add address to PsqlBindAray"
+                         " '192.168.1.1' is not an IPv6 address");
+
+        // Add addresses to bind array.
+        b.addOptionalInet6(empty);
+        b.addOptionalInet6(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 : \"3001::1\"\n";
+
+    EXPECT_EQ(expected, b.toText());
+}
+
 /// @brief Verifies that PgResultSet row and column meta-data is correct
 TEST_F(PgSqlBasicsTest, rowColumnBasics) {
     // We fetch the table contents, which at this point should be no rows.
@@ -947,21 +981,30 @@ TEST_F(PgSqlBasicsTest, ptimeTimestamp) {
 TEST(PsqlBindArray, insertString) {
     PsqlBindArray b;
 
-    // Make not temporary strings to insert.
+    // Make a non-temporary string to insert.
     std::string one("one");
-//    std::string three("three");
 
     // Add all the items within a different scope. Everything should
     // still be valid once we exit this scope.
     {
-        b.add("two");
+        // Make sure you can "insert" at the front of an empty array.
+        b.insert("two", 0);
+
+        // Add a binding.
         b.add("four");
 
-        ASSERT_THROW_MSG(b.insert(std::string("too far"), 5), OutOfRange, 
+        // Verify an out of range index throws.
+        ASSERT_THROW_MSG(b.insert(std::string("too far"), 5), OutOfRange,
                          "PsqlBindArray::insert - index: 5, "
                          "is larger than the array size: 2");
+
+        // Insert a non-temporary string at the front.
         b.insert(one, 0);
+
+        // Insert a temporary string.
         b.insert("three", 2);
+
+        // Add another one.
         b.add("five");
     }
 
@@ -979,4 +1022,118 @@ TEST(PsqlBindArray, insertString) {
     EXPECT_EQ(expected, b.toText());
 }
 
+/// @brief Verify that we can read and write IPv4 addresses
+/// using INET columns.
+TEST_F(PgSqlBasicsTest, inetTest4) {
+    // Create a prepared statement for inserting a SMALLINT
+    const char* st_name = "smallint_insert";
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_TEXT }, st_name,
+          "INSERT INTO BASICS (inet_col) values (cast($1 as inet))" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Build our reference list of reference values
+    std::vector<asiolink::IOAddress>inets;
+    inets.push_back(asiolink::IOAddress("0.0.0.0"));
+    inets.push_back(asiolink::IOAddress("192.168.1.1"));
+
+    // Insert a row for each reference value
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    for (int i = 0; i < inets.size(); ++i) {
+        bind_array.reset(new PsqlBindArray());
+        bind_array->addInet4(inets[i]);
+        RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+    }
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, inets.size());
+
+    // Iterate over the rows, verifying each value against its reference
+    int row = 0;
+    for ( ; row  < inets.size(); ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, INET_COL));
+
+        // Fetch and verify the column value
+        asiolink::IOAddress fetched_inet("0.0.0.0");
+        ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue4(*r, row, INET_COL));
+        EXPECT_EQ(fetched_inet, inets[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, INET_COL));
+}
+
+/// @brief Verify that we can read and write IPv6 addresses
+/// using INET columns.
+TEST_F(PgSqlBasicsTest, inetTest6) {
+    // Create a prepared statement for inserting a SMALLINT
+    const char* st_name = "smallint_insert";
+    PgSqlTaggedStatement statement[] = {
+        { 1, { OID_TEXT }, st_name,
+          "INSERT INTO BASICS (inet_col) values (cast($1 as inet))" }
+    };
+
+    ASSERT_NO_THROW(conn_->prepareStatement(statement[0]));
+
+    // Build our reference list of reference values
+    std::vector<asiolink::IOAddress>inets;
+    inets.push_back(asiolink::IOAddress("::"));
+    inets.push_back(asiolink::IOAddress("3001::1"));
+
+    // Insert a row for each reference value
+    PsqlBindArrayPtr bind_array;
+    PgSqlResultPtr r;
+    for (int i = 0; i < inets.size(); ++i) {
+        bind_array.reset(new PsqlBindArray());
+        bind_array->addInet6(inets[i]);
+        RUN_PREP(r, statement[0], bind_array, PGRES_COMMAND_OK);
+    }
+
+    // Fetch the newly inserted rows.
+    FETCH_ROWS(r, inets.size());
+
+    // Iterate over the rows, verifying each value against its reference
+    int row = 0;
+    for ( ; row  < inets.size(); ++row ) {
+        // Verify the column is not null.
+        ASSERT_FALSE(PgSqlExchange::isColumnNull(*r, row, INET_COL));
+
+        // Fetch and verify the column value
+        asiolink::IOAddress fetched_inet("::");
+        ASSERT_NO_THROW(fetched_inet = PgSqlExchange::getInetValue6(*r, row, INET_COL));
+        EXPECT_EQ(fetched_inet, inets[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, INET_COL));
+}
+
+
+
 }; // namespace