From 0d9bd74a8adc6daed9e00b2d2c754323da33b562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Pu=C5=A1i=C4=87?= Date: Tue, 16 May 2023 20:05:35 +0200 Subject: [PATCH] Add support for map projection (#892) --- src/query/frontend/ast/ast.cpp | 6 + src/query/frontend/ast/ast.hpp | 89 +++++++++- src/query/frontend/ast/ast_visitor.hpp | 24 +-- .../frontend/ast/cypher_main_visitor.cpp | 36 ++++ .../frontend/ast/cypher_main_visitor.hpp | 5 + src/query/frontend/ast/pretty_print.cpp | 24 +++ .../frontend/opencypher/grammar/Cypher.g4 | 13 ++ src/query/interpret/eval.hpp | 125 +++++++++++++- src/query/plan/preprocess.hpp | 2 + src/query/plan/rule_based_planner.cpp | 9 +- src/utils/typeinfo.hpp | 2 + tests/benchmark/query/eval.cpp | 4 +- .../features/map_projection.feature | 76 +++++++++ tests/unit/cypher_main_visitor.cpp | 57 +++++++ tests/unit/query_common.hpp | 13 ++ tests/unit/query_expression_evaluator.cpp | 154 +++++++++++++++++- tests/unit/query_plan.cpp | 31 +++- tests/unit/query_pretty_print.cpp | 11 ++ tests/unit/stripped.cpp | 13 +- 19 files changed, 676 insertions(+), 18 deletions(-) create mode 100644 tests/gql_behave/tests/memgraph_V1/features/map_projection.feature diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp index 631bba687..1aed24e84 100644 --- a/src/query/frontend/ast/ast.cpp +++ b/src/query/frontend/ast/ast.cpp @@ -114,12 +114,18 @@ constexpr utils::TypeInfo query::ListLiteral::kType{utils::TypeId::AST_LIST_LITE constexpr utils::TypeInfo query::MapLiteral::kType{utils::TypeId::AST_MAP_LITERAL, "MapLiteral", &query::BaseLiteral::kType}; +constexpr utils::TypeInfo query::MapProjectionLiteral::kType{utils::TypeId::AST_MAP_PROJECTION_LITERAL, + "MapProjectionLiteral", &query::BaseLiteral::kType}; + constexpr utils::TypeInfo query::Identifier::kType{utils::TypeId::AST_IDENTIFIER, "Identifier", &query::Expression::kType}; constexpr utils::TypeInfo query::PropertyLookup::kType{utils::TypeId::AST_PROPERTY_LOOKUP, "PropertyLookup", &query::Expression::kType}; +constexpr utils::TypeInfo query::AllPropertiesLookup::kType{utils::TypeId::AST_ALL_PROPERTIES_LOOKUP, + "AllPropertiesLookup", &query::Expression::kType}; + constexpr utils::TypeInfo query::LabelsTest::kType{utils::TypeId::AST_LABELS_TEST, "LabelsTest", &query::Expression::kType}; diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp index c9f27da66..3bdf704ce 100644 --- a/src/query/frontend/ast/ast.hpp +++ b/src/query/frontend/ast/ast.hpp @@ -1063,8 +1063,9 @@ class MapLiteral : public memgraph::query::BaseLiteral { DEFVISITABLE(ExpressionVisitor); bool Accept(HierarchicalTreeVisitor &visitor) override { if (visitor.PreVisit(*this)) { - for (auto pair : elements_) + for (auto pair : elements_) { if (!pair.second->Accept(visitor)) break; + } } return visitor.PostVisit(*this); } @@ -1087,6 +1088,60 @@ class MapLiteral : public memgraph::query::BaseLiteral { friend class AstStorage; }; +struct MapProjectionData { + Expression *map_variable; + std::unordered_map elements; +}; + +class MapProjectionLiteral : public memgraph::query::BaseLiteral { + public: + static const utils::TypeInfo kType; + const utils::TypeInfo &GetTypeInfo() const override { return kType; } + + MapProjectionLiteral() = default; + + DEFVISITABLE(ExpressionVisitor); + DEFVISITABLE(ExpressionVisitor); + DEFVISITABLE(ExpressionVisitor); + bool Accept(HierarchicalTreeVisitor &visitor) override { + if (visitor.PreVisit(*this)) { + for (auto pair : elements_) { + if (!pair.second) continue; + + if (!pair.second->Accept(visitor)) break; + } + } + return visitor.PostVisit(*this); + } + + Expression *map_variable_; + std::unordered_map elements_; + + MapProjectionLiteral *Clone(AstStorage *storage) const override { + MapProjectionLiteral *object = storage->Create(); + object->map_variable_ = map_variable_; + + for (const auto &entry : elements_) { + auto key = storage->GetPropertyIx(entry.first.name); + + if (!entry.second) { + object->elements_[key] = nullptr; + continue; + } + + object->elements_[key] = entry.second->Clone(storage); + } + return object; + } + + protected: + explicit MapProjectionLiteral(Expression *map_variable, std::unordered_map &&elements) + : map_variable_(map_variable), elements_(std::move(elements)) {} + + private: + friend class AstStorage; +}; + class Identifier : public memgraph::query::Expression { public: static const utils::TypeInfo kType; @@ -1158,6 +1213,38 @@ class PropertyLookup : public memgraph::query::Expression { friend class AstStorage; }; +class AllPropertiesLookup : public memgraph::query::Expression { + public: + static const utils::TypeInfo kType; + const utils::TypeInfo &GetTypeInfo() const override { return kType; } + + AllPropertiesLookup() = default; + + DEFVISITABLE(ExpressionVisitor); + DEFVISITABLE(ExpressionVisitor); + DEFVISITABLE(ExpressionVisitor); + bool Accept(HierarchicalTreeVisitor &visitor) override { + if (visitor.PreVisit(*this)) { + expression_->Accept(visitor); + } + return visitor.PostVisit(*this); + } + + memgraph::query::Expression *expression_{nullptr}; + + AllPropertiesLookup *Clone(AstStorage *storage) const override { + AllPropertiesLookup *object = storage->Create(); + object->expression_ = expression_ ? expression_->Clone(storage) : nullptr; + return object; + } + + protected: + explicit AllPropertiesLookup(Expression *expression) : expression_(expression) {} + + private: + friend class AstStorage; +}; + class LabelsTest : public memgraph::query::Expression { public: static const utils::TypeInfo kType; diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp index fe805f4b6..68e4be6f9 100644 --- a/src/query/frontend/ast/ast_visitor.hpp +++ b/src/query/frontend/ast/ast_visitor.hpp @@ -22,6 +22,7 @@ class CypherUnion; class NamedExpression; class Identifier; class PropertyLookup; +class AllPropertiesLookup; class LabelsTest; class Aggregation; class Function; @@ -44,6 +45,7 @@ class EdgeAtom; class PrimitiveLiteral; class ListLiteral; class MapLiteral; +class MapProjectionLiteral; class OrOperator; class XorOperator; class AndOperator; @@ -106,9 +108,10 @@ using TreeCompositeVisitor = utils::CompositeVisitor< SubtractionOperator, MultiplicationOperator, DivisionOperator, ModOperator, NotEqualOperator, EqualOperator, LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, SubscriptOperator, ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral, - PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, None, CallProcedure, - Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete, Where, SetProperty, SetProperties, SetLabels, - RemoveProperty, RemoveLabels, Merge, Unwind, RegexMatch, LoadCsv, Foreach, Exists, CallSubquery, CypherQuery>; + MapProjectionLiteral, PropertyLookup, AllPropertiesLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, + Extract, All, Single, Any, None, CallProcedure, Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete, + Where, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, Merge, Unwind, RegexMatch, LoadCsv, + Foreach, Exists, CallSubquery, CypherQuery>; using TreeLeafVisitor = utils::LeafVisitor; @@ -122,13 +125,14 @@ class HierarchicalTreeVisitor : public TreeCompositeVisitor, public TreeLeafVisi template class ExpressionVisitor - : public utils::Visitor< - TResult, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator, - SubtractionOperator, MultiplicationOperator, DivisionOperator, ModOperator, NotEqualOperator, EqualOperator, - LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, SubscriptOperator, - ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, - MapLiteral, PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, - None, ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch, Exists> {}; + : public utils::Visitor {}; template class QueryVisitor diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index 502d6ca5d..8b772ca0d 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -1701,6 +1701,38 @@ antlrcpp::Any CypherMainVisitor::visitMapLiteral(MemgraphCypher::MapLiteralConte return map; } +antlrcpp::Any CypherMainVisitor::visitMapProjectionLiteral(MemgraphCypher::MapProjectionLiteralContext *ctx) { + MapProjectionData map_projection_data; + + map_projection_data.map_variable = + storage_->Create(std::any_cast(ctx->variable()->accept(this))); + for (auto *map_el : ctx->mapElement()) { + if (map_el->propertyLookup()) { + auto key = std::any_cast(map_el->propertyLookup()->propertyKeyName()->accept(this)); + auto property = std::any_cast(map_el->propertyLookup()->accept(this)); + auto *property_lookup = storage_->Create(map_projection_data.map_variable, property); + map_projection_data.elements.insert_or_assign(key, property_lookup); + } + if (map_el->allPropertiesLookup()) { + auto key = AddProperty("*"); + auto *all_properties_lookup = storage_->Create(map_projection_data.map_variable); + map_projection_data.elements.insert_or_assign(key, all_properties_lookup); + } + if (map_el->variable()) { + auto key = AddProperty(std::any_cast(map_el->variable()->accept(this))); + auto *variable = storage_->Create(std::any_cast(map_el->variable()->accept(this))); + map_projection_data.elements.insert_or_assign(key, variable); + } + if (map_el->propertyKeyValuePair()) { + auto key = std::any_cast(map_el->propertyKeyValuePair()->propertyKeyName()->accept(this)); + auto *value = std::any_cast(map_el->propertyKeyValuePair()->expression()->accept(this)); + map_projection_data.elements.insert_or_assign(key, value); + } + } + + return map_projection_data; +} + antlrcpp::Any CypherMainVisitor::visitListLiteral(MemgraphCypher::ListLiteralContext *ctx) { std::vector expressions; for (auto *expr_ctx : ctx->expression()) { @@ -2281,6 +2313,10 @@ antlrcpp::Any CypherMainVisitor::visitLiteral(MemgraphCypher::LiteralContext *ct } else if (ctx->listLiteral()) { return static_cast( storage_->Create(std::any_cast>(ctx->listLiteral()->accept(this)))); + } else if (ctx->mapProjectionLiteral()) { + auto map_projection_data = std::any_cast(ctx->mapProjectionLiteral()->accept(this)); + return static_cast(storage_->Create(map_projection_data.map_variable, + std::move(map_projection_data.elements))); } else { return static_cast(storage_->Create( std::any_cast>(ctx->mapLiteral()->accept(this)))); diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp index 7dcc7e2fd..687d91e45 100644 --- a/src/query/frontend/ast/cypher_main_visitor.hpp +++ b/src/query/frontend/ast/cypher_main_visitor.hpp @@ -608,6 +608,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor { */ antlrcpp::Any visitMapLiteral(MemgraphCypher::MapLiteralContext *ctx) override; + /** + * @return MapProjectionData + */ + antlrcpp::Any visitMapProjectionLiteral(MemgraphCypher::MapProjectionLiteralContext *ctx) override; + /** * @return vector */ diff --git a/src/query/frontend/ast/pretty_print.cpp b/src/query/frontend/ast/pretty_print.cpp index c9fb4826c..8275f1cb5 100644 --- a/src/query/frontend/ast/pretty_print.cpp +++ b/src/query/frontend/ast/pretty_print.cpp @@ -54,6 +54,7 @@ class ExpressionPrettyPrinter : public ExpressionVisitor { void Visit(IfOperator &op) override; void Visit(ListLiteral &op) override; void Visit(MapLiteral &op) override; + void Visit(MapProjectionLiteral &op) override; void Visit(LabelsTest &op) override; void Visit(Aggregation &op) override; void Visit(Function &op) override; @@ -68,6 +69,7 @@ class ExpressionPrettyPrinter : public ExpressionVisitor { void Visit(Identifier &op) override; void Visit(PrimitiveLiteral &op) override; void Visit(PropertyLookup &op) override; + void Visit(AllPropertiesLookup &op) override; void Visit(ParameterLookup &op) override; void Visit(NamedExpression &op) override; void Visit(RegexMatch &op) override; @@ -89,6 +91,8 @@ void PrintObject(std::ostream *out, Aggregation::Op op); void PrintObject(std::ostream *out, Expression *expr); +void PrintObject(std::ostream *out, AllPropertiesLookup *apl); + void PrintObject(std::ostream *out, Identifier *expr); void PrintObject(std::ostream *out, const storage::PropertyValue &value); @@ -122,6 +126,15 @@ void PrintObject(std::ostream *out, Expression *expr) { } } +void PrintObject(std::ostream *out, AllPropertiesLookup *apl) { + if (apl) { + ExpressionPrettyPrinter printer{out}; + *out << ".*"; + } else { + *out << ""; + } +} + void PrintObject(std::ostream *out, Identifier *expr) { PrintObject(out, static_cast(expr)); } void PrintObject(std::ostream *out, const storage::PropertyValue &value) { @@ -249,6 +262,17 @@ void ExpressionPrettyPrinter::Visit(MapLiteral &op) { PrintObject(out_, map); } +void ExpressionPrettyPrinter::Visit(MapProjectionLiteral &op) { + std::map map_projection_elements; + for (const auto &kv : op.elements_) { + map_projection_elements[kv.first.name] = kv.second; + } + PrintObject(out_, op.map_variable_); + PrintObject(out_, map_projection_elements); +} + +void ExpressionPrettyPrinter::Visit(AllPropertiesLookup &op) { PrintObject(out_, &op); } + void ExpressionPrettyPrinter::Visit(LabelsTest &op) { PrintOperator(out_, "LabelsTest", op.expression_); } void ExpressionPrettyPrinter::Visit(Aggregation &op) { PrintOperator(out_, "Aggregation", op.op_); } diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index fceb4b906..04dcc1499 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -250,6 +250,7 @@ literal : numberLiteral | booleanLiteral | CYPHERNULL | mapLiteral + | mapProjectionLiteral | listLiteral ; @@ -292,6 +293,8 @@ patternComprehension : '[' ( variable '=' )? relationshipsPattern ( WHERE expres propertyLookup : '.' ( propertyKeyName ) ; +allPropertiesLookup : '.' '*' ; + caseExpression : ( ( CASE ( caseAlternatives )+ ) | ( CASE test=expression ( caseAlternatives )+ ) ) ( ELSE else_expression=expression )? END ; caseAlternatives : WHEN when_expression=expression THEN then_expression=expression ; @@ -304,12 +307,22 @@ numberLiteral : doubleLiteral mapLiteral : '{' ( propertyKeyName ':' expression ( ',' propertyKeyName ':' expression )* )? '}' ; +mapProjectionLiteral : variable '{' ( mapElement ( ',' mapElement )* )? '}' ; + +mapElement : propertyLookup + | allPropertiesLookup + | variable + | propertyKeyValuePair + ; + parameter : '$' ( symbolicName | DecimalLiteral ) ; propertyExpression : atom ( propertyLookup )+ ; propertyKeyName : symbolicName ; +propertyKeyValuePair : propertyKeyName ':' expression ; + integerLiteral : DecimalLiteral | OctalLiteral | HexadecimalLiteral diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp index a094fcea0..66fb06e23 100644 --- a/src/query/interpret/eval.hpp +++ b/src/query/interpret/eval.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include "query/common.hpp" @@ -73,11 +74,13 @@ class ReferenceExpressionEvaluator : public ExpressionVisitor { UNSUCCESSFUL_VISIT(ListSlicingOperator); UNSUCCESSFUL_VISIT(IsNullOperator); UNSUCCESSFUL_VISIT(PropertyLookup); + UNSUCCESSFUL_VISIT(AllPropertiesLookup); UNSUCCESSFUL_VISIT(LabelsTest); UNSUCCESSFUL_VISIT(PrimitiveLiteral); UNSUCCESSFUL_VISIT(ListLiteral); UNSUCCESSFUL_VISIT(MapLiteral); + UNSUCCESSFUL_VISIT(MapProjectionLiteral); UNSUCCESSFUL_VISIT(Aggregation); UNSUCCESSFUL_VISIT(Coalesce); UNSUCCESSFUL_VISIT(Function); @@ -466,7 +469,101 @@ class ExpressionEvaluator : public ExpressionVisitor { throw QueryRuntimeException("Invalid property name {} for Graph", prop_name); } default: - throw QueryRuntimeException("Only nodes, edges, maps and temporal types have properties to be looked-up."); + throw QueryRuntimeException( + "Only nodes, edges, maps, temporal types and graphs have properties to be looked up."); + } + } + + TypedValue Visit(AllPropertiesLookup &all_properties_lookup) override { + TypedValue::TMap result(ctx_->memory); + + auto expression_result = all_properties_lookup.expression_->Accept(*this); + switch (expression_result.type()) { + case TypedValue::Type::Null: + return TypedValue(ctx_->memory); + case TypedValue::Type::Vertex: { + for (const auto properties = *expression_result.ValueVertex().Properties(view_); + const auto &[property_id, value] : properties) { + result.emplace(dba_->PropertyToName(property_id), value); + } + return TypedValue(result, ctx_->memory); + } + case TypedValue::Type::Edge: { + for (const auto properties = *expression_result.ValueEdge().Properties(view_); + const auto &[property_id, value] : properties) { + result.emplace(dba_->PropertyToName(property_id), value); + } + return TypedValue(result, ctx_->memory); + } + case TypedValue::Type::Map: { + for (auto &[name, value] : expression_result.ValueMap()) { + result.emplace(name, value); + } + return TypedValue(result, ctx_->memory); + } + case TypedValue::Type::Duration: { + const auto &dur = expression_result.ValueDuration(); + result.emplace("day", TypedValue(dur.Days(), ctx_->memory)); + result.emplace("hour", TypedValue(dur.SubDaysAsHours(), ctx_->memory)); + result.emplace("minute", TypedValue(dur.SubDaysAsMinutes(), ctx_->memory)); + result.emplace("second", TypedValue(dur.SubDaysAsSeconds(), ctx_->memory)); + result.emplace("millisecond", TypedValue(dur.SubDaysAsMilliseconds(), ctx_->memory)); + result.emplace("microseconds", TypedValue(dur.SubDaysAsMicroseconds(), ctx_->memory)); + result.emplace("nanoseconds", TypedValue(dur.SubDaysAsNanoseconds(), ctx_->memory)); + return TypedValue(result, ctx_->memory); + } + case TypedValue::Type::Date: { + const auto &date = expression_result.ValueDate(); + result.emplace("year", TypedValue(date.year, ctx_->memory)); + result.emplace("month", TypedValue(date.month, ctx_->memory)); + result.emplace("day", TypedValue(date.day, ctx_->memory)); + return TypedValue(result, ctx_->memory); + } + case TypedValue::Type::LocalTime: { + const auto < = expression_result.ValueLocalTime(); + result.emplace("hour", TypedValue(lt.hour, ctx_->memory)); + result.emplace("minute", TypedValue(lt.minute, ctx_->memory)); + result.emplace("second", TypedValue(lt.second, ctx_->memory)); + result.emplace("millisecond", TypedValue(lt.millisecond, ctx_->memory)); + result.emplace("microsecond", TypedValue(lt.microsecond, ctx_->memory)); + return TypedValue(result, ctx_->memory); + } + case TypedValue::Type::LocalDateTime: { + const auto &ldt = expression_result.ValueLocalDateTime(); + const auto &date = ldt.date; + const auto < = ldt.local_time; + result.emplace("year", TypedValue(date.year, ctx_->memory)); + result.emplace("month", TypedValue(date.month, ctx_->memory)); + result.emplace("day", TypedValue(date.day, ctx_->memory)); + result.emplace("hour", TypedValue(lt.hour, ctx_->memory)); + result.emplace("minute", TypedValue(lt.minute, ctx_->memory)); + result.emplace("second", TypedValue(lt.second, ctx_->memory)); + result.emplace("millisecond", TypedValue(lt.millisecond, ctx_->memory)); + result.emplace("microsecond", TypedValue(lt.microsecond, ctx_->memory)); + return TypedValue(result, ctx_->memory); + } + case TypedValue::Type::Graph: { + const auto &graph = expression_result.ValueGraph(); + + utils::pmr::vector vertices(ctx_->memory); + vertices.reserve(graph.vertices().size()); + for (const auto &v : graph.vertices()) { + vertices.emplace_back(TypedValue(v, ctx_->memory)); + } + result.emplace("nodes", TypedValue(std::move(vertices), ctx_->memory)); + + utils::pmr::vector edges(ctx_->memory); + edges.reserve(graph.edges().size()); + for (const auto &e : graph.edges()) { + edges.emplace_back(TypedValue(e, ctx_->memory)); + } + result.emplace("edges", TypedValue(std::move(edges), ctx_->memory)); + + return TypedValue(result, ctx_->memory); + } + default: + throw QueryRuntimeException( + "Only nodes, edges, maps, temporal types and graphs have properties to be looked up."); } } @@ -531,6 +628,30 @@ class ExpressionEvaluator : public ExpressionVisitor { return TypedValue(result, ctx_->memory); } + TypedValue Visit(MapProjectionLiteral &literal) override { + constexpr std::string_view kAllPropertiesSelector{"*"}; + + TypedValue::TMap result(ctx_->memory); + TypedValue::TMap all_properties_lookup(ctx_->memory); + for (const auto &[property_key, property_value] : literal.elements_) { + if (property_key.name == kAllPropertiesSelector.data()) { + auto maybe_all_properties_lookup = property_value->Accept(*this); + + if (maybe_all_properties_lookup.type() != TypedValue::Type::Map) { + throw QueryRuntimeException("Expected a map from AllPropertiesLookup, got {}.", + maybe_all_properties_lookup.type()); + } + all_properties_lookup = std::move(maybe_all_properties_lookup.ValueMap()); + continue; + } + + result.emplace(property_key.name, property_value->Accept(*this)); + } + if (!all_properties_lookup.empty()) result.merge(all_properties_lookup); + + return TypedValue(result, ctx_->memory); + } + TypedValue Visit(Aggregation &aggregation) override { return TypedValue(frame_->at(symbol_table_->at(aggregation)), ctx_->memory); } @@ -852,7 +973,7 @@ class ExpressionEvaluator : public ExpressionVisitor { DbAccessor *dba_; // which switching approach should be used when evaluating storage::View view_; -}; +}; // namespace memgraph::query /// A helper function for evaluating an expression that's an int. /// diff --git a/src/query/plan/preprocess.hpp b/src/query/plan/preprocess.hpp index a9aee4a71..50b6a621f 100644 --- a/src/query/plan/preprocess.hpp +++ b/src/query/plan/preprocess.hpp @@ -180,6 +180,7 @@ class PatternFilterVisitor : public ExpressionVisitor { void Visit(IfOperator &op) override{}; void Visit(ListLiteral &op) override{}; void Visit(MapLiteral &op) override{}; + void Visit(MapProjectionLiteral &op) override{}; void Visit(LabelsTest &op) override{}; void Visit(Aggregation &op) override{}; void Visit(Function &op) override{}; @@ -194,6 +195,7 @@ class PatternFilterVisitor : public ExpressionVisitor { void Visit(Identifier &op) override{}; void Visit(PrimitiveLiteral &op) override{}; void Visit(PropertyLookup &op) override{}; + void Visit(AllPropertiesLookup &op) override{}; void Visit(ParameterLookup &op) override{}; void Visit(NamedExpression &op) override{}; void Visit(RegexMatch &op) override{}; diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp index e2805ceef..65e4361e1 100644 --- a/src/query/plan/rule_based_planner.cpp +++ b/src/query/plan/rule_based_planner.cpp @@ -124,11 +124,18 @@ class ReturnBodyContext : public HierarchicalTreeVisitor { bool PostVisit(MapLiteral &map_literal) override { MG_ASSERT(map_literal.elements_.size() <= has_aggregation_.size(), - "Expected has_aggregation_ flags as much as there are map elements."); + "Expected as many has_aggregation_ flags as there are map elements."); PostVisitCollectionLiteral(map_literal, [](auto it) { return it->second; }); return true; } + bool PostVisit(MapProjectionLiteral &map_projection_literal) override { + MG_ASSERT(map_projection_literal.elements_.size() <= has_aggregation_.size(), + "Expected as many has_aggregation_ flags as there are map elements."); + PostVisitCollectionLiteral(map_projection_literal, [](auto it) { return it->second; }); + return true; + } + bool PostVisit(All &all) override { // Remove the symbol which is bound by all, because we are only interested // in free (unbound) symbols. diff --git a/src/utils/typeinfo.hpp b/src/utils/typeinfo.hpp index 8f568feab..9bce30aff 100644 --- a/src/utils/typeinfo.hpp +++ b/src/utils/typeinfo.hpp @@ -118,8 +118,10 @@ enum class TypeId : uint64_t { AST_PRIMITIVE_LITERAL, AST_LIST_LITERAL, AST_MAP_LITERAL, + AST_MAP_PROJECTION_LITERAL, AST_IDENTIFIER, AST_PROPERTY_LOOKUP, + AST_ALL_PROPERTIES_LOOKUP, AST_LABELS_TEST, AST_FUNCTION, AST_REDUCE, diff --git a/tests/benchmark/query/eval.cpp b/tests/benchmark/query/eval.cpp index 7320c0cc7..72e185793 100644 --- a/tests/benchmark/query/eval.cpp +++ b/tests/benchmark/query/eval.cpp @@ -1,4 +1,4 @@ -// Copyright 2022 Memgraph Ltd. +// Copyright 2023 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -60,6 +60,8 @@ BENCHMARK_TEMPLATE(MapLiteral, NewDeleteResource)->Range(512, 1U << 15U)->Unit(b BENCHMARK_TEMPLATE(MapLiteral, MonotonicBufferResource)->Range(512, 1U << 15U)->Unit(benchmark::kMicrosecond); +// TODO ante benchmark template for MapProjectionLiteral + template // NOLINTNEXTLINE(google-runtime-references) static void AdditionOperator(benchmark::State &state) { diff --git a/tests/gql_behave/tests/memgraph_V1/features/map_projection.feature b/tests/gql_behave/tests/memgraph_V1/features/map_projection.feature new file mode 100644 index 000000000..f04b04a6e --- /dev/null +++ b/tests/gql_behave/tests/memgraph_V1/features/map_projection.feature @@ -0,0 +1,76 @@ +Feature: Map projection + + Scenario: Returning an empty map projection + When executing query: + """ + WITH {} AS map + RETURN map {} as result + """ + Then the result should be: + | result | + | {} | + + Scenario: Returning a map projection with each type of map projection element + Given an empty graph + And having executed + """ + CREATE (n:Actor {name: "Morgan", lastName: "Freeman"}) + """ + When executing query: + """ + WITH 85 as age + MATCH (actor:Actor) + RETURN actor {.*, .name, age, oscars: 1} AS result + """ + Then the result should be: + | result | + | {age: 85, lastName: 'Freeman', name: 'Morgan', oscars: 1} | + + Scenario: Projecting a nonexistent property + When executing query: + """ + WITH {name: "Morgan", lastName: "Freeman"} as actor + RETURN actor.age; + """ + Then the result should be: + | actor.age | + | null | + + Scenario: Storing a map projection as a property + Given an empty graph + And having executed + """ + WITH {name: "Morgan", lastName: "Freeman"} as person + WITH person {.*, wonOscars: true} as actor + CREATE (n:Movie {lead: actor}); + """ + When executing query: + """ + MATCH (movie:Movie) + RETURN movie.lead + """ + Then the result should be: + | movie.lead | + | {lastName: 'Freeman', name: 'Morgan', wonOscars: true} | + + Scenario: Looking up the properties of a map projection + When executing query: + """ + WITH {name: "Morgan", lastName: "Freeman"} as actor, {oscars: 1} as awards + WITH actor {.*, awards: awards} AS actor + RETURN actor.name, actor.awards.oscars; + """ + Then the result should be: + | actor.name | actor.awards.oscars | + | 'Morgan' | 1 | + + Scenario: Indexing a map projection + When executing query: + """ + WITH {name: "Morgan", lastName: "Freeman"} as actor, {oscars: 1} as awards + WITH actor {.*, awards: awards} AS actor + RETURN actor["name"], actor["awards"]["oscars"] + """ + Then the result should be: + | actor["name"] | actor["awards"]["oscars"] | + | 'Morgan' | 1 | diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index c87e000b6..effa96cd7 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -1029,6 +1029,63 @@ TEST_P(CypherMainVisitorTest, MapLiteral) { EXPECT_EQ(1, elem_2_1->elements_.size()); } +TEST_P(CypherMainVisitorTest, MapProjectionLiteral) { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast(ast_generator.ParseQuery( + "WITH {name: \"Morgan\"} as actor, 85 as age RETURN actor {.name, .*, age, lastName: \"Freeman\"}")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + auto *return_clause = dynamic_cast(single_query->clauses_[1]); + auto *map_projection_literal = + dynamic_cast(return_clause->body_.named_expressions[0]->expression_); + ASSERT_TRUE(map_projection_literal); + ASSERT_EQ(4, map_projection_literal->elements_.size()); + + ASSERT_EQ(std::string(map_projection_literal->elements_[ast_generator.Prop("name")]->GetTypeInfo().name), + std::string("PropertyLookup")); + ASSERT_EQ(std::string(map_projection_literal->elements_[ast_generator.Prop("*")]->GetTypeInfo().name), + std::string("AllPropertiesLookup")); + ASSERT_EQ(std::string(map_projection_literal->elements_[ast_generator.Prop("age")]->GetTypeInfo().name), + std::string("Identifier")); + ASSERT_EQ(std::string(map_projection_literal->elements_[ast_generator.Prop("lastName")]->GetTypeInfo().name), + std::string(typeid(ast_generator).name()).ends_with("CachedAstGenerator") + ? std::string("ParameterLookup") + : std::string("PrimitiveLiteral")); +} + +TEST_P(CypherMainVisitorTest, MapProjectionRepeatedKeySameTypeValue) { + auto &ast_generator = *GetParam(); + auto *query = dynamic_cast(ast_generator.ParseQuery("WITH {} as x RETURN x {a: 0, a: 1}")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + auto *return_clause = dynamic_cast(single_query->clauses_[1]); + auto *map_projection_literal = + dynamic_cast(return_clause->body_.named_expressions[0]->expression_); + ASSERT_TRUE(map_projection_literal); + // When multiple map properties have the same name, only one gets in + ASSERT_EQ(1, map_projection_literal->elements_.size()); +} + +TEST_P(CypherMainVisitorTest, MapProjectionRepeatedKeyDifferentTypeValue) { + auto &ast_generator = *GetParam(); + auto *query = + dynamic_cast(ast_generator.ParseQuery("WITH {a: 0} as x, 1 as a RETURN x {a: 2, .a, a}")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + auto *return_clause = dynamic_cast(single_query->clauses_[1]); + auto *map_projection_literal = + dynamic_cast(return_clause->body_.named_expressions[0]->expression_); + ASSERT_TRUE(map_projection_literal); + // When multiple map properties have the same name, only one gets in + ASSERT_EQ(1, map_projection_literal->elements_.size()); + // The last-given map property is the one that gets in + ASSERT_EQ(std::string(map_projection_literal->elements_[ast_generator.Prop("a")]->GetTypeInfo().name), + std::string("Identifier")); +} + TEST_P(CypherMainVisitorTest, NodePattern) { auto &ast_generator = *GetParam(); auto *query = diff --git a/tests/unit/query_common.hpp b/tests/unit/query_common.hpp index 01d8a1820..444053e01 100644 --- a/tests/unit/query_common.hpp +++ b/tests/unit/query_common.hpp @@ -153,6 +153,14 @@ auto GetPropertyLookup(AstStorage &storage, TDbAccessor &, Expression *expr, return storage.Create(expr, storage.GetPropertyIx(prop_pair.first)); } +/// Create an AllPropertiesLookup from the given name. +auto GetAllPropertiesLookup(AstStorage &storage, const std::string &name) { + return storage.Create(storage.Create(name)); +} + +/// Create an AllPropertiesLookup from the given expression. +auto GetAllPropertiesLookup(AstStorage &storage, Expression *expr) { return storage.Create(expr); } + /// Create an EdgeAtom with given name, direction and edge_type. /// /// Name is used to create the Identifier which is assigned to the edge. @@ -519,8 +527,13 @@ auto GetForeach(AstStorage &storage, NamedExpression *named_expr, const std::vec #define MAP(...) \ storage.Create( \ std::unordered_map{__VA_ARGS__}) +#define MAP_PROJECTION(map_variable, elements) \ + storage.Create( \ + (memgraph::query::Expression *){map_variable}, \ + std::unordered_map{elements}) #define PROPERTY_PAIR(property_name) std::make_pair(property_name, dba.NameToProperty(property_name)) #define PROPERTY_LOOKUP(...) memgraph::query::test_common::GetPropertyLookup(storage, dba, __VA_ARGS__) +#define ALL_PROPERTIES_LOOKUP(expr) memgraph::query::test_common::GetAllPropertiesLookup(storage, expr) #define PARAMETER_LOOKUP(token_position) storage.Create((token_position)) #define NEXPR(name, expr) storage.Create((name), (expr)) // AS is alternative to NEXPR which does not initialize NamedExpression with diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp index f1ae0d652..6f51facdf 100644 --- a/tests/unit/query_expression_evaluator.cpp +++ b/tests/unit/query_expression_evaluator.cpp @@ -373,6 +373,81 @@ TEST_F(ExpressionEvaluatorTest, MapIndexing) { } } +TEST_F(ExpressionEvaluatorTest, MapProjectionIndexing) { + auto *map_variable = storage.Create( + std::unordered_map{{storage.GetPropertyIx("x"), storage.Create(0)}}); + auto *map_projection_literal = storage.Create( + map_variable, + std::unordered_map{ + {storage.GetPropertyIx("a"), storage.Create(1)}, + {storage.GetPropertyIx("y"), storage.Create(map_variable, storage.GetPropertyIx("y"))}}); + + { + // Legal indexing. + auto *op = storage.Create(map_projection_literal, storage.Create("a")); + auto value = Eval(op); + EXPECT_EQ(value.ValueInt(), 1); + } + { + // Legal indexing; property created by PropertyLookup of a non-existent map variable key + auto *op = storage.Create(map_projection_literal, storage.Create("y")); + auto value = Eval(op); + EXPECT_TRUE(value.IsNull()); + } + { + // Legal indexing, non-existing property. + auto *op = storage.Create(map_projection_literal, storage.Create("z")); + auto value = Eval(op); + EXPECT_TRUE(value.IsNull()); + } + { + // Wrong key type. + auto *op = storage.Create(map_projection_literal, storage.Create(42)); + EXPECT_THROW(Eval(op), QueryRuntimeException); + } + { + // Indexing with Null. + auto *op = storage.Create(map_projection_literal, + storage.Create(memgraph::storage::PropertyValue())); + auto value = Eval(op); + EXPECT_TRUE(value.IsNull()); + } +} + +TEST_F(ExpressionEvaluatorTest, MapProjectionAllPropertiesLookupBefore) { + // AllPropertiesLookup (.*) may contain properties whose names also occur in MapProjectionLiteral + // The ones in MapProjectionLiteral are explicitly given and thus take precedence over those in AllPropertiesLookup + // Test case: AllPropertiesLookup comes before the identically-named properties + + auto *map_variable = storage.Create( + std::unordered_map{{storage.GetPropertyIx("x"), storage.Create(0)}}); + auto *map_projection_literal = storage.Create( + map_variable, std::unordered_map{ + {storage.GetPropertyIx("*"), storage.Create(map_variable)}, + {storage.GetPropertyIx("x"), storage.Create(1)}}); + + auto *op = storage.Create(map_projection_literal, storage.Create("x")); + auto value = Eval(op); + EXPECT_EQ(value.ValueInt(), 1); +} + +TEST_F(ExpressionEvaluatorTest, MapProjectionAllPropertiesLookupAfter) { + // AllPropertiesLookup (.*) may contain properties whose names also occur in MapProjectionLiteral + // The ones in MapProjectionLiteral are explicitly given and thus take precedence over those in AllPropertiesLookup + // Test case: AllPropertiesLookup comes after the identically-named properties + + auto *map_variable = storage.Create( + std::unordered_map{{storage.GetPropertyIx("x"), storage.Create(0)}}); + auto *map_projection_literal = storage.Create( + map_variable, std::unordered_map{ + {storage.GetPropertyIx("x"), storage.Create(1)}, + {storage.GetPropertyIx("*"), storage.Create(map_variable)}}); + + auto *op = storage.Create(map_projection_literal, storage.Create("x")); + auto value = Eval(op); + EXPECT_EQ(value.ValueInt(), 1); +} + TEST_F(ExpressionEvaluatorTest, VertexAndEdgeIndexing) { auto edge_type = dba.NameToEdgeType("edge_type"); auto prop = dba.NameToProperty("prop"); @@ -1197,12 +1272,89 @@ TEST_F(ExpressionEvaluatorPropertyLookup, Null) { EXPECT_TRUE(Value(prop_age).IsNull()); } -TEST_F(ExpressionEvaluatorPropertyLookup, MapLiteral) { +TEST_F(ExpressionEvaluatorPropertyLookup, Map) { frame[symbol] = TypedValue(std::map{{prop_age.first, TypedValue(10)}}); EXPECT_EQ(Value(prop_age).ValueInt(), 10); EXPECT_TRUE(Value(prop_height).IsNull()); } +class ExpressionEvaluatorAllPropertiesLookup : public ExpressionEvaluatorTest { + protected: + std::pair prop_age = std::make_pair("age", dba.NameToProperty("age")); + std::pair prop_height = + std::make_pair("height", dba.NameToProperty("height")); + Identifier *identifier = storage.Create("element"); + Symbol symbol = symbol_table.CreateSymbol("element", true); + + void SetUp() { identifier->MapTo(symbol); } + + auto Value() { + auto *op = storage.Create(identifier); + return Eval(op); + } +}; + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, Vertex) { + auto v1 = dba.InsertVertex(); + ASSERT_TRUE(v1.SetProperty(prop_age.second, memgraph::storage::PropertyValue(10)).HasValue()); + dba.AdvanceCommand(); + frame[symbol] = TypedValue(v1); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsMap()); +} + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, Edge) { + auto v1 = dba.InsertVertex(); + auto v2 = dba.InsertVertex(); + auto e12 = dba.InsertEdge(&v1, &v2, dba.NameToEdgeType("edge_type")); + ASSERT_TRUE(e12.HasValue()); + ASSERT_TRUE(e12->SetProperty(prop_age.second, memgraph::storage::PropertyValue(10)).HasValue()); + dba.AdvanceCommand(); + frame[symbol] = TypedValue(*e12); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsMap()); +} + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, Duration) { + const memgraph::utils::Duration dur({10, 1, 30, 2, 22, 45}); + frame[symbol] = TypedValue(dur); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsMap()); +} + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, Date) { + const memgraph::utils::Date date({1996, 11, 22}); + frame[symbol] = TypedValue(date); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsMap()); +} + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, LocalTime) { + const memgraph::utils::LocalTime lt({1, 2, 3, 11, 22}); + frame[symbol] = TypedValue(lt); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsMap()); +} + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, LocalDateTime) { + const memgraph::utils::LocalDateTime ldt({1993, 8, 6}, {2, 3, 4, 55, 40}); + frame[symbol] = TypedValue(ldt); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsMap()); +} + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, Null) { + frame[symbol] = TypedValue(); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsNull()); +} + +TEST_F(ExpressionEvaluatorAllPropertiesLookup, Map) { + frame[symbol] = TypedValue(std::map{{prop_age.first, TypedValue(10)}}); + auto all_properties = Value(); + EXPECT_TRUE(all_properties.IsMap()); +} + class FunctionTest : public ExpressionEvaluatorTest { protected: std::vector ExpressionsFromTypedValues(const std::vector &tvs) { diff --git a/tests/unit/query_plan.cpp b/tests/unit/query_plan.cpp index 4bfd81b2a..5d2fe6d46 100644 --- a/tests/unit/query_plan.cpp +++ b/tests/unit/query_plan.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include @@ -958,6 +959,20 @@ TYPED_TEST(TestPlanner, MapLiteralAggregationReturn) { CheckPlan(query, storage, aggr, ExpectProduce()); } +TYPED_TEST(TestPlanner, MapProjectionLiteralAggregationReturn) { + // Test WITH {} as map RETURN map {sum: SUM(2)} AS result, 42 AS group_by + AstStorage storage; + FakeDbAccessor dba; + auto sum = SUM(LITERAL(2), false); + auto group_by_literal = LITERAL(42); + auto elements = std::unordered_map{ + {storage.GetPropertyIx("sum"), sum}}; + auto *query = QUERY(SINGLE_QUERY(WITH(MAP(), AS("map")), RETURN(MAP_PROJECTION(IDENT("map"), elements), AS("result"), + group_by_literal, AS("group_by")))); + auto aggr = ExpectAggregate({sum}, {group_by_literal}); + CheckPlan(query, storage, ExpectProduce(), aggr, ExpectProduce()); +} + TYPED_TEST(TestPlanner, EmptyListIndexAggregation) { // Test RETURN [][SUM(2)] AS result, 42 AS group_by AstStorage storage; @@ -1009,7 +1024,7 @@ TYPED_TEST(TestPlanner, AggregatonWithListWithAggregationAndGroupBy) { } TYPED_TEST(TestPlanner, MapWithAggregationAndGroupBy) { - // Test RETURN {lit: 42, sum: sum(2)} + // Test RETURN {lit: 42, sum: sum(2)} AS result AstStorage storage; FakeDbAccessor dba; auto sum = SUM(LITERAL(2), false); @@ -1020,6 +1035,20 @@ TYPED_TEST(TestPlanner, MapWithAggregationAndGroupBy) { CheckPlan(query, storage, aggr, ExpectProduce()); } +TYPED_TEST(TestPlanner, MapProjectionWithAggregationAndGroupBy) { + // Test WITH {} as map RETURN map {lit: 42, sum: SUM(2)} AS result + AstStorage storage; + FakeDbAccessor dba; + auto sum = SUM(LITERAL(2), false); + auto group_by_literal = LITERAL(42); + auto projection = std::unordered_map{ + {storage.GetPropertyIx("lit"), group_by_literal}, {storage.GetPropertyIx("sum"), sum}}; + auto *query = + QUERY(SINGLE_QUERY(WITH(MAP(), AS("map")), RETURN(MAP_PROJECTION(IDENT("map"), projection), AS("result")))); + auto aggr = ExpectAggregate({sum}, {group_by_literal}); + CheckPlan(query, storage, ExpectProduce(), aggr, ExpectProduce()); +} + TYPED_TEST(TestPlanner, AtomIndexedLabelProperty) { // Test MATCH (n :label {property: 42, not_indexed: 0}) RETURN n AstStorage storage; diff --git a/tests/unit/query_pretty_print.cpp b/tests/unit/query_pretty_print.cpp index 27103631a..1f5893253 100644 --- a/tests/unit/query_pretty_print.cpp +++ b/tests/unit/query_pretty_print.cpp @@ -9,6 +9,7 @@ // by the Apache License, Version 2.0, included in the file // licenses/APL.txt. +#include #include #include @@ -70,6 +71,16 @@ TEST_F(ExpressionPrettyPrinterTest, Literals) { EXPECT_EQ(ToString(LITERAL(memgraph::storage::PropertyValue(tt_vec))), "[DURATION(\"P0DT0H0M0.000001S\"), DURATION(\"P0DT0H0M-0.000002S\"), LOCALTIME(\"00:00:00.000002\"), " "LOCALDATETIME(\"1970-01-01T00:00:00.000003\"), DATE(\"1970-01-01\")]"); + + // map {literalEntry: 10, variableSelector: a, .map, .*} + auto elements = std::unordered_map{ + {storage.GetPropertyIx("literalEntry"), LITERAL(10)}, + {storage.GetPropertyIx("variableSelector"), IDENT("a")}, + {storage.GetPropertyIx("propertySelector"), PROPERTY_LOOKUP("map", PROPERTY_PAIR("hello"))}, + {storage.GetPropertyIx("allPropertiesSelector"), ALL_PROPERTIES_LOOKUP("map")}}; + EXPECT_EQ(ToString(MAP_PROJECTION(IDENT("map"), elements)), + "(Identifier \"map\"){\"allPropertiesSelector\": .*, \"literalEntry\": 10, \"propertySelector\": " + "(PropertyLookup (Identifier \"map\") \"hello\"), \"variableSelector\": (Identifier \"a\")}"); } TEST_F(ExpressionPrettyPrinterTest, Identifiers) { diff --git a/tests/unit/stripped.cpp b/tests/unit/stripped.cpp index 969f641e4..6e2b6bd3e 100644 --- a/tests/unit/stripped.cpp +++ b/tests/unit/stripped.cpp @@ -1,4 +1,4 @@ -// Copyright 2022 Memgraph Ltd. +// Copyright 2023 Memgraph Ltd. // // Use of this software is governed by the Business Source License // included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source @@ -194,6 +194,12 @@ TEST(QueryStripper, MapLiteral) { EXPECT_EQ(stripped.query(), "MATCH ( n ) RETURN { val : n }"); } +TEST(QueryStripper, MapProjectionLiteral) { + StrippedQuery stripped("WITH 0 as var MATCH (n) RETURN n {.x, var, key: 'a'}"); + EXPECT_EQ(stripped.literals().size(), 2); + EXPECT_EQ(stripped.query(), "WITH 0 as var MATCH ( n ) RETURN n { . x , var , key : \"a\" }"); +} + TEST(QueryStripper, RangeLiteral) { StrippedQuery stripped("MATCH (n)-[*2..3]-() RETURN n"); EXPECT_EQ(stripped.literals().size(), 2); @@ -357,6 +363,11 @@ TEST(QueryStripper, QueryReturnMap) { EXPECT_THAT(stripped.named_expressions(), UnorderedElementsAre(Pair(1, "{a: 1, b: 'foo'}"))); } +TEST(QueryStripper, QueryReturnMapProjection) { + StrippedQuery stripped("RETURN a {.prop, var, key: 2}"); + EXPECT_THAT(stripped.named_expressions(), UnorderedElementsAre(Pair(1, "a {.prop, var, key: 2}"))); +} + TEST(QueryStripper, QuerySemicolonEndingQuery1) { StrippedQuery stripped("RETURN 1;"); EXPECT_THAT(stripped.named_expressions(), UnorderedElementsAre(Pair(1, "1")));