Allow fuzzy string comparisons in $filter expr
Filter allows comparing certain strings as numeric greater than or less
than operators. The most obvious example of this is something like
\$filter=Created gt <timestamp>
Because internally timestamps are treated as strings, this requires
including and parsing out the timestamps again, which we have utilities
for.
In addition, "fuzzy" string comparisons, like
GPU_2 gt GPU_1
Should also be supported.
Tested: Unit tests pass
Change-Id: I39fc543921ed8cc93664d9cf297dad8ee902b68f
Signed-off-by: Ed Tanous <ed@tanous.net>
diff --git a/redfish-core/src/filter_expr_executor.cpp b/redfish-core/src/filter_expr_executor.cpp
index 291b1e4..70046f0 100644
--- a/redfish-core/src/filter_expr_executor.cpp
+++ b/redfish-core/src/filter_expr_executor.cpp
@@ -1,7 +1,9 @@
#include "filter_expr_executor.hpp"
#include "filter_expr_parser_ast.hpp"
+#include "human_sort.hpp"
#include "logging.hpp"
+#include "utils/time_utils.hpp"
namespace redfish
{
@@ -9,12 +11,83 @@
namespace
{
+// A value that has been parsed as a time string per Edm.DateTimeOffset
+struct DateTimeString
+{
+ time_utils::usSinceEpoch value = time_utils::usSinceEpoch::zero();
+
+ // The following is created by dumping all key names of type
+ // Edm.DateTimeOffset. While imperfect that it's a hardcoded list, these
+ // keys don't change that often
+ static constexpr auto timeKeys =
+ std::to_array<std::string_view>({"AccountExpiration",
+ "CalibrationTime",
+ "CoefficientUpdateTime",
+ "Created",
+ "CreatedDate",
+ "CreatedTime",
+ "CreateTime",
+ "DateTime",
+ "EndDateTime",
+ "EndTime",
+ "EventTimestamp",
+ "ExpirationDate",
+ "FirstOverflowTimestamp",
+ "InitialStartTime",
+ "InstallDate",
+ "LastOverflowTimestamp",
+ "LastResetTime",
+ "LastStateTime",
+ "LastUpdated",
+ "LifetimeStartDateTime",
+ "LowestReadingTime",
+ "MaintenanceWindowStartTime",
+ "Modified",
+ "PasswordExpiration",
+ "PeakReadingTime",
+ "PresentedPublicHostKeyTimestamp",
+ "ProductionDate",
+ "ReadingTime",
+ "ReleaseDate",
+ "ReservationTime",
+ "SensorResetTime",
+ "ServicedDate",
+ "SetPointUpdateTime",
+ "StartDateTime",
+ "StartTime",
+ "Time",
+ "Timestamp",
+ "ValidNotAfter",
+ "ValidNotBefore"});
+
+ explicit DateTimeString(std::string_view strvalue)
+ {
+ std::optional<time_utils::usSinceEpoch> out =
+ time_utils::dateStringToEpoch(strvalue);
+ if (!out)
+ {
+ BMCWEB_LOG_ERROR(
+ "Internal datetime value didn't parse as datetime?");
+ }
+ else
+ {
+ value = *out;
+ }
+ }
+
+ static bool isDateTimeKey(std::string_view key)
+ {
+ auto out = std::equal_range(timeKeys.begin(), timeKeys.end(), key);
+ return out.first != out.second;
+ }
+};
+
// Class that can convert an arbitrary AST type into a structured value
// Pulling from the json pointer when required
struct ValueVisitor
{
- using result_type =
- std::variant<std::monostate, double, int64_t, std::string>;
+ using result_type = std::variant<std::monostate, double, int64_t,
+ std::string, DateTimeString>;
nlohmann::json& body;
result_type operator()(double n);
result_type operator()(int64_t x);
@@ -63,6 +136,10 @@
const std::string* strValue = entry->get_ptr<const std::string*>();
if (strValue != nullptr)
{
+ if (DateTimeString::isDateTimeKey(x))
+ {
+ return DateTimeString(*strValue);
+ }
return {*strValue};
}
@@ -166,6 +243,14 @@
return left == right;
case filter_ast::ComparisonOpEnum::NotEquals:
return left != right;
+ case filter_ast::ComparisonOpEnum::GreaterThan:
+ return alphanumComp(left, right) > 0;
+ case filter_ast::ComparisonOpEnum::GreaterThanOrEqual:
+ return alphanumComp(left, right) >= 0;
+ case filter_ast::ComparisonOpEnum::LessThan:
+ return alphanumComp(left, right) < 0;
+ case filter_ast::ComparisonOpEnum::LessThanOrEqual:
+ return alphanumComp(left, right) <= 0;
default:
BMCWEB_LOG_ERROR(
"Got comparator that should never happen. Attempt to do numeric comparison on string {}",
@@ -177,10 +262,10 @@
bool ApplyFilter::operator()(const filter_ast::Comparison& x)
{
ValueVisitor numeric(body);
- std::variant<std::monostate, double, int64_t, std::string> left =
- boost::apply_visitor(numeric, x.left);
- std::variant<std::monostate, double, int64_t, std::string> right =
- boost::apply_visitor(numeric, x.right);
+ std::variant<std::monostate, double, int64_t, std::string, DateTimeString>
+ left = boost::apply_visitor(numeric, x.left);
+ std::variant<std::monostate, double, int64_t, std::string, DateTimeString>
+ right = boost::apply_visitor(numeric, x.right);
// Numeric comparisons
const double* lDoubleValue = std::get_if<double>(&left);
@@ -221,6 +306,27 @@
// String comparisons
const std::string* lStrValue = std::get_if<std::string>(&left);
const std::string* rStrValue = std::get_if<std::string>(&right);
+
+ const DateTimeString* lDateValue = std::get_if<DateTimeString>(&left);
+ const DateTimeString* rDateValue = std::get_if<DateTimeString>(&right);
+
+ // If we're trying to compare a date string to a string, construct a
+ // datestring from the string
+ if (lDateValue != nullptr && rStrValue != nullptr)
+ {
+ rDateValue = &right.emplace<DateTimeString>(std::string(*rStrValue));
+ }
+ if (lStrValue != nullptr && rDateValue != nullptr)
+ {
+ lDateValue = &left.emplace<DateTimeString>(std::string(*lStrValue));
+ }
+
+ if (lDateValue != nullptr && rDateValue != nullptr)
+ {
+ return doIntComparison(lDateValue->value.count(), x.token,
+ rDateValue->value.count());
+ }
+
if (lStrValue != nullptr && rStrValue != nullptr)
{
return doStringComparison(*lStrValue, x.token, *rStrValue);
diff --git a/test/redfish-core/include/filter_expr_executor_test.cpp b/test/redfish-core/include/filter_expr_executor_test.cpp
index adc5748..0761668 100644
--- a/test/redfish-core/include/filter_expr_executor_test.cpp
+++ b/test/redfish-core/include/filter_expr_executor_test.cpp
@@ -147,22 +147,104 @@
TEST(FilterParser, String)
{
const nlohmann::json members =
- R"({"Members": [{"SerialNumber": "Foo"}]})"_json;
+ R"({"Members": [{"SerialNumber": "1234"}]})"_json;
// Forward true conditions
- filterTrue("SerialNumber eq 'Foo'", members);
+ filterTrue("SerialNumber eq '1234'", members);
filterTrue("SerialNumber ne 'NotFoo'", members);
+ filterTrue("SerialNumber gt '1233'", members);
+ filterTrue("SerialNumber ge '1234'", members);
+ filterTrue("SerialNumber lt '1235'", members);
+ filterTrue("SerialNumber le '1234'", members);
// Reverse true conditions
- filterTrue("'Foo' eq SerialNumber", members);
+ filterTrue("'1234' eq SerialNumber", members);
filterTrue("'NotFoo' ne SerialNumber", members);
+ filterTrue("'1235' gt SerialNumber", members);
+ filterTrue("'1234' ge SerialNumber", members);
+ filterTrue("'1233' lt SerialNumber", members);
+ filterTrue("'1234' le SerialNumber", members);
// Forward false conditions
filterFalse("SerialNumber eq 'NotFoo'", members);
- filterFalse("SerialNumber ne 'Foo'", members);
+ filterFalse("SerialNumber ne '1234'", members);
+ filterFalse("SerialNumber gt '1234'", members);
+ filterFalse("SerialNumber ge '1235'", members);
+ filterFalse("SerialNumber lt '1234'", members);
+ filterFalse("SerialNumber le '1233'", members);
// Reverse false conditions
filterFalse("'NotFoo' eq SerialNumber", members);
- filterFalse("'Foo' ne SerialNumber", members);
+ filterFalse("'1234' ne SerialNumber", members);
+ filterFalse("'1234' gt SerialNumber", members);
+ filterFalse("'1233' ge SerialNumber", members);
+ filterFalse("'1234' lt SerialNumber", members);
+ filterFalse("'1235' le SerialNumber", members);
+}
+
+TEST(FilterParser, StringHuman)
+{
+ // Ensure that we're sorting based on human facing numbers, not
+ // lexicographic comparison
+
+ const nlohmann::json members = R"({"Members": [{}]})"_json;
+ // Forward true conditions
+ filterFalse("'20' eq '3'", members);
+ filterTrue("'20' ne '3'", members);
+ filterTrue("'20' gt '3'", members);
+ filterTrue("'20' ge '3'", members);
+ filterFalse("'20' lt '3'", members);
+ filterFalse("'20' le '3'", members);
+}
+
+TEST(FilterParser, StringSemver)
+{
+ const nlohmann::json members =
+ R"({"Members": [{"Version": "20.0.2"}]})"_json;
+ // Forward true conditions
+ filterTrue("Version eq '20.0.2'", members);
+ filterTrue("Version ne '20.2.0'", members);
+ filterTrue("Version gt '20.0.1'", members);
+ filterTrue("Version gt '1.9.9'", members);
+ filterTrue("Version gt '10.9.9'", members);
+}
+
+TEST(FilterParser, Dates)
+{
+ const nlohmann::json members =
+ R"({"Members": [{"Created": "2021-11-30T22:41:35.123+00:00"}]})"_json;
+
+ // Note, all comparisons below differ by a single millisecond
+ // Forward true conditions
+ filterTrue("Created eq '2021-11-30T22:41:35.123+00:00'", members);
+ filterTrue("Created ne '2021-11-30T22:41:35.122+00:00'", members);
+ filterTrue("Created gt '2021-11-30T22:41:35.122+00:00'", members);
+ filterTrue("Created ge '2021-11-30T22:41:35.123+00:00'", members);
+ filterTrue("Created lt '2021-11-30T22:41:35.124+00:00'", members);
+ filterTrue("Created le '2021-11-30T22:41:35.123+00:00'", members);
+
+ // Reverse true conditions
+ filterTrue("'2021-11-30T22:41:35.123+00:00' eq Created", members);
+ filterTrue("'2021-11-30T22:41:35.122+00:00' ne Created", members);
+ filterTrue("'2021-11-30T22:41:35.124+00:00' gt Created", members);
+ filterTrue("'2021-11-30T22:41:35.123+00:00' ge Created", members);
+ filterTrue("'2021-11-30T22:41:35.122+00:00' lt Created", members);
+ filterTrue("'2021-11-30T22:41:35.123+00:00' le Created", members);
+
+ // Forward false conditions
+ filterFalse("Created eq '2021-11-30T22:41:35.122+00:00'", members);
+ filterFalse("Created ne '2021-11-30T22:41:35.123+00:00'", members);
+ filterFalse("Created gt '2021-11-30T22:41:35.123+00:00'", members);
+ filterFalse("Created ge '2021-11-30T22:41:35.124+00:00'", members);
+ filterFalse("Created lt '2021-11-30T22:41:35.123+00:00'", members);
+ filterFalse("Created le '2021-11-30T22:41:35.122+00:00'", members);
+
+ // Reverse false conditions
+ filterFalse("'2021-11-30T22:41:35.122+00:00' eq Created", members);
+ filterFalse("'2021-11-30T22:41:35.123+00:00' ne Created", members);
+ filterFalse("'2021-11-30T22:41:35.123+00:00' gt Created", members);
+ filterFalse("'2021-11-30T22:41:35.122+00:00' ge Created", members);
+ filterFalse("'2021-11-30T22:41:35.123+00:00' lt Created", members);
+ filterFalse("'2021-11-30T22:41:35.124+00:00' le Created", members);
}
} // namespace redfish