From 920cfbfa998ca9d04cce5f7731aced7e950a3477 Mon Sep 17 00:00:00 2001 From: rsgowman Date: Fri, 13 Jul 2018 16:22:07 -0400 Subject: Initial (incomplete) implementation of f:f:core::Filter (#1495) And RelationFilter subclass. Used to implement the next step in core::Query. --- .../src/firebase/firestore/core/CMakeLists.txt | 4 ++ .../core/src/firebase/firestore/core/filter.cc | 41 ++++++++++++ .../core/src/firebase/firestore/core/filter.h | 70 ++++++++++++++++++++ .../core/src/firebase/firestore/core/query.cc | 24 ++++++- Firestore/core/src/firebase/firestore/core/query.h | 28 ++++++-- .../src/firebase/firestore/core/relation_filter.cc | 77 ++++++++++++++++++++++ .../src/firebase/firestore/core/relation_filter.h | 59 +++++++++++++++++ .../src/firebase/firestore/model/field_value.cc | 34 ++++------ .../src/firebase/firestore/model/field_value.h | 9 +++ .../test/firebase/firestore/core/query_test.cc | 15 +++++ .../test/firebase/firestore/testutil/testutil.h | 40 ++++++++++- 11 files changed, 372 insertions(+), 29 deletions(-) create mode 100644 Firestore/core/src/firebase/firestore/core/filter.cc create mode 100644 Firestore/core/src/firebase/firestore/core/filter.h create mode 100644 Firestore/core/src/firebase/firestore/core/relation_filter.cc create mode 100644 Firestore/core/src/firebase/firestore/core/relation_filter.h (limited to 'Firestore') diff --git a/Firestore/core/src/firebase/firestore/core/CMakeLists.txt b/Firestore/core/src/firebase/firestore/core/CMakeLists.txt index 1aab09a..c1557b2 100644 --- a/Firestore/core/src/firebase/firestore/core/CMakeLists.txt +++ b/Firestore/core/src/firebase/firestore/core/CMakeLists.txt @@ -17,10 +17,14 @@ cc_library( SOURCES database_info.cc database_info.h + filter.cc + filter.h target_id_generator.cc target_id_generator.h query.cc query.h + relation_filter.cc + relation_filter.h DEPENDS absl_strings firebase_firestore_model diff --git a/Firestore/core/src/firebase/firestore/core/filter.cc b/Firestore/core/src/firebase/firestore/core/filter.cc new file mode 100644 index 0000000..4c27f27 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/filter.cc @@ -0,0 +1,41 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/src/firebase/firestore/core/filter.h" + +#include + +#include "Firestore/core/src/firebase/firestore/core/relation_filter.h" + +namespace firebase { +namespace firestore { +namespace core { + +using model::FieldPath; +using model::FieldValue; + +std::shared_ptr Filter::Create(FieldPath path, + Operator op, + FieldValue value) { + // TODO(rsgowman): Java performs a number of checks here, and then invokes the + // ctor of the relevant Filter subclass. Port those checks here. + return std::make_shared(std::move(path), op, + std::move(value)); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/filter.h b/Firestore/core/src/firebase/firestore/core/filter.h new file mode 100644 index 0000000..9f25a39 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/filter.h @@ -0,0 +1,70 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_FILTER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_FILTER_H_ + +#include +#include + +#include "Firestore/core/src/firebase/firestore/model/document.h" +#include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/field_value.h" + +namespace firebase { +namespace firestore { +namespace core { + +/** Interface used for all query filters. All filters are immutable. */ +class Filter { + public: + enum class Operator { + LessThan, + LessThanOrEqual, + Equal, + GreaterThan, + GreaterThanOrEqual, + }; + + /** + * Creates a Filter instance for the provided path, operator, and value. + * + * Note that if the relational operator is Equal and the value is NullValue or + * NaN, then this will return the appropriate NullFilter or NaNFilter class + * instead of a RelationFilter. + */ + static std::shared_ptr Create(model::FieldPath path, + Operator op, + model::FieldValue value); + + virtual ~Filter() { + } + + /** Returns the field the Filter operates over. */ + virtual const model::FieldPath& field() const = 0; + + /** Returns true if a document matches the filter. */ + virtual bool Matches(const model::Document& doc) const = 0; + + /** A unique ID identifying the filter; used when serializing queries. */ + virtual std::string CanonicalId() const = 0; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_FILTER_H_ diff --git a/Firestore/core/src/firebase/firestore/core/query.cc b/Firestore/core/src/firebase/firestore/core/query.cc index ad0f7a3..0c0a0fd 100644 --- a/Firestore/core/src/firebase/firestore/core/query.cc +++ b/Firestore/core/src/firebase/firestore/core/query.cc @@ -16,8 +16,12 @@ #include "Firestore/core/src/firebase/firestore/core/query.h" +#include + #include "Firestore/core/src/firebase/firestore/model/document_key.h" +#include "Firestore/core/src/firebase/firestore/model/field_path.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" namespace firebase { namespace firestore { @@ -41,9 +45,11 @@ bool Query::MatchesPath(const Document& doc) const { } } -bool Query::MatchesFilters(const Document&) const { - // TODO(rsgowman): Implement this correctly. - return true; +bool Query::MatchesFilters(const Document& doc) const { + return std::all_of(filters_.begin(), filters_.end(), + [&](const std::shared_ptr& filter) { + return filter->Matches(doc); + }); } bool Query::MatchesOrderBy(const Document&) const { @@ -56,6 +62,18 @@ bool Query::MatchesBounds(const Document&) const { return true; } +Query Query::Filter(std::shared_ptr filter) const { + HARD_ASSERT(!DocumentKey::IsDocumentKey(path_), + "No filter is allowed for document query"); + + // TODO(rsgowman): ensure only one inequality field + // TODO(rsgowman): ensure first orderby must match inequality field + + std::vector> updated_filters = filters_; + updated_filters.push_back(std::move(filter)); + return Query(path_, std::move(updated_filters)); +} + } // namespace core } // namespace firestore } // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/query.h b/Firestore/core/src/firebase/firestore/core/query.h index 84d645d..3df1d25 100644 --- a/Firestore/core/src/firebase/firestore/core/query.h +++ b/Firestore/core/src/firebase/firestore/core/query.h @@ -17,8 +17,11 @@ #ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_QUERY_H_ #define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_QUERY_H_ +#include #include +#include +#include "Firestore/core/src/firebase/firestore/core/filter.h" #include "Firestore/core/src/firebase/firestore/model/document.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" @@ -26,7 +29,10 @@ namespace firebase { namespace firestore { namespace core { -/** Represents the internal structure of a Firestore Query. */ +/** + * Represents the internal structure of a Firestore Query. Query instances are + * immutable. + */ class Query { public: /** @@ -36,14 +42,17 @@ class Query { * @return A new instance of Query. */ static Query AtPath(model::ResourcePath path) { - return Query(std::move(path)); + return Query(std::move(path), {}); } /** Initializes a query with all of its components directly. */ - explicit Query(model::ResourcePath path /* TODO(rsgowman): other params */) - : path_(std::move(path)) { + Query(model::ResourcePath path, + std::vector> + filters /* TODO(rsgowman): other params */) + : path_(std::move(path)), filters_(std::move(filters)) { } + /** The base path of the query. */ const model::ResourcePath& path() const { return path_; } @@ -51,6 +60,11 @@ class Query { /** Returns true if the document matches the constraints of this query. */ bool Matches(const model::Document& doc) const; + /** + * Returns a copy of this Query object with the additional specified filter. + */ + Query Filter(std::shared_ptr filter) const; + private: bool MatchesPath(const model::Document& doc) const; bool MatchesFilters(const model::Document& doc) const; @@ -58,6 +72,12 @@ class Query { bool MatchesBounds(const model::Document& doc) const; const model::ResourcePath path_; + + // Filters are shared across related Query instance. i.e. when you call + // Query::Filter(f), a new Query instance is created that contains all of the + // existing filters, plus the new one. (Both Query and Filter objects are + // immutable.) Filters are not shared across unrelated Query instances. + const std::vector> filters_; }; } // namespace core diff --git a/Firestore/core/src/firebase/firestore/core/relation_filter.cc b/Firestore/core/src/firebase/firestore/core/relation_filter.cc new file mode 100644 index 0000000..a6ae916 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/relation_filter.cc @@ -0,0 +1,77 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "Firestore/core/src/firebase/firestore/core/relation_filter.h" + +#include + +#include "absl/types/optional.h" + +namespace firebase { +namespace firestore { +namespace core { + +using model::FieldPath; +using model::FieldValue; + +RelationFilter::RelationFilter(FieldPath field, Operator op, FieldValue value) + : field_(std::move(field)), op_(op), value_(std::move(value)) { +} + +const FieldPath& RelationFilter::field() const { + return field_; +} + +bool RelationFilter::Matches(const model::Document& doc) const { + if (field_.IsKeyFieldPath()) { + // TODO(rsgowman): Port this case + abort(); + } else { + absl::optional doc_field_value = doc.field(field_); + return doc_field_value && MatchesValue(doc_field_value.value()); + } +} + +bool RelationFilter::MatchesValue(const FieldValue& other) const { + // Only compare types with matching backend order (such as double and int). + return FieldValue::Comparable(value_.type(), other.type()) && + MatchesComparison(other); +} + +bool RelationFilter::MatchesComparison(const FieldValue& other) const { + switch (op_) { + case Operator::LessThan: + return value_ < other; + case Operator::LessThanOrEqual: + return value_ <= other; + case Operator::Equal: + return value_ == other; + case Operator::GreaterThan: + return value_ > other; + case Operator::GreaterThanOrEqual: + return value_ >= other; + } + UNREACHABLE(); +} + +std::string RelationFilter::CanonicalId() const { + // TODO(rsgowman): Port this + abort(); +} + +} // namespace core +} // namespace firestore +} // namespace firebase diff --git a/Firestore/core/src/firebase/firestore/core/relation_filter.h b/Firestore/core/src/firebase/firestore/core/relation_filter.h new file mode 100644 index 0000000..94da208 --- /dev/null +++ b/Firestore/core/src/firebase/firestore/core/relation_filter.h @@ -0,0 +1,59 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_RELATION_FILTER_H_ +#define FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_RELATION_FILTER_H_ + +#include + +#include "Firestore/core/src/firebase/firestore/core/filter.h" +#include "Firestore/core/src/firebase/firestore/model/document.h" +#include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/field_value.h" + +namespace firebase { +namespace firestore { +namespace core { + +/** Represents a filter to be applied to the query. */ +class RelationFilter : public Filter { + public: + /** + * Creates a new filter that compares fields and values. Only intended to be + * called from Filter::Create(). + */ + RelationFilter(model::FieldPath field, Operator op, model::FieldValue value); + + const model::FieldPath& field() const override; + + bool Matches(const model::Document& doc) const override; + + std::string CanonicalId() const override; + + private: + bool MatchesValue(const model::FieldValue& other) const; + bool MatchesComparison(const model::FieldValue& other) const; + + const model::FieldPath field_; + const Operator op_; + const model::FieldValue value_; +}; + +} // namespace core +} // namespace firestore +} // namespace firebase + +#endif // FIRESTORE_CORE_SRC_FIREBASE_FIRESTORE_CORE_RELATION_FILTER_H_ diff --git a/Firestore/core/src/firebase/firestore/model/field_value.cc b/Firestore/core/src/firebase/firestore/model/field_value.cc index 4f92c8b..d2fadc8 100644 --- a/Firestore/core/src/firebase/firestore/model/field_value.cc +++ b/Firestore/core/src/firebase/firestore/model/field_value.cc @@ -35,25 +35,6 @@ using Type = FieldValue::Type; using firebase::firestore::util::ComparisonResult; namespace { -/** - * This deviates from the other platforms that define TypeOrder. Since - * we already define Type for union types, we use it together with this - * function to achieve the equivalent order of types i.e. - * i) if two types are comparable, then they are of equal order; - * ii) otherwise, their order is the same as the order of their Type. - */ -bool Comparable(Type lhs, Type rhs) { - switch (lhs) { - case Type::Integer: - case Type::Double: - return rhs == Type::Integer || rhs == Type::Double; - case Type::Timestamp: - case Type::ServerTimestamp: - return rhs == Type::Timestamp || rhs == Type::ServerTimestamp; - default: - return lhs == rhs; - } -} // Makes a copy excluding the specified child, which is expected to be assigned // different value afterwards. @@ -165,6 +146,19 @@ FieldValue& FieldValue::operator=(FieldValue&& value) { } } +bool FieldValue::Comparable(Type lhs, Type rhs) { + switch (lhs) { + case Type::Integer: + case Type::Double: + return rhs == Type::Integer || rhs == Type::Double; + case Type::Timestamp: + case Type::ServerTimestamp: + return rhs == Type::Timestamp || rhs == Type::ServerTimestamp; + default: + return lhs == rhs; + } +} + FieldValue FieldValue::Set(const FieldPath& field_path, FieldValue value) const { HARD_ASSERT(type() == Type::Object, @@ -379,7 +373,7 @@ FieldValue FieldValue::ObjectValueFromMap(ObjectValue::Map&& value) { } bool operator<(const FieldValue& lhs, const FieldValue& rhs) { - if (!Comparable(lhs.type(), rhs.type())) { + if (!FieldValue::Comparable(lhs.type(), rhs.type())) { return lhs.type() < rhs.type(); } diff --git a/Firestore/core/src/firebase/firestore/model/field_value.h b/Firestore/core/src/firebase/firestore/model/field_value.h index 09c8531..697d5b3 100644 --- a/Firestore/core/src/firebase/firestore/model/field_value.h +++ b/Firestore/core/src/firebase/firestore/model/field_value.h @@ -108,6 +108,15 @@ class FieldValue { return tag_; } + /** + * PORTING NOTE: This deviates from the other platforms that define TypeOrder. + * Since we already define Type for union types, we use it together with this + * function to achieve the equivalent order of types i.e. + * i) if two types are comparable, then they are of equal order; + * ii) otherwise, their order is the same as the order of their Type. + */ + static bool Comparable(Type lhs, Type rhs); + bool boolean_value() const { HARD_ASSERT(tag_ == Type::Boolean); return boolean_value_; diff --git a/Firestore/core/test/firebase/firestore/core/query_test.cc b/Firestore/core/test/firebase/firestore/core/query_test.cc index 3ac22a9..6099bee 100644 --- a/Firestore/core/test/firebase/firestore/core/query_test.cc +++ b/Firestore/core/test/firebase/firestore/core/query_test.cc @@ -17,6 +17,8 @@ #include "Firestore/core/src/firebase/firestore/core/query.h" #include "Firestore/core/src/firebase/firestore/model/document.h" +#include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/test/firebase/firestore/testutil/testutil.h" #include "gtest/gtest.h" @@ -26,7 +28,9 @@ namespace firestore { namespace core { using model::Document; +using model::FieldValue; using testutil::Doc; +using testutil::Filter; TEST(QueryTest, MatchesBasedOnDocumentKey) { Document doc1 = Doc("rooms/eros/messages/1"); @@ -52,6 +56,17 @@ TEST(QueryTest, MatchesShallowAncestorQuery) { EXPECT_FALSE(query.Matches(doc3)); } +TEST(QueryTest, EmptyFieldsAreAllowedForQueries) { + Document doc1 = Doc("rooms/eros/messages/1", 0, + {{"text", FieldValue::StringValue("msg1")}}); + Document doc2 = Doc("rooms/eros/messages/2"); + + Query query = Query::AtPath({"rooms", "eros", "messages"}) + .Filter(Filter("text", "==", "msg1")); + EXPECT_TRUE(query.Matches(doc1)); + EXPECT_FALSE(query.Matches(doc2)); +} + } // namespace core } // namespace firestore } // namespace firebase diff --git a/Firestore/core/test/firebase/firestore/testutil/testutil.h b/Firestore/core/test/firebase/firestore/testutil/testutil.h index c470250..f13c1b7 100644 --- a/Firestore/core/test/firebase/firestore/testutil/testutil.h +++ b/Firestore/core/test/firebase/firestore/testutil/testutil.h @@ -19,14 +19,21 @@ #include // NOLINT(build/c++11) #include +#include +#include +#include #include "Firestore/core/include/firebase/firestore/timestamp.h" +#include "Firestore/core/src/firebase/firestore/core/relation_filter.h" #include "Firestore/core/src/firebase/firestore/model/document.h" #include "Firestore/core/src/firebase/firestore/model/document_key.h" #include "Firestore/core/src/firebase/firestore/model/field_path.h" +#include "Firestore/core/src/firebase/firestore/model/field_value.h" #include "Firestore/core/src/firebase/firestore/model/no_document.h" #include "Firestore/core/src/firebase/firestore/model/resource_path.h" #include "Firestore/core/src/firebase/firestore/model/snapshot_version.h" +#include "Firestore/core/src/firebase/firestore/util/hard_assert.h" +#include "absl/memory/memory.h" #include "absl/strings/string_view.h" namespace firebase { @@ -59,8 +66,10 @@ inline model::SnapshotVersion Version(int64_t version) { return model::SnapshotVersion{Timestamp::FromTimePoint(timepoint)}; } -inline model::Document Doc(absl::string_view key, int64_t version = 0) { - return model::Document{model::FieldValue::ObjectValueFromMap({}), Key(key), +inline model::Document Doc(absl::string_view key, + int64_t version = 0, + const model::ObjectValue::Map& data = {}) { + return model::Document{model::FieldValue::ObjectValueFromMap(data), Key(key), Version(version), /* has_local_mutations= */ false}; } @@ -69,6 +78,33 @@ inline model::NoDocument DeletedDoc(absl::string_view key, int64_t version) { return model::NoDocument{Key(key), Version(version)}; } +inline core::RelationFilter::Operator OperatorFromString(absl::string_view s) { + if (s == "<") + return core::RelationFilter::Operator::LessThan; + else if (s == "<=") + return core::RelationFilter::Operator::LessThanOrEqual; + else if (s == "==") + return core::RelationFilter::Operator::Equal; + else if (s == ">") + return core::RelationFilter::Operator::GreaterThan; + else if (s == ">=") + return core::RelationFilter::Operator::GreaterThanOrEqual; + HARD_FAIL("Unknown operator: %s", s); +} + +inline std::shared_ptr Filter(absl::string_view key, + absl::string_view op, + model::FieldValue value) { + return core::Filter::Create(Field(key), OperatorFromString(op), + std::move(value)); +} + +inline std::shared_ptr Filter(absl::string_view key, + absl::string_view op, + const std::string& value) { + return Filter(key, op, model::FieldValue::StringValue(value)); +} + // Add a non-inline function to make this library buildable. // TODO(zxu123): remove once there is non-inline function. void dummy(); -- cgit v1.2.3