From ad4c80af13d1e39806cd8044d032f05909b65b2c Mon Sep 17 00:00:00 2001 From: antonio2368 Date: Tue, 16 Mar 2021 09:05:38 +0100 Subject: [PATCH] Add memory limit clause for cypher queries (#106) * Add memory limit clause for cypher queries * Add PROCEDURE and QUERY keywords * Improve memory limit logs * Update CHANGELOG --- CHANGELOG.md | 7 + src/query/CMakeLists.txt | 1 + src/query/frontend/ast/ast.lcp | 6 +- .../frontend/ast/cypher_main_visitor.cpp | 46 +++++-- .../frontend/opencypher/grammar/Cypher.g4 | 14 +- .../opencypher/grammar/CypherLexer.g4 | 2 + src/query/frontend/stripped.cpp | 3 +- .../frontend/stripped_lexer_constants.hpp | 2 +- src/query/interpret/eval.cpp | 24 ++++ src/query/interpret/eval.hpp | 11 +- src/query/plan/operator.cpp | 16 +-- tests/unit/cypher_main_visitor.cpp | 129 ++++++++++++++++-- 12 files changed, 216 insertions(+), 45 deletions(-) create mode 100644 src/query/interpret/eval.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index b09c241f5..6d166530e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Future +### Breaking Changes + +* Changed `MEMORY LIMIT num (KB|MB)` clause in the procedure calls to `PROCEDURE MEMORY LIMIT num (KB|MB)`. + The functionality is still the same. + ### Major Feature and Improvements * Added replication to community version. @@ -15,6 +20,8 @@ memgraph can allocate during its runtime. * Added `FREE MEMORY` query which tries to free unusued memory chunks in different parts of storage. * Added the memory limit and amount of currently allocated bytes in the result of `SHOW STORAGE INFO` query. +* Added `QUERY MEMORY LIMIT num (KB|MB)` to Cypher queries which allows you to limit memory allocation for + the entire query. It can be added only at the end of the entire Cypher query. ### Bug Fixes diff --git a/src/query/CMakeLists.txt b/src/query/CMakeLists.txt index 17626e681..d68774818 100644 --- a/src/query/CMakeLists.txt +++ b/src/query/CMakeLists.txt @@ -17,6 +17,7 @@ set(mg_query_sources frontend/semantic/symbol_generator.cpp frontend/stripped.cpp interpret/awesome_memgraph_functions.cpp + interpret/eval.cpp interpreter.cpp plan/operator.cpp plan/preprocess.cpp diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index aaa855ac5..b42e07a08 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -1543,7 +1543,11 @@ cpp<# :scope :public :slk-save #'slk-save-ast-vector :slk-load (slk-load-ast-vector "CypherUnion") - :documentation "Contains remaining queries that should form and union with `single_query_`.")) + :documentation "Contains remaining queries that should form and union with `single_query_`.") + (memory-limit "Expression *" :initval "nullptr" :scope :public + :slk-save #'slk-save-ast-pointer + :slk-load (slk-load-ast-pointer "Expression")) + (memory-scale "size_t" :initval "1024U" :scope :public)) (:public #>cpp CypherQuery() = default; diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index 0c7fb2b33..7773ace33 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -33,6 +33,28 @@ namespace query::frontend { const std::string CypherMainVisitor::kAnonPrefix = "anon"; +namespace { +template +std::optional> VisitMemoryLimit( + MemgraphCypher::MemoryLimitContext *memory_limit_ctx, TVisitor *visitor) { + MG_ASSERT(memory_limit_ctx); + if (memory_limit_ctx->UNLIMITED()) { + return std::nullopt; + } + + auto memory_limit = memory_limit_ctx->literal()->accept(visitor); + size_t memory_scale = 1024U; + if (memory_limit_ctx->MB()) { + memory_scale = 1024U * 1024U; + } else { + MG_ASSERT(memory_limit_ctx->KB()); + memory_scale = 1024U; + } + + return std::make_pair(memory_limit, memory_scale); +} +} // namespace + antlrcpp::Any CypherMainVisitor::visitExplainQuery(MemgraphCypher::ExplainQueryContext *ctx) { MG_ASSERT(ctx->children.size() == 2, "ExplainQuery should have exactly two children!"); auto *cypher_query = ctx->children[1]->accept(this).as(); @@ -127,6 +149,14 @@ antlrcpp::Any CypherMainVisitor::visitCypherQuery(MemgraphCypher::CypherQueryCon cypher_query->cypher_unions_.push_back(child->accept(this).as()); } + if (auto *memory_limit_ctx = ctx->queryMemoryLimit()) { + const auto memory_limit_info = VisitMemoryLimit(memory_limit_ctx->memoryLimit(), this); + if (memory_limit_info) { + cypher_query->memory_limit_ = memory_limit_info->first; + cypher_query->memory_scale_ = memory_limit_info->second; + } + } + query_ = cypher_query; return cypher_query; } @@ -489,21 +519,19 @@ antlrcpp::Any CypherMainVisitor::visitCallProcedure(MemgraphCypher::CallProcedur for (auto *expr : ctx->expression()) { call_proc->arguments_.push_back(expr->accept(this)); } - if (auto *memory_limit_ctx = ctx->callProcedureMemoryLimit()) { - if (memory_limit_ctx->LIMIT()) { - call_proc->memory_limit_ = memory_limit_ctx->literal()->accept(this); - if (memory_limit_ctx->MB()) { - call_proc->memory_scale_ = 1024U * 1024U; - } else { - MG_ASSERT(memory_limit_ctx->KB()); - call_proc->memory_scale_ = 1024U; - } + + if (auto *memory_limit_ctx = ctx->procedureMemoryLimit()) { + const auto memory_limit_info = VisitMemoryLimit(memory_limit_ctx->memoryLimit(), this); + if (memory_limit_info) { + call_proc->memory_limit_ = memory_limit_info->first; + call_proc->memory_scale_ = memory_limit_info->second; } } else { // Default to 100 MB call_proc->memory_limit_ = storage_->Create(TypedValue(100)); call_proc->memory_scale_ = 1024U * 1024U; } + auto *yield_ctx = ctx->yieldProcedureResults(); if (!yield_ctx) { const auto &maybe_found = diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index c26a331c2..b2276389b 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -52,7 +52,7 @@ explainQuery : EXPLAIN cypherQuery ; profileQuery : PROFILE cypherQuery ; -cypherQuery : singleQuery ( cypherUnion )* ; +cypherQuery : singleQuery ( cypherUnion )* ( queryMemoryLimit )? ; indexQuery : createIndex | dropIndex; @@ -106,14 +106,18 @@ with : WITH ( DISTINCT )? returnBody ( where )? ; cypherReturn : RETURN ( DISTINCT )? returnBody ; -callProcedure : CALL procedureName '(' ( expression ( ',' expression )* )? ')' ( callProcedureMemoryLimit )? ( yieldProcedureResults )? ; +callProcedure : CALL procedureName '(' ( expression ( ',' expression )* )? ')' ( procedureMemoryLimit )? ( yieldProcedureResults )? ; procedureName : symbolicName ( '.' symbolicName )* ; -callProcedureMemoryLimit : MEMORY ( UNLIMITED | LIMIT literal ( MB | KB ) ) ; - yieldProcedureResults : YIELD ( '*' | ( procedureResult ( ',' procedureResult )* ) ) ; +memoryLimit : MEMORY ( UNLIMITED | LIMIT literal ( MB | KB ) ) ; + +queryMemoryLimit : QUERY memoryLimit ; + +procedureMemoryLimit : PROCEDURE memoryLimit ; + procedureResult : ( variable AS variable ) | variable ; returnBody : returnItems ( order )? ( skip )? ( limit )? ; @@ -357,7 +361,9 @@ cypherKeyword : ALL | OPTIONAL | OR | ORDER + | PROCEDURE | PROFILE + | QUERY | REDUCE | REMOVE | RETURN diff --git a/src/query/frontend/opencypher/grammar/CypherLexer.g4 b/src/query/frontend/opencypher/grammar/CypherLexer.g4 index c36114c61..1377fbc82 100644 --- a/src/query/frontend/opencypher/grammar/CypherLexer.g4 +++ b/src/query/frontend/opencypher/grammar/CypherLexer.g4 @@ -118,7 +118,9 @@ ON : O N ; OPTIONAL : O P T I O N A L ; OR : O R ; ORDER : O R D E R ; +PROCEDURE : P R O C E D U R E ; PROFILE : P R O F I L E ; +QUERY : Q U E R Y ; REDUCE : R E D U C E ; REMOVE : R E M O V E ; RETURN : R E T U R N ; diff --git a/src/query/frontend/stripped.cpp b/src/query/frontend/stripped.cpp index 4f8988389..8f2a3e67d 100644 --- a/src/query/frontend/stripped.cpp +++ b/src/query/frontend/stripped.cpp @@ -168,7 +168,8 @@ StrippedQuery::StrippedQuery(const std::string &query) : original_(query) { for (; jt != tokens.end() && (jt->second != "," || num_open_braces || num_open_parantheses || num_open_brackets) && !utils::IEquals(jt->second, "order") && !utils::IEquals(jt->second, "skip") && - !utils::IEquals(jt->second, "limit") && !utils::IEquals(jt->second, "union") && jt->second != ";"; + !utils::IEquals(jt->second, "limit") && !utils::IEquals(jt->second, "union") && + !utils::IEquals(jt->second, "query") && jt->second != ";"; ++jt) { if (jt->second == "(") { ++num_open_parantheses; diff --git a/src/query/frontend/stripped_lexer_constants.hpp b/src/query/frontend/stripped_lexer_constants.hpp index 71949c4a7..a73b777a6 100644 --- a/src/query/frontend/stripped_lexer_constants.hpp +++ b/src/query/frontend/stripped_lexer_constants.hpp @@ -87,7 +87,7 @@ const trie::Trie kKeywords = { "single", "true", "false", "reduce", "coalesce", "user", "password", "alter", "drop", "show", "stats", "unique", "explain", "profile", "storage", "index", "info", "exists", "assert", "constraint", "node", "key", "dump", "database", "call", "yield", "memory", - "mb", "kb", "unlimited", "free"}; + "mb", "kb", "unlimited", "free", "procedure", "query"}; // Unicode codepoints that are allowed at the start of the unescaped name. const std::bitset kUnescapedNameAllowedStarts( diff --git a/src/query/interpret/eval.cpp b/src/query/interpret/eval.cpp new file mode 100644 index 000000000..6d32d38aa --- /dev/null +++ b/src/query/interpret/eval.cpp @@ -0,0 +1,24 @@ +#include "query/interpret/eval.hpp" + +namespace query { + +int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, const std::string &what) { + TypedValue value = expr->Accept(*evaluator); + try { + return value.ValueInt(); + } catch (TypedValueException &e) { + throw QueryRuntimeException(what + " must be an int"); + } +} + +std::optional EvaluateMemoryLimit(ExpressionEvaluator *eval, Expression *memory_limit, size_t memory_scale) { + if (!memory_limit) return std::nullopt; + auto limit_value = memory_limit->Accept(*eval); + if (!limit_value.IsInt() || limit_value.ValueInt() <= 0) + throw QueryRuntimeException("Memory limit must be a non-negative integer."); + size_t limit = limit_value.ValueInt(); + if (std::numeric_limits::max() / memory_scale < limit) throw QueryRuntimeException("Memory limit overflow."); + return limit * memory_scale; +} + +} // namespace query diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp index d0ffec3fc..40d0b1abb 100644 --- a/src/query/interpret/eval.hpp +++ b/src/query/interpret/eval.hpp @@ -656,13 +656,8 @@ class ExpressionEvaluator : public ExpressionVisitor { /// @param what - Name of what's getting evaluated. Used for user feedback (via /// exception) when the evaluated value is not an int. /// @throw QueryRuntimeException if expression doesn't evaluate to an int. -inline int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, const std::string &what) { - TypedValue value = expr->Accept(*evaluator); - try { - return value.ValueInt(); - } catch (TypedValueException &e) { - throw QueryRuntimeException(what + " must be an int"); - } -} +int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, const std::string &what); + +std::optional EvaluateMemoryLimit(ExpressionEvaluator *eval, Expression *memory_limit, size_t memory_scale); } // namespace query diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 4de436c88..7511aab03 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -33,6 +33,7 @@ #include "utils/pmr/unordered_map.hpp" #include "utils/pmr/unordered_set.hpp" #include "utils/pmr/vector.hpp" +#include "utils/readable_size.hpp" #include "utils/string.hpp" // macro for the default implementation of LogicalOperator::Accept @@ -3486,16 +3487,6 @@ std::unordered_map CallProcedure::GetAndResetCounters() { namespace { -std::optional EvalMemoryLimit(ExpressionEvaluator *eval, Expression *memory_limit, size_t memory_scale) { - if (!memory_limit) return std::nullopt; - auto limit_value = memory_limit->Accept(*eval); - if (!limit_value.IsInt() || limit_value.ValueInt() <= 0) - throw QueryRuntimeException("Memory limit must be a non-negative integer."); - size_t limit = limit_value.ValueInt(); - if (std::numeric_limits::max() / memory_scale < limit) throw QueryRuntimeException("Memory limit overflow."); - return limit * memory_scale; -} - void CallCustomProcedure(const std::string_view &fully_qualified_procedure_name, const mgp_proc &proc, const std::vector &args, const mgp_graph &graph, ExpressionEvaluator *evaluator, utils::MemoryResource *memory, std::optional memory_limit, mgp_result *result) { @@ -3545,7 +3536,8 @@ void CallCustomProcedure(const std::string_view &fully_qualified_procedure_name, proc_args.elems.emplace_back(std::get<2>(proc.opt_args[i]), &graph); } if (memory_limit) { - SPDLOG_INFO("Running '{}' with memory limit of {} bytes", fully_qualified_procedure_name, *memory_limit); + SPDLOG_INFO("Running '{}' with memory limit of {}", fully_qualified_procedure_name, + utils::GetReadableSize(*memory_limit)); utils::LimitedMemoryResource limited_mem(memory, *memory_limit); mgp_memory proc_memory{&limited_mem}; MG_ASSERT(result->signature == &proc.results); @@ -3624,7 +3616,7 @@ class CallProcedureCursor : public Cursor { // TODO: This will probably need to be changed when we add support for // generator like procedures which yield a new result on each invocation. auto *memory = context.evaluation_context.memory; - auto memory_limit = EvalMemoryLimit(&evaluator, self_->memory_limit_, self_->memory_scale_); + auto memory_limit = EvaluateMemoryLimit(&evaluator, self_->memory_limit_, self_->memory_scale_); mgp_graph graph{context.db_accessor, graph_view, &context}; CallCustomProcedure(self_->procedure_name_, *proc, self_->arguments_, graph, &evaluator, memory, memory_limit, &result_); diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index 46c7847fa..294cdbf24 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.cpp @@ -18,6 +18,7 @@ #include #include +#include "query/exceptions.hpp" #include "query/frontend/ast/ast.hpp" #include "query/frontend/ast/cypher_main_visitor.hpp" #include "query/frontend/opencypher/parser.hpp" @@ -2730,7 +2731,8 @@ TEST_P(CypherMainVisitorTest, CallWithoutYield) { TEST_P(CypherMainVisitorTest, CallWithMemoryLimitWithoutYield) { auto &ast_generator = *GetParam(); - auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() MEMORY LIMIT 32 KB")); + auto *query = + dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() PROCEDURE MEMORY LIMIT 32 KB")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; @@ -2747,7 +2749,7 @@ TEST_P(CypherMainVisitorTest, CallWithMemoryLimitWithoutYield) { TEST_P(CypherMainVisitorTest, CallWithMemoryUnlimitedWithoutYield) { auto &ast_generator = *GetParam(); - auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() MEMORY UNLIMITED")); + auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() PROCEDURE MEMORY UNLIMITED")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; @@ -2763,8 +2765,8 @@ TEST_P(CypherMainVisitorTest, CallWithMemoryUnlimitedWithoutYield) { TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryLimit) { auto &ast_generator = *GetParam(); - auto *query = - dynamic_cast(ast_generator.ParseQuery("CALL proc.with.dots() MEMORY LIMIT 32 MB YIELD res")); + auto *query = dynamic_cast( + ast_generator.ParseQuery("CALL proc.with.dots() PROCEDURE MEMORY LIMIT 32 MB YIELD res")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; @@ -2788,8 +2790,8 @@ TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryLimit) { TEST_P(CypherMainVisitorTest, CallProcedureWithMemoryUnlimited) { auto &ast_generator = *GetParam(); - auto *query = - dynamic_cast(ast_generator.ParseQuery("CALL proc.with.dots() MEMORY UNLIMITED YIELD res")); + auto *query = dynamic_cast( + ast_generator.ParseQuery("CALL proc.with.dots() PROCEDURE MEMORY UNLIMITED YIELD res")); ASSERT_TRUE(query); ASSERT_TRUE(query->single_query_); auto *single_query = query->single_query_; @@ -2822,12 +2824,10 @@ TEST_P(CypherMainVisitorTest, IncorrectCallProcedure) { ASSERT_THROW(ast_generator.ParseQuery("RETURN 42, CALL procedure() YIELD"), SyntaxException); ASSERT_THROW(ast_generator.ParseQuery("RETURN 42, CALL procedure() YIELD res"), SyntaxException); ASSERT_THROW(ast_generator.ParseQuery("RETURN 42 AS x CALL procedure() YIELD res"), SemanticException); - ASSERT_THROW(ast_generator.ParseQuery("CALL proc.with.dots() YIELD res MEMORY UNLIMITED"), SyntaxException); - ASSERT_THROW(ast_generator.ParseQuery("CALL proc.with.dots() YIELD res MEMORY LIMIT 32 KB"), SyntaxException); ASSERT_THROW(ast_generator.ParseQuery("CALL proc.with.dots() MEMORY YIELD res"), SyntaxException); // mg.procedures returns something, so it needs to have a YIELD. ASSERT_THROW(ast_generator.ParseQuery("CALL mg.procedures()"), SemanticException); - ASSERT_THROW(ast_generator.ParseQuery("CALL mg.procedures() MEMORY UNLIMITED"), SemanticException); + ASSERT_THROW(ast_generator.ParseQuery("CALL mg.procedures() PROCEDURE MEMORY UNLIMITED"), SemanticException); // TODO: Implement support for the following syntax. These are defined in // Neo4j and accepted in openCypher CIP. ASSERT_THROW(ast_generator.ParseQuery("CALL proc"), SyntaxException); @@ -2935,4 +2935,115 @@ TEST_P(CypherMainVisitorTest, TestLoadCsvClause) { } } +TEST_P(CypherMainVisitorTest, MemoryLimit) { + auto &ast_generator = *GetParam(); + + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUE"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUERY"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUERY MEM"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUERY MEMORY"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUERY MEMORY LIM"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUERY MEMORY LIMIT"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUERY MEMORY LIMIT KB"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("RETURN x QUERY MEMORY LIMIT 12GB"), SyntaxException); + ASSERT_THROW(ast_generator.ParseQuery("QUERY MEMORY LIMIT 12KB RETURN x"), SyntaxException); + + { + auto *query = dynamic_cast(ast_generator.ParseQuery("RETURN x")); + ASSERT_TRUE(query); + ASSERT_FALSE(query->memory_limit_); + } + + { + auto *query = dynamic_cast(ast_generator.ParseQuery("RETURN x QUERY MEMORY LIMIT 12KB")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->memory_limit_); + ast_generator.CheckLiteral(query->memory_limit_, 12); + ASSERT_EQ(query->memory_scale_, 1024U); + } + + { + auto *query = dynamic_cast(ast_generator.ParseQuery("RETURN x QUERY MEMORY LIMIT 12MB")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->memory_limit_); + ast_generator.CheckLiteral(query->memory_limit_, 12); + ASSERT_EQ(query->memory_scale_, 1024U * 1024U); + } + + { + auto *query = dynamic_cast( + ast_generator.ParseQuery("CALL mg.procedures() YIELD x RETURN x QUERY MEMORY LIMIT 12MB")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->memory_limit_); + ast_generator.CheckLiteral(query->memory_limit_, 12); + ASSERT_EQ(query->memory_scale_, 1024U * 1024U); + + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + ASSERT_EQ(single_query->clauses_.size(), 2U); + auto *call_proc = dynamic_cast(single_query->clauses_[0]); + CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); + } + + { + auto *query = dynamic_cast(ast_generator.ParseQuery( + "CALL mg.procedures() PROCEDURE MEMORY LIMIT 3KB YIELD x RETURN x QUERY MEMORY LIMIT 12MB")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->memory_limit_); + ast_generator.CheckLiteral(query->memory_limit_, 12); + ASSERT_EQ(query->memory_scale_, 1024U * 1024U); + + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + ASSERT_EQ(single_query->clauses_.size(), 2U); + auto *call_proc = dynamic_cast(single_query->clauses_[0]); + ASSERT_TRUE(call_proc->memory_limit_); + ast_generator.CheckLiteral(call_proc->memory_limit_, 3); + ASSERT_EQ(call_proc->memory_scale_, 1024U); + } + + { + auto *query = dynamic_cast( + ast_generator.ParseQuery("CALL mg.procedures() PROCEDURE MEMORY LIMIT 3KB YIELD x RETURN x")); + ASSERT_TRUE(query); + ASSERT_FALSE(query->memory_limit_); + + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + ASSERT_EQ(single_query->clauses_.size(), 2U); + auto *call_proc = dynamic_cast(single_query->clauses_[0]); + ASSERT_TRUE(call_proc->memory_limit_); + ast_generator.CheckLiteral(call_proc->memory_limit_, 3); + ASSERT_EQ(call_proc->memory_scale_, 1024U); + } + + { + auto *query = + dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() PROCEDURE MEMORY LIMIT 3KB")); + ASSERT_TRUE(query); + ASSERT_FALSE(query->memory_limit_); + + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + ASSERT_EQ(single_query->clauses_.size(), 1U); + auto *call_proc = dynamic_cast(single_query->clauses_[0]); + ASSERT_TRUE(call_proc->memory_limit_); + ast_generator.CheckLiteral(call_proc->memory_limit_, 3); + ASSERT_EQ(call_proc->memory_scale_, 1024U); + } + + { + auto *query = dynamic_cast(ast_generator.ParseQuery("CALL mg.load_all() QUERY MEMORY LIMIT 3KB")); + ASSERT_TRUE(query); + ASSERT_TRUE(query->memory_limit_); + ast_generator.CheckLiteral(query->memory_limit_, 3); + ASSERT_EQ(query->memory_scale_, 1024U); + + ASSERT_TRUE(query->single_query_); + auto *single_query = query->single_query_; + ASSERT_EQ(single_query->clauses_.size(), 1U); + auto *call_proc = dynamic_cast(single_query->clauses_[0]); + CheckCallProcedureDefaultMemoryLimit(ast_generator, *call_proc); + } +} } // namespace