Bläddra i källkod

feat(client): expose all filter operators via QueryOptions::Filter (v1.4.2)

The server already supports EQ/NE/GT/GTE/LT/LTE/IN/CONTAINS/EXISTS/REGEX/SEARCH
operators. The client library was hardcoding every filter to EQ. This exposes
the operators via a new Client::Filter struct:

  opts.filters = {
      {"status", FilterOp::NE, "deleted"},
      {"age", FilterOp::GTE, 18}
  };

Backward compatible: the 2-arg Filter constructor defaults to EQ, so existing
callers using brace-init {{"field", "value"}} continue to compile and
behave the same.

Minor behavior change: the magic '_search' field name no longer auto-selects
FILTER_OP_SEARCH. Use FilterOp::SEARCH explicitly instead.
fszontagh 3 månader sedan
förälder
incheckning
88e582562f
4 ändrade filer med 99 tillägg och 36 borttagningar
  1. 1 1
      VERSION
  2. 36 2
      client/include/smartbotic/database/client.hpp
  3. 17 31
      client/src/client.cpp
  4. 45 2
      docs/integration-guide.md

+ 1 - 1
VERSION

@@ -1 +1 @@
-1.4.1
+1.4.2

+ 36 - 2
client/include/smartbotic/database/client.hpp

@@ -148,8 +148,42 @@ public:
 
     // ===== Query Operations =====
 
+    /**
+     * Filter operator for query filters.
+     * Mirrors the server-side FilterOp enum (see database.proto).
+     */
+    enum class FilterOp {
+        EQ = 1,        // equal
+        NE = 2,        // not equal
+        GT = 3,        // greater than
+        GTE = 4,       // greater or equal
+        LT = 5,        // less than
+        LTE = 6,       // less or equal
+        IN = 7,        // value in array (value must be JSON array)
+        CONTAINS = 8,  // array contains value
+        EXISTS = 9,    // field exists (value is bool)
+        REGEX = 10,    // regex match (value is string pattern)
+        SEARCH = 11    // full-text search across ID and string fields
+    };
+
+    /**
+     * A single query filter with operator.
+     */
+    struct Filter {
+        std::string field;
+        FilterOp op = FilterOp::EQ;
+        nlohmann::json value;
+
+        Filter() = default;
+        Filter(std::string f, FilterOp o, nlohmann::json v)
+            : field(std::move(f)), op(o), value(std::move(v)) {}
+        // Convenience: equality filter
+        Filter(std::string f, nlohmann::json v)
+            : field(std::move(f)), op(FilterOp::EQ), value(std::move(v)) {}
+    };
+
     struct QueryOptions {
-        std::vector<std::pair<std::string, nlohmann::json>> filters;  // field, value pairs (EQ only)
+        std::vector<Filter> filters;
         std::string sortField;
         bool sortDescending = false;
         uint32_t limit = 100;
@@ -193,7 +227,7 @@ public:
      * Count documents matching filters.
      */
     [[nodiscard]] uint64_t count(const std::string& collection,
-                                 const std::vector<std::pair<std::string, nlohmann::json>>& filters);
+                                 const std::vector<Filter>& filters);
 
     /**
      * Count all documents in a collection.

+ 17 - 31
client/src/client.cpp

@@ -293,16 +293,11 @@ public:
         request.set_collection(collection);
 
         // Set filters
-        for (const auto& [field, value] : options.filters) {
-            auto* filter = request.add_filters();
-            filter->set_field(field);
-            filter->set_value(value.dump());
-            // Use SEARCH op for _search field, EQ for others
-            if (field == "_search") {
-                filter->set_op(smartbotic::databasepb::FILTER_OP_SEARCH);
-            } else {
-                filter->set_op(smartbotic::databasepb::FILTER_OP_EQ);
-            }
+        for (const auto& filter : options.filters) {
+            auto* pb = request.add_filters();
+            pb->set_field(filter.field);
+            pb->set_value(filter.value.dump());
+            pb->set_op(static_cast<smartbotic::databasepb::FilterOp>(filter.op));
         }
 
         // Set sorting
@@ -348,15 +343,11 @@ public:
         request.set_collection(collection);
 
         // Set filters
-        for (const auto& [field, value] : options.filters) {
-            auto* filter = request.add_filters();
-            filter->set_field(field);
-            filter->set_value(value.dump());
-            if (field == "_search") {
-                filter->set_op(smartbotic::databasepb::FILTER_OP_SEARCH);
-            } else {
-                filter->set_op(smartbotic::databasepb::FILTER_OP_EQ);
-            }
+        for (const auto& filter : options.filters) {
+            auto* pb = request.add_filters();
+            pb->set_field(filter.field);
+            pb->set_value(filter.value.dump());
+            pb->set_op(static_cast<smartbotic::databasepb::FilterOp>(filter.op));
         }
 
         // Set sorting
@@ -405,20 +396,15 @@ public:
     }
 
     uint64_t count(const std::string& collection,
-                   const std::vector<std::pair<std::string, nlohmann::json>>& filters) {
+                   const std::vector<Client::Filter>& filters) {
         smartbotic::databasepb::CountRequest request;
         request.set_collection(collection);
 
-        for (const auto& [field, value] : filters) {
-            auto* filter = request.add_filters();
-            filter->set_field(field);
-            filter->set_value(value.dump());
-            // Use SEARCH op for _search field, EQ for others
-            if (field == "_search") {
-                filter->set_op(smartbotic::databasepb::FILTER_OP_SEARCH);
-            } else {
-                filter->set_op(smartbotic::databasepb::FILTER_OP_EQ);
-            }
+        for (const auto& filter : filters) {
+            auto* pb = request.add_filters();
+            pb->set_field(filter.field);
+            pb->set_value(filter.value.dump());
+            pb->set_op(static_cast<smartbotic::databasepb::FilterOp>(filter.op));
         }
 
         smartbotic::databasepb::CountResponse response;
@@ -1201,7 +1187,7 @@ Client::FindResult Client::findWithMetrics(const std::string& collection,
 }
 
 uint64_t Client::count(const std::string& collection,
-                               const std::vector<std::pair<std::string, nlohmann::json>>& filters) {
+                               const std::vector<Filter>& filters) {
     return impl_->count(collection, filters);
 }
 

+ 45 - 2
docs/integration-guide.md

@@ -245,9 +245,12 @@ if (!db.updateIfVersion("users", "user-123", *doc, version)) {
 ### Querying
 
 ```cpp
-// Find with filters
+// Find with filters (equality is default)
 smartbotic::database::Client::QueryOptions opts;
-opts.filters = {{"status", "active"}, {"role", "admin"}};
+opts.filters = {
+    {"status", "active"},   // EQ (default) — 2-arg Filter constructor
+    {"role", "admin"},      // EQ
+};
 opts.sortField = "name";
 opts.sortDescending = false;
 opts.limit = 50;
@@ -266,6 +269,46 @@ uint64_t total = db.count("users");
 uint64_t admins = db.count("users", {{"role", "admin"}});
 ```
 
+#### Filter operators
+
+Filters support a range of operators via `Client::FilterOp`. The 2-argument
+`Filter(field, value)` constructor defaults to `EQ` — existing brace-init code
+continues to compile and behave the same. To use other operators, pass the
+operator explicitly with the 3-argument constructor:
+
+```cpp
+using Op = smartbotic::database::Client::FilterOp;
+
+smartbotic::database::Client::QueryOptions opts;
+opts.filters = {
+    {"age", Op::GTE, 18},                                             // age >= 18
+    {"status", Op::NE, "deleted"},                                    // status != "deleted"
+    {"tags", Op::CONTAINS, "admin"},                                  // tags array contains "admin"
+    {"role", Op::IN, nlohmann::json::array({"admin", "moderator"})},  // role IN [...]
+    {"email", Op::REGEX, ".*@example\\.com$"},                        // regex match
+    {"name", Op::SEARCH, "alice"},                                    // full-text search
+    {"deleted_at", Op::EXISTS, false},                                // field does NOT exist
+};
+auto docs = db.find("users", opts);
+```
+
+| Operator              | Semantics                                                      | Value type                  |
+|-----------------------|----------------------------------------------------------------|-----------------------------|
+| `FilterOp::EQ`        | field equals value                                             | any JSON                    |
+| `FilterOp::NE`        | field does not equal value                                     | any JSON                    |
+| `FilterOp::GT`        | field > value                                                  | number or string            |
+| `FilterOp::GTE`       | field >= value                                                 | number or string            |
+| `FilterOp::LT`        | field < value                                                  | number or string            |
+| `FilterOp::LTE`       | field <= value                                                 | number or string            |
+| `FilterOp::IN`        | field value appears in the supplied array                      | JSON array                  |
+| `FilterOp::CONTAINS`  | field (array) contains the supplied value                      | any JSON                    |
+| `FilterOp::EXISTS`    | field presence check (`true` = must exist, `false` = must not) | bool                        |
+| `FilterOp::REGEX`     | field matches the supplied regex pattern                       | string                      |
+| `FilterOp::SEARCH`    | full-text search across document ID and string fields          | string                      |
+
+Note: the old `_search` magic field name that auto-selected SEARCH is no
+longer special — use `FilterOp::SEARCH` explicitly instead.
+
 ### Collection Management
 
 ```cpp