diff --git a/src/query/context.hpp b/src/query/context.hpp index 3040d6e10..8dfe09ecc 100644 --- a/src/query/context.hpp +++ b/src/query/context.hpp @@ -51,6 +51,8 @@ struct EvaluationContext { /// All counters generated by `counter` function, mutable because the function /// modifies the values mutable std::unordered_map<std::string, int64_t> counters{}; + /// Property lookup cache ({symbol: {property_id: property_value, ...}, ...}) + mutable std::unordered_map<int32_t, std::map<storage::PropertyId, storage::PropertyValue>> property_lookups_cache{}; }; inline std::vector<storage::PropertyId> NamesToProperties(const std::vector<std::string> &property_names, diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp index 36c88dad1..201f4e230 100644 --- a/src/query/frontend/ast/ast.hpp +++ b/src/query/frontend/ast/ast.hpp @@ -1186,6 +1186,8 @@ class PropertyLookup : public memgraph::query::Expression { static const utils::TypeInfo kType; const utils::TypeInfo &GetTypeInfo() const override { return kType; } + enum class EvaluationMode { GET_OWN_PROPERTY, GET_ALL_PROPERTIES }; + PropertyLookup() = default; DEFVISITABLE(ExpressionVisitor<TypedValue>); @@ -1200,11 +1202,13 @@ class PropertyLookup : public memgraph::query::Expression { memgraph::query::Expression *expression_{nullptr}; memgraph::query::PropertyIx property_; + memgraph::query::PropertyLookup::EvaluationMode evaluation_mode_{EvaluationMode::GET_OWN_PROPERTY}; PropertyLookup *Clone(AstStorage *storage) const override { PropertyLookup *object = storage->Create<PropertyLookup>(); object->expression_ = expression_ ? expression_->Clone(storage) : nullptr; object->property_ = storage->GetPropertyIx(property_.name); + object->evaluation_mode_ = evaluation_mode_; return object; } diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp index cde169675..709df16c0 100644 --- a/src/query/frontend/semantic/symbol_generator.cpp +++ b/src/query/frontend/semantic/symbol_generator.cpp @@ -400,6 +400,29 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) { return true; } +bool SymbolGenerator::PostVisit(MapLiteral &map_literal) { + std::unordered_map<int32_t, PropertyLookup *> property_lookups{}; + + for (const auto &pair : map_literal.elements_) { + if (pair.second->GetTypeInfo() != PropertyLookup::kType) continue; + auto *property_lookup = static_cast<PropertyLookup *>(pair.second); + if (property_lookup->expression_->GetTypeInfo() != Identifier::kType) continue; + + auto symbol_pos = static_cast<Identifier *>(property_lookup->expression_)->symbol_pos_; + try { + auto *existing_property_lookup = property_lookups.at(symbol_pos); + // If already there (no exception), update the original and current PropertyLookups + existing_property_lookup->evaluation_mode_ = PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES; + property_lookup->evaluation_mode_ = PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES; + } catch (const std::out_of_range &) { + // Otherwise, add the PropertyLookup to the map + property_lookups.emplace(symbol_pos, property_lookup); + } + } + + return true; +} + bool SymbolGenerator::PreVisit(Aggregation &aggr) { auto &scope = scopes_.back(); // Check if the aggregation can be used in this context. This check should diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp index 5b98958c6..b2f7a928c 100644 --- a/src/query/frontend/semantic/symbol_generator.hpp +++ b/src/query/frontend/semantic/symbol_generator.hpp @@ -72,6 +72,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor { // Expressions ReturnType Visit(Identifier &) override; ReturnType Visit(PrimitiveLiteral &) override { return true; } + bool PreVisit(MapLiteral &) override { return true; } + bool PostVisit(MapLiteral &) override; ReturnType Visit(ParameterLookup &) override { return true; } bool PreVisit(Aggregation &) override; bool PostVisit(Aggregation &) override; diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp index 39e6a7c5f..47a7cb598 100644 --- a/src/query/interpret/eval.hpp +++ b/src/query/interpret/eval.hpp @@ -546,9 +546,35 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> { case TypedValue::Type::Null: return TypedValue(ctx_->memory); case TypedValue::Type::Vertex: - return TypedValue(GetProperty(expression_result_ptr->ValueVertex(), property_lookup.property_), ctx_->memory); + if (property_lookup.evaluation_mode_ == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES) { + auto symbol_pos = static_cast<Identifier *>(property_lookup.expression_)->symbol_pos_; + if (!ctx_->property_lookups_cache.contains(symbol_pos)) { + ctx_->property_lookups_cache.emplace(symbol_pos, GetAllProperties(expression_result_ptr->ValueVertex())); + } + + auto property_id = ctx_->properties[property_lookup.property_.ix]; + if (ctx_->property_lookups_cache[symbol_pos].contains(property_id)) { + return TypedValue(ctx_->property_lookups_cache[symbol_pos][property_id], ctx_->memory); + } + return TypedValue(ctx_->memory); + } else { + return TypedValue(GetProperty(expression_result_ptr->ValueVertex(), property_lookup.property_), ctx_->memory); + } case TypedValue::Type::Edge: - return TypedValue(GetProperty(expression_result_ptr->ValueEdge(), property_lookup.property_), ctx_->memory); + if (property_lookup.evaluation_mode_ == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES) { + auto symbol_pos = static_cast<Identifier *>(property_lookup.expression_)->symbol_pos_; + if (!ctx_->property_lookups_cache.contains(symbol_pos)) { + ctx_->property_lookups_cache.emplace(symbol_pos, GetAllProperties(expression_result_ptr->ValueEdge())); + } + + auto property_id = ctx_->properties[property_lookup.property_.ix]; + if (ctx_->property_lookups_cache[symbol_pos].contains(property_id)) { + return TypedValue(ctx_->property_lookups_cache[symbol_pos][property_id], ctx_->memory); + } + return TypedValue(ctx_->memory); + } else { + return TypedValue(GetProperty(expression_result_ptr->ValueEdge(), property_lookup.property_), ctx_->memory); + } case TypedValue::Type::Map: { auto &map = expression_result_ptr->ValueMap(); auto found = map.find(property_lookup.property_.name.c_str()); @@ -754,7 +780,14 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> { TypedValue Visit(MapLiteral &literal) override { TypedValue::TMap result(ctx_->memory); - for (const auto &pair : literal.elements_) result.emplace(pair.first.name, pair.second->Accept(*this)); + for (const auto &pair : literal.elements_) { + result.emplace(pair.first.name, pair.second->Accept(*this)); + } + + ctx_->property_lookups_cache.clear(); + // TODO Don’t clear the cache if there are remaining MapLiterals with PropertyLookups that read the same properties + // from the same variable (symbol & value) + return TypedValue(result, ctx_->memory); } @@ -1048,6 +1081,33 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> { } private: + template <class TRecordAccessor> + std::map<storage::PropertyId, storage::PropertyValue> GetAllProperties(const TRecordAccessor &record_accessor) { + auto maybe_props = record_accessor.Properties(view_); + if (maybe_props.HasError() && maybe_props.GetError() == storage::Error::NONEXISTENT_OBJECT) { + // This is a very nasty and temporary hack in order to make MERGE work. + // The old storage had the following logic when returning an `OLD` view: + // `return old ? old : new`. That means that if the `OLD` view didn't + // exist, it returned the NEW view. With this hack we simulate that + // behavior. + // TODO (mferencevic, teon.banek): Remove once MERGE is reimplemented. + maybe_props = record_accessor.Properties(storage::View::NEW); + } + if (maybe_props.HasError()) { + switch (maybe_props.GetError()) { + case storage::Error::DELETED_OBJECT: + throw QueryRuntimeException("Trying to get properties from a deleted object."); + case storage::Error::NONEXISTENT_OBJECT: + throw query::QueryRuntimeException("Trying to get properties from an object that doesn't exist."); + case storage::Error::SERIALIZATION_ERROR: + case storage::Error::VERTEX_HAS_EDGES: + case storage::Error::PROPERTIES_DISABLED: + throw QueryRuntimeException("Unexpected error when getting properties."); + } + } + return *maybe_props; + } + template <class TRecordAccessor> storage::PropertyValue GetProperty(const TRecordAccessor &record_accessor, PropertyIx prop) { auto maybe_prop = record_accessor.GetProperty(view_, ctx_->properties[prop.ix]); diff --git a/tests/gql_behave/tests/memgraph_V1/features/map.feature b/tests/gql_behave/tests/memgraph_V1/features/map.feature new file mode 100644 index 000000000..2b4327ce6 --- /dev/null +++ b/tests/gql_behave/tests/memgraph_V1/features/map.feature @@ -0,0 +1,167 @@ +Feature: Creating map values + + Scenario: Creating a map with multiple properties from a vertex + Given an empty graph + And having executed + """ + CREATE (:Employee {name: "Andy", surname: "Walker", age: 24, id: 1234}); + """ + When executing query: + """ + MATCH (e:Employee) RETURN {name: e.name, surname: e.surname, age: e.age} AS public_data; + """ + Then the result should be: + | public_data | + | {age: 24, name: 'Andy', surname: 'Walker'} | + + Scenario: Creating instances of a map with multiple properties from a vertex + Given an empty graph + And having executed + """ + CREATE (:Employee {name: "Andy", surname: "Walker", age: 24, id: 1234}), (:Person), (:Person); + """ + When executing query: + """ + MATCH (e:Employee), (n:Person) RETURN {name: e.name, surname: e.surname, age: e.age} AS public_data; + """ + Then the result should be: + | public_data | + | {age: 24, name: 'Andy', surname: 'Walker'} | + | {age: 24, name: 'Andy', surname: 'Walker'} | + + Scenario: Creating a map with multiple properties from each vertex + Given an empty graph + And having executed + """ + CREATE (:Cat {name: "Luigi", age: 11}), (:Dog {name: "Don", age: 10}), (:Owner {name: "Ivan"}); + """ + When executing query: + """ + MATCH (m:Cat), (n:Dog), (o:Owner) SET o += {catName: m.name, catAge: m.age, dogName: n.name, dogAge: n.age} RETURN o; + """ + Then the result should be: + | o | + | (:Owner {catAge: 11, catName: 'Luigi', dogAge: 10, dogName: 'Don', name: 'Ivan'}) | + + Scenario: Creating distinct maps with multiple properties, each from one vertex + Given an empty graph + And having executed + """ + FOREACH (i in range(1, 5) | CREATE (:Node {prop1: i, prop2: 2 * i})); + """ + When executing query: + """ + MATCH (n) RETURN {prop1: n.prop1, prop2: n.prop2} AS prop_data; + """ + Then the result should be: + | prop_data | + | {prop1: 1, prop2: 2} | + | {prop1: 2, prop2: 4} | + | {prop1: 3, prop2: 6} | + | {prop1: 4, prop2: 8} | + | {prop1: 5, prop2: 10} | + + Scenario: Creating a map with multiple properties from a vertex; one property is null + Given an empty graph + And having executed + """ + CREATE (:Employee {name: "Andy", surname: "Walker", age: 24, id: 1234}); + """ + When executing query: + """ + MATCH (e:Employee) RETURN {name: e.name, surname: e.surname, age: e.age, null_prop: e.nonexistent} AS public_data; + """ + Then the result should be: + | public_data | + | {age: 24, name: 'Andy', null_prop: null, surname: 'Walker'} | + + Scenario: Creating a map with multiple properties from an edge + Given an empty graph + And having executed + """ + CREATE (m)-[:ROUTE {km: 466, cross_border: true}]->(n); + """ + When executing query: + """ + MATCH ()-[r:ROUTE]->() RETURN {km: r.km, cross_border: r.cross_border} AS route_data; + """ + Then the result should be: + | route_data | + | {cross_border: true, km: 466} | + + Scenario: Creating instances of a map with multiple properties from an edge + Given an empty graph + And having executed + """ + CREATE (m)-[:ROUTE {km: 466, cross_border: true}]->(n), (:City), (:City); + """ + When executing query: + """ + MATCH (:City), ()-[r:ROUTE]->() RETURN {km: r.km, cross_border: r.cross_border} AS route_data; + """ + Then the result should be: + | route_data | + | {cross_border: true, km: 466} | + | {cross_border: true, km: 466} | + + Scenario: Creating a map with multiple properties from each edge + Given an empty graph + And having executed + """ + CREATE (m)-[:HIGHWAY {km: 466, cross_border: true}]->(n), (m)-[:FLIGHT {km: 350, daily: true}]->(n); + """ + When executing query: + """ + MATCH ()-[h:HIGHWAY]->(), ()-[f:FLIGHT]->() + RETURN {km_hwy: h.km, cross_border: h.cross_border, km_air: f.km, daily_flight: f.daily} AS routes_data; + """ + Then the result should be: + | routes_data | + | {cross_border: true, daily_flight: true, km_air: 350, km_hwy: 466} | + + Scenario: Creating distinct maps with multiple properties, each from one edge + Given an empty graph + And having executed + """ + MERGE (m:City) MERGE (n:Country) FOREACH (i in range(1, 5) | CREATE (m)-[:IN {prop1: i, prop2: 2 * i}]->(n)); + """ + When executing query: + """ + MATCH (m)-[r]->(n) RETURN {prop1: r.prop1, prop2: r.prop2} AS prop_data; + """ + Then the result should be: + | prop_data | + | {prop1: 1, prop2: 2} | + | {prop1: 2, prop2: 4} | + | {prop1: 3, prop2: 6} | + | {prop1: 4, prop2: 8} | + | {prop1: 5, prop2: 10} | + + Scenario: Creating a map with multiple properties from an edge; one property is null + Given an empty graph + And having executed + """ + CREATE (m)-[:ROUTE {km: 466, cross_border: true}]->(n); + """ + When executing query: + """ + MATCH ()-[r:ROUTE]->() RETURN {km: r.km, cross_border: r.cross_border, null_prop: r.nonexistent} AS route_data; + """ + Then the result should be: + | route_data | + | {cross_border: true, km: 466, null_prop: null} | + + Scenario: Creating a map with multiple properties from both a vertex and an edge + Given an empty graph + And having executed + """ + CREATE (m:City {name: "Split", highway_connected: true})-[:ROUTE {km: 466, cross_border: true}]->(n:City {name: "Ljubljana"}); + """ + When executing query: + """ + MATCH (m:City {name: "Split"})-[r:ROUTE]->() + RETURN {km: r.km, cross_border: r.cross_border, start_city: m.name, highway_connected: m.highway_connected} AS route_data; + """ + Then the result should be: + | route_data | + | {cross_border: true, highway_connected: true, km: 466, start_city: 'Split'} | diff --git a/tests/unit/query_plan_create_set_remove_delete.cpp b/tests/unit/query_plan_create_set_remove_delete.cpp index e3873b20a..cafb711db 100644 --- a/tests/unit/query_plan_create_set_remove_delete.cpp +++ b/tests/unit/query_plan_create_set_remove_delete.cpp @@ -1653,6 +1653,21 @@ TYPED_TEST(QueryPlanTest, MergeNoInput) { EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD))); } +TYPED_TEST(QueryPlanTest, SetPropertyWithCaching) { + // SET (Null).prop = 42 + auto storage_dba = this->db->Access(); + memgraph::query::DbAccessor dba(storage_dba.get()); + SymbolTable symbol_table; + auto prop = PROPERTY_PAIR(dba, "property"); + auto null = LITERAL(TypedValue()); + auto literal = LITERAL(42); + auto n_prop = PROPERTY_LOOKUP(dba, null, prop); + auto once = std::make_shared<Once>(); + auto set_op = std::make_shared<plan::SetProperty>(once, prop.second, n_prop, literal); + auto context = MakeContext(this->storage, symbol_table, &dba); + EXPECT_EQ(1, PullAll(*set_op, &context)); +} + TYPED_TEST(QueryPlanTest, SetPropertyOnNull) { // SET (Null).prop = 42 auto storage_dba = this->db->Access(); @@ -1717,6 +1732,46 @@ TYPED_TEST(QueryPlanTest, UpdateSetPropertiesFromMap) { EXPECT_EQ(*new_properties, expected_properties); } +TYPED_TEST(QueryPlanTest, SetPropertiesFromMapWithCaching) { + auto storage_dba = this->db->Access(); + memgraph::query::DbAccessor dba(storage_dba.get()); + + // Add a single vertex. ({prop1: 43, prop2: 44}) + auto vertex_accessor = dba.InsertVertex(); + auto old_value = vertex_accessor.SetProperty(dba.NameToProperty("prop1"), memgraph::storage::PropertyValue{43}); + old_value = vertex_accessor.SetProperty(dba.NameToProperty("prop2"), memgraph::storage::PropertyValue{44}); + EXPECT_EQ(old_value.HasError(), false); + EXPECT_EQ(*old_value, memgraph::storage::PropertyValue()); + dba.AdvanceCommand(); + EXPECT_EQ(1, CountIterable(dba.Vertices(memgraph::storage::View::OLD))); + + SymbolTable symbol_table; + // MATCH (n) SET n += {new_prop1: n.prop1, new_prop2: n.prop2}; + auto n = MakeScanAll(this->storage, symbol_table, "n"); + auto prop_new_prop1 = PROPERTY_PAIR(dba, "new_prop1"); + auto prop_new_prop2 = PROPERTY_PAIR(dba, "new_prop2"); + std::unordered_map<PropertyIx, Expression *> prop_map; + prop_map.emplace(this->storage.GetPropertyIx(prop_new_prop1.first), LITERAL(43)); + prop_map.emplace(this->storage.GetPropertyIx(prop_new_prop2.first), LITERAL(44)); + auto *rhs = this->storage.template Create<MapLiteral>(prop_map); + + auto op_type{plan::SetProperties::Op::UPDATE}; + auto set_op = std::make_shared<plan::SetProperties>(n.op_, n.sym_, rhs, op_type); + auto context = MakeContext(this->storage, symbol_table, &dba); + PullAll(*set_op, &context); + dba.AdvanceCommand(); + + auto new_properties = vertex_accessor.Properties(memgraph::storage::View::OLD); + std::map<memgraph::storage::PropertyId, memgraph::storage::PropertyValue> expected_properties; + expected_properties.emplace(dba.NameToProperty("prop1"), memgraph::storage::PropertyValue(43)); + expected_properties.emplace(dba.NameToProperty("prop2"), memgraph::storage::PropertyValue(44)); + expected_properties.emplace(dba.NameToProperty("new_prop1"), memgraph::storage::PropertyValue(43)); + expected_properties.emplace(dba.NameToProperty("new_prop2"), memgraph::storage::PropertyValue(44)); + EXPECT_EQ(context.evaluation_context.property_lookups_cache.size(), 0); + EXPECT_EQ(new_properties.HasError(), false); + EXPECT_EQ(*new_properties, expected_properties); +} + TYPED_TEST(QueryPlanTest, SetLabelsOnNull) { // OPTIONAL MATCH (n) SET n :label auto storage_dba = this->db->Access(); diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp index 0c75e7027..d49eb3798 100644 --- a/tests/unit/query_semantic.cpp +++ b/tests/unit/query_semantic.cpp @@ -1020,7 +1020,7 @@ TYPED_TEST(TestSymbolGenerator, MatchUnionReturnSymbols) { EXPECT_EQ(symbol_table.max_position(), 8); } -TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNameThrowSemanticExpcetion) { +TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNameThrowSemanticException) { // WITH 1 as X, 2 AS Y RETURN * UNION RETURN 3 AS Z, 4 AS Y auto ret = this->storage.template Create<Return>(); ret->body_.all_identifiers = true; @@ -1029,7 +1029,7 @@ TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNameThrowSemanticExpcetion) { EXPECT_THROW(memgraph::query::MakeSymbolTable(query), SemanticException); } -TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNumberThrowSemanticExpcetion) { +TYPED_TEST(TestSymbolGenerator, MatchUnionParameterNumberThrowSemanticException) { // WITH 1 as X, 2 AS Y RETURN * UNION RETURN 4 AS Y auto ret = this->storage.template Create<Return>(); ret->body_.all_identifiers = true; @@ -1278,3 +1278,166 @@ TYPED_TEST(TestSymbolGenerator, Subqueries) { symbol_table = MakeSymbolTable(query); ASSERT_EQ(symbol_table.max_position(), 13); } + +TYPED_TEST(TestSymbolGenerator, PropertyCachingSingleLookup) { + // WITH {icode: 0000} AS item + // RETURN {icode: item.icode} AS new_map; + + auto prop1_key = this->storage.GetPropertyIx("icode"); + auto prop1_val = PROPERTY_LOOKUP(this->dba, "item", this->dba.NameToProperty("icode")); + + auto has_properties = MAP({prop1_key, LITERAL(0000)}); + auto new_map = MAP({prop1_key, prop1_val}); + auto query = QUERY(SINGLE_QUERY(WITH(has_properties, AS("item")), RETURN(new_map, AS("new_map")))); + + memgraph::query::MakeSymbolTable(query); + + auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[prop1_key])->evaluation_mode_; + + ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY); +} + +TYPED_TEST(TestSymbolGenerator, PropertyCachingTwoSingleLookups) { + // WITH {icode: 0000} AS item1, {icode: 1111} AS item2 + // RETURN {icode1: item1.icode, icode2: item2.icode} AS new_map; + + auto in_prop1_key = this->storage.GetPropertyIx("icode"); + auto out_prop1_key = this->storage.GetPropertyIx("icode1"); + auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode")); + auto out_prop2_key = this->storage.GetPropertyIx("icode2"); + auto out_prop2_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode")); + + auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)}); + auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)}); + auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop2_key, out_prop2_val}); + auto query = QUERY( + SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map")))); + + memgraph::query::MakeSymbolTable(query); + + auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_; + auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop2_key])->evaluation_mode_; + + ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY); + ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY); +} + +TYPED_TEST(TestSymbolGenerator, PropertyCachingMultipleLookup) { + // WITH {icode: 0000, price: 10} AS item + // RETURN {icode: item.icode, price: item.price} AS new_map; + + auto prop1_key = this->storage.GetPropertyIx("icode"); + auto prop1_val = PROPERTY_LOOKUP(this->dba, "item", this->dba.NameToProperty("icode")); + auto prop2_key = this->storage.GetPropertyIx("price"); + auto prop2_val = PROPERTY_LOOKUP(this->dba, "item", this->dba.NameToProperty("price")); + + auto has_properties = MAP({prop1_key, LITERAL(0000)}, {prop2_key, LITERAL(10)}); + auto new_map = MAP({prop1_key, prop1_val}, {prop2_key, prop2_val}); + auto query = QUERY(SINGLE_QUERY(WITH(has_properties, AS("item")), RETURN(new_map, AS("new_map")))); + + memgraph::query::MakeSymbolTable(query); + + auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[prop1_key])->evaluation_mode_; + auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[prop2_key])->evaluation_mode_; + + ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); + ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); +} + +TYPED_TEST(TestSymbolGenerator, PropertyCachingTwoMultipleLookups) { + // WITH {icode: 0000, price: 10} AS item1, {icode: 1111, price: 16} AS item2 + // RETURN {icode1: item1.icode, price1: item1.price, icode2: item2.icode, price2: item2.price} AS new_map; + + auto in_prop1_key = this->storage.GetPropertyIx("icode"); + auto in_prop2_key = this->storage.GetPropertyIx("price"); + + auto out_prop1_key = this->storage.GetPropertyIx("icode1"); + auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode")); + auto out_prop2_key = this->storage.GetPropertyIx("price1"); + auto out_prop2_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("price")); + auto out_prop3_key = this->storage.GetPropertyIx("icode2"); + auto out_prop3_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode")); + auto out_prop4_key = this->storage.GetPropertyIx("price2"); + auto out_prop4_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("price")); + + auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)}, {in_prop2_key, LITERAL(10)}); + auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)}, {in_prop2_key, LITERAL(16)}); + auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop2_key, out_prop2_val}, {out_prop3_key, out_prop3_val}, + {out_prop4_key, out_prop4_val}); + auto query = QUERY( + SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map")))); + + memgraph::query::MakeSymbolTable(query); + + auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_; + auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop2_key])->evaluation_mode_; + auto prop3_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop3_key])->evaluation_mode_; + auto prop4_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop4_key])->evaluation_mode_; + + ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); + ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); + ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); + ASSERT_TRUE(prop4_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); +} + +TYPED_TEST(TestSymbolGenerator, PropertyCachingMixedLookups1) { + // WITH {icode: 0000, price: 10} AS item1, {icode: 1111, price: 16} AS item2 + // RETURN {icode1: item1.icode, price1: item1.price, icode2: item2.icode} AS new_map; + + auto in_prop1_key = this->storage.GetPropertyIx("icode"); + auto in_prop2_key = this->storage.GetPropertyIx("price"); + + auto out_prop1_key = this->storage.GetPropertyIx("icode1"); + auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode")); + auto out_prop2_key = this->storage.GetPropertyIx("price1"); + auto out_prop2_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("price")); + auto out_prop3_key = this->storage.GetPropertyIx("icode2"); + auto out_prop3_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode")); + + auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)}, {in_prop2_key, LITERAL(10)}); + auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)}, {in_prop2_key, LITERAL(16)}); + auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop2_key, out_prop2_val}, {out_prop3_key, out_prop3_val}); + auto query = QUERY( + SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map")))); + + memgraph::query::MakeSymbolTable(query); + + auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_; + auto prop2_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop2_key])->evaluation_mode_; + auto prop3_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop3_key])->evaluation_mode_; + + ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); + ASSERT_TRUE(prop2_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); + ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY); +} + +TYPED_TEST(TestSymbolGenerator, PropertyCachingMixedLookups2) { + // WITH {icode: 0000, price: 10} AS item1, {icode: 1111, price: 16} AS item2 + // RETURN {icode1: item1.icode, icode2: item2.icode, price2: item2.price} AS new_map; + + auto in_prop1_key = this->storage.GetPropertyIx("icode"); + auto in_prop2_key = this->storage.GetPropertyIx("price"); + + auto out_prop1_key = this->storage.GetPropertyIx("icode1"); + auto out_prop1_val = PROPERTY_LOOKUP(this->dba, "item1", this->dba.NameToProperty("icode")); + auto out_prop3_key = this->storage.GetPropertyIx("icode2"); + auto out_prop3_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("icode")); + auto out_prop4_key = this->storage.GetPropertyIx("price2"); + auto out_prop4_val = PROPERTY_LOOKUP(this->dba, "item2", this->dba.NameToProperty("price")); + + auto has_properties1 = MAP({in_prop1_key, LITERAL(0000)}, {in_prop2_key, LITERAL(10)}); + auto has_properties2 = MAP({in_prop1_key, LITERAL(1111)}, {in_prop2_key, LITERAL(16)}); + auto new_map = MAP({out_prop1_key, out_prop1_val}, {out_prop3_key, out_prop3_val}, {out_prop4_key, out_prop4_val}); + auto query = QUERY( + SINGLE_QUERY(WITH(has_properties1, AS("item1"), has_properties2, AS("item2")), RETURN(new_map, AS("new_map")))); + + memgraph::query::MakeSymbolTable(query); + + auto prop1_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop1_key])->evaluation_mode_; + auto prop3_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop3_key])->evaluation_mode_; + auto prop4_eval_mode = dynamic_cast<PropertyLookup *>(new_map->elements_[out_prop4_key])->evaluation_mode_; + + ASSERT_TRUE(prop1_eval_mode == PropertyLookup::EvaluationMode::GET_OWN_PROPERTY); + ASSERT_TRUE(prop3_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); + ASSERT_TRUE(prop4_eval_mode == PropertyLookup::EvaluationMode::GET_ALL_PROPERTIES); +}