diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e7c15fa..4f0ea4270 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,15 @@ ### Major Features and Improvements * User specified transaction execution timeout. +* Support for query parameters (except for parameters in place of property maps). ## v0.6.0 ### Major Features and Improvements -* AST caching -* Label + property index support -* Different logging setup & format +* AST caching. +* Label + property index support. +* Different logging setup & format. ## v0.5.0 @@ -27,7 +28,7 @@ ### Bug Fixes and Other Changes * Fixed race condition in MVCC. Hints exp+aborted race condition prevented. -* Fixed conceptual bug in MVCC GC. Evaluate old records w.r.t. the oldest +* Fixed conceptual bug in MVCC GC. Evaluate old records w.r.t. the oldest. transaction's id AND snapshot. * User friendly error messages thrown from the query engine. @@ -35,33 +36,33 @@ ### Bug Fixes and Other Changes -* List indexing supported with preceeding IN (for example in query `RETURN 1 IN [[1,2]][0]`) +* List indexing supported with preceeding IN (for example in query `RETURN 1 IN [[1,2]][0]`). ## Build 825 ### Major Features and Improvements -* RETURN *, count(*), OPTIONAL MATCH, UNWIND, DISTINCT (except DISTINCT in aggregate functions), list indexing and slicing, escaped labels, IN LIST operator, range function +* RETURN *, count(*), OPTIONAL MATCH, UNWIND, DISTINCT (except DISTINCT in aggregate functions), list indexing and slicing, escaped labels, IN LIST operator, range function. ### Bug Fixes and Other Changes -* TCP_NODELAY -> import should be faster -* Clear hint bits +* TCP_NODELAY -> import should be faster. +* Clear hint bits. ## Build 783 ### Major Features and Improvements -* SKIP, LIMIT, ORDER BY -* Math functions -* Initial support for MERGE clause +* SKIP, LIMIT, ORDER BY. +* Math functions. +* Initial support for MERGE clause. ### Bug Fixes and Other Changes -* Unhandled Lock Timeout Exception +* Unhandled Lock Timeout Exception. ## Build 755 ### Major Features and Improvements -* MATCH, CREATE, WHERE, SET, REMOVE, DELETE +* MATCH, CREATE, WHERE, SET, REMOVE, DELETE. diff --git a/src/communication/bolt/v1/states/idle_result.hpp b/src/communication/bolt/v1/states/idle_result.hpp index 6cd02ff20..7d93242b0 100644 --- a/src/communication/bolt/v1/states/idle_result.hpp +++ b/src/communication/bolt/v1/states/idle_result.hpp @@ -1,5 +1,6 @@ #pragma once +#include <map> #include <string> #include <glog/logging.h> @@ -59,7 +60,8 @@ State StateIdleResultRun(Session &session, State state) { try { DLOG(INFO) << fmt::format("[Run] '{}'", query.Value<std::string>()); auto is_successfully_executed = session.query_engine_.Run( - query.Value<std::string>(), *db_accessor, session.output_stream_); + query.Value<std::string>(), *db_accessor, session.output_stream_, + params.Value<std::map<std::string, query::TypedValue>>()); if (!is_successfully_executed) { // abort transaction diff --git a/src/query/common.cpp b/src/query/common.cpp index 718d7fd18..95b5de321 100644 --- a/src/query/common.cpp +++ b/src/query/common.cpp @@ -113,4 +113,21 @@ double ParseDoubleLiteral(const std::string &s) { throw SemanticException("Couldn't parse string to double"); } } + +std::string ParseParameter(const std::string &s) { + debug_assert(s[0] == '$', "Invalid string passed as parameter name"); + if (s[1] != '`') return s.substr(1); + // If parameter name is escaped symbolic name then symbolic name should be + // unescaped and leading and trailing backquote should be removed. + debug_assert(s.size() > 3U && s.back() == '`', + "Invalid string passed as parameter name"); + std::string out; + for (int i = 2; i < static_cast<int>(s.size()) - 1; ++i) { + if (s[i] == '`') { + ++i; + } + out.push_back(s[i]); + } + return out; +} } diff --git a/src/query/common.hpp b/src/query/common.hpp index d354b68c7..fd50b2102 100644 --- a/src/query/common.hpp +++ b/src/query/common.hpp @@ -5,10 +5,12 @@ namespace query { -// These are the functions for parsing literals from opepncypher query. +// These are the functions for parsing literals and parameter names from +// opencypher query. int64_t ParseIntegerLiteral(const std::string &s); std::string ParseStringLiteral(const std::string &s); double ParseDoubleLiteral(const std::string &s); +std::string ParseParameter(const std::string &s); /** * Indicates that some part of query execution should diff --git a/src/query/console.cpp b/src/query/console.cpp index 60597c81e..f0b2dd0f5 100644 --- a/src/query/console.cpp +++ b/src/query/console.cpp @@ -140,7 +140,7 @@ void query::Repl(Dbms &dbms) { try { auto dba = dbms.active(); ResultStreamFaker results; - interpeter.Interpret(command, *dba, results); + interpeter.Interpret(command, *dba, results, {}); PrintResults(results); dba->commit(); } catch (const query::SyntaxException &e) { diff --git a/src/query/engine.hpp b/src/query/engine.hpp index 45cd4d243..0a2061555 100644 --- a/src/query/engine.hpp +++ b/src/query/engine.hpp @@ -14,6 +14,7 @@ namespace fs = std::experimental::filesystem; #include "query/plan_interface.hpp" #include "utils/datetime/timestamp.hpp" #include "utils/dynamic_lib.hpp" +#include "utils/exceptions.hpp" #include "utils/timer.hpp" DECLARE_bool(interpret); @@ -66,12 +67,18 @@ class QueryEngine { * true if query execution was successfull */ auto Run(const std::string &query, GraphDbAccessor &db_accessor, - Stream &stream) { + Stream &stream, + const std::map<std::string, query::TypedValue> ¶ms) { if (FLAGS_interpret) { - interpreter_.Interpret(query, db_accessor, stream); + interpreter_.Interpret(query, db_accessor, stream, params); return true; } + if (!params.empty()) { + throw utils::NotYetImplemented( + "Params not yet implemented in compiled queries"); + } + utils::Timer parsing_timer; query::StrippedQuery stripped(query); auto parsing_time = parsing_timer.Elapsed(); diff --git a/src/query/exceptions.hpp b/src/query/exceptions.hpp index a8651cbb7..de81da0ab 100644 --- a/src/query/exceptions.hpp +++ b/src/query/exceptions.hpp @@ -66,6 +66,12 @@ class HintedAbortError : public QueryException { HintedAbortError() : QueryException("") {} }; +class UnprovidedParameterError : public QueryException { + public: + using QueryException::QueryException; + UnprovidedParameterError() : QueryException("") {} +}; + /** * An exception for an illegal operation that can not be detected * before the query starts executing over data. diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp index ab08ac26b..a1b53863e 100644 --- a/src/query/frontend/ast/cypher_main_visitor.cpp +++ b/src/query/frontend/ast/cypher_main_visitor.cpp @@ -738,8 +738,8 @@ antlrcpp::Any CypherMainVisitor::visitAtom(CypherParser::AtomContext *ctx) { return static_cast<Expression *>( ctx->literal()->accept(this).as<BaseLiteral *>()); } else if (ctx->parameter()) { - // TODO: implement other clauses. - throw utils::NotYetImplemented("atom parameters"); + return static_cast<Expression *>( + ctx->parameter()->accept(this).as<PrimitiveLiteral *>()); } else if (ctx->parenthesizedExpression()) { return static_cast<Expression *>( ctx->parenthesizedExpression()->accept(this)); @@ -761,6 +761,15 @@ antlrcpp::Any CypherMainVisitor::visitAtom(CypherParser::AtomContext *ctx) { throw utils::NotYetImplemented("atom expression '{}'", ctx->getText()); } +antlrcpp::Any CypherMainVisitor::visitParameter( + CypherParser::ParameterContext *ctx) { + return storage_.Create<PrimitiveLiteral>( + ctx->getText(), // Not really important since we do parameter + // substitution by token position not by its name. + // Lookup by name is already done in stage before. + ctx->getStart()->getTokenIndex()); +} + antlrcpp::Any CypherMainVisitor::visitLiteral( CypherParser::LiteralContext *ctx) { int token_position = ctx->getStart()->getTokenIndex(); diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp index 6c45818be..855a4d54a 100644 --- a/src/query/frontend/ast/cypher_main_visitor.hpp +++ b/src/query/frontend/ast/cypher_main_visitor.hpp @@ -419,6 +419,11 @@ class CypherMainVisitor : public antlropencypher::CypherBaseVisitor { */ antlrcpp::Any visitAtom(CypherParser::AtomContext *ctx) override; + /** + * @return PrimitiveLiteral* + */ + antlrcpp::Any visitParameter(CypherParser::ParameterContext *ctx) override; + /** * @return Expression* */ diff --git a/src/query/frontend/stripped.cpp b/src/query/frontend/stripped.cpp index 6aad045e1..e1d93f32d 100644 --- a/src/query/frontend/stripped.cpp +++ b/src/query/frontend/stripped.cpp @@ -28,6 +28,7 @@ StrippedQuery::StrippedQuery(const std::string &query) : original_(query) { STRING, INT, // Decimal, octal and hexadecimal. REAL, + PARAMETER, ESCAPED_NAME, UNESCAPED_NAME, SPACE @@ -50,6 +51,7 @@ StrippedQuery::StrippedQuery(const std::string &query) : original_(query) { update(MatchOctalInt(i), Token::INT); update(MatchHexadecimalInt(i), Token::INT); update(MatchReal(i), Token::REAL); + update(MatchParameter(i), Token::PARAMETER); update(MatchEscapedName(i), Token::ESCAPED_NAME); update(MatchUnescapedName(i), Token::UNESCAPED_NAME); update(MatchWhitespaceAndComments(i), Token::SPACE); @@ -77,8 +79,10 @@ StrippedQuery::StrippedQuery(const std::string &query) : original_(query) { const auto &token = tokens[i]; // Position is calculated in query after stripping and whitespace // normalisation, not before. There will be twice as much tokens before - // this one because space tokens will be inserted between every one. - int token_index = token_strings.size() * 2; + // this one because space tokens will be inserted between every one we also + // need to shift token index for every parameter since antlr's parser thinks + // of parameter as two tokens. + int token_index = token_strings.size() * 2 + parameters_.size(); switch (token.first) { case Token::UNMATCHED: debug_assert(false, "Shouldn't happen"); @@ -113,6 +117,10 @@ StrippedQuery::StrippedQuery(const std::string &query) : original_(query) { case Token::UNESCAPED_NAME: token_strings.push_back(token.second); break; + case Token::PARAMETER: + parameters_[token_index] = ParseParameter(token.second); + token_strings.push_back(token.second); + break; } if (token.first != Token::SPACE) { @@ -375,6 +383,19 @@ int StrippedQuery::MatchReal(int start) const { return i - start; } +int StrippedQuery::MatchParameter(int start) const { + int len = original_.size(); + if (start + 1 == len) return 0; + if (original_[start] != '$') return 0; + int max_len = 0; + max_len = std::max(max_len, MatchUnescapedName(start + 1)); + max_len = std::max(max_len, MatchEscapedName(start + 1)); + max_len = std::max(max_len, MatchKeyword(start + 1)); + max_len = std::max(max_len, MatchDecimalInt(start + 1)); + if (max_len == 0) return 0; + return 1 + max_len; +} + int StrippedQuery::MatchEscapedName(int start) const { int len = original_.size(); int i = start; diff --git a/src/query/frontend/stripped.hpp b/src/query/frontend/stripped.hpp index 9e1a3d397..d46a1a96f 100644 --- a/src/query/frontend/stripped.hpp +++ b/src/query/frontend/stripped.hpp @@ -48,8 +48,9 @@ class StrippedQuery { StrippedQuery &operator=(StrippedQuery &&other) = default; const std::string &query() const { return query_; } - auto &literals() const { return literals_; } - auto &named_expressions() const { return named_exprs_; } + const auto &literals() const { return literals_; } + const auto &named_expressions() const { return named_exprs_; } + const auto ¶meters() const { return parameters_; } HashType hash() const { return hash_; } private: @@ -63,6 +64,7 @@ class StrippedQuery { int MatchOctalInt(int start) const; int MatchHexadecimalInt(int start) const; int MatchReal(int start) const; + int MatchParameter(int start) const; int MatchEscapedName(int start) const; int MatchUnescapedName(int start) const; int MatchWhitespaceAndComments(int start) const; @@ -74,8 +76,14 @@ class StrippedQuery { std::string query_; // Token positions of stripped out literals mapped to their values. + // TODO: Parameters class really doesn't provided anything interesting. This + // could be changed to std::unordered_map, but first we need to rewrite (or + // get rid of) hardcoded queries which expect Parameters. Parameters literals_; + // Token positions of query parameters mapped to theirs names. + std::unordered_map<int, std::string> parameters_; + // Token positions of nonaliased named expressions in return statement mapped // to theirs original/unstripped string. std::unordered_map<int, std::string> named_exprs_; diff --git a/src/query/interpreter.hpp b/src/query/interpreter.hpp index 3414cc59e..f78efbedf 100644 --- a/src/query/interpreter.hpp +++ b/src/query/interpreter.hpp @@ -8,6 +8,7 @@ #include "database/graph_db_accessor.hpp" #include "query/context.hpp" +#include "query/exceptions.hpp" #include "query/frontend/ast/cypher_main_visitor.hpp" #include "query/frontend/opencypher/parser.hpp" #include "query/frontend/semantic/symbol_generator.hpp" @@ -28,7 +29,8 @@ class Interpreter { Interpreter() {} template <typename Stream> void Interpret(const std::string &query, GraphDbAccessor &db_accessor, - Stream &stream) { + Stream &stream, + const std::map<std::string, TypedValue> ¶ms) { utils::Timer frontend_timer; Config config; Context ctx(config, db_accessor); @@ -37,6 +39,13 @@ class Interpreter { // stripped query -> high level tree AstTreeStorage ast_storage = [&]() { if (!FLAGS_ast_cache) { + // This is totally fine, since we don't really expect anyone to turn off + // the cache. + if (!params.empty()) { + throw utils::NotYetImplemented( + "Params not implemented if ast cache is turned off"); + } + // stripped query -> AST frontend::opencypher::Parser parser(query); auto low_level_tree = parser.tree(); @@ -67,7 +76,20 @@ class Interpreter { CachedAst(std::move(visitor.storage()))) .first; } - return it->second.Plug(stripped.literals(), stripped.named_expressions()); + + // Update literals map with provided parameters. + auto literals = stripped.literals(); + for (const auto ¶m_pair : stripped.parameters()) { + auto param_it = params.find(param_pair.second); + if (param_it == params.end()) { + throw query::UnprovidedParameterError( + fmt::format("Parameter$ {} not provided", param_pair.second)); + } + literals.Add(param_pair.first, param_it->second); + } + + // Plug literals, parameters and named expressions. + return it->second.Plug(literals, stripped.named_expressions()); }(); auto frontend_time = frontend_timer.Elapsed(); diff --git a/tests/integration/query_engine_common.hpp b/tests/integration/query_engine_common.hpp index bcdbe3be4..58243288a 100644 --- a/tests/integration/query_engine_common.hpp +++ b/tests/integration/query_engine_common.hpp @@ -134,7 +134,7 @@ auto ExecuteQueryPlans(QueryEngineT &engine, Dbms &dbms, const fs::path &path, // Create new db_accessor since one query is associated with one // transaction. auto db_accessor = dbms.active(); - engine.Run(query, *db_accessor, stream); + engine.Run(query, *db_accessor, stream, {}); } } diff --git a/tests/manual/query_engine.cpp b/tests/manual/query_engine.cpp index 2326f51f8..fef039daa 100644 --- a/tests/manual/query_engine.cpp +++ b/tests/manual/query_engine.cpp @@ -65,7 +65,7 @@ int main(int argc, char *argv[]) { try { query_engine.ReloadCustom(query, event.path); auto db_accessor = dbms.active(); - query_engine.Run(query, *db_accessor, stream); + query_engine.Run(query, *db_accessor, stream, {}); } catch (query::PlanCompilationException &e) { DLOG(ERROR) << fmt::format("Query compilation failed: {}", e.what()); diff --git a/tests/qa/tck_engine/tests/memgraph_V1/features/parameters.feature b/tests/qa/tck_engine/tests/memgraph_V1/features/parameters.feature new file mode 100644 index 000000000..908507e43 --- /dev/null +++ b/tests/qa/tck_engine/tests/memgraph_V1/features/parameters.feature @@ -0,0 +1,68 @@ +Feature: Parameters + + Scenario: Simple parameter names: + Given an empty graph + And parameters are: + | y | 2 | + | x | 1 | + When executing query: + """ + RETURN $x, $y, 5 + """ + Then the result should be: + | $x | $y | 5 | + | 1 | 2 | 5 | + + Scenario: Integers as parameter names: + Given an empty graph + And parameters are: + | 0 | 5 | + | 2 | 6 | + When executing query: + """ + RETURN $0, $2 + """ + Then the result should be: + | $0 | $2 | + | 5 | 6 | + + Scenario: Escaped symbolic names as parameter names: + Given an empty graph + And parameters are: + | a b | 2 | + | a `b | 3 | + When executing query: + """ + RETURN $`a b`, $`a ``b` + """ + Then the result should be: + | $`a b` | $`a ``b` | + | 2 | 3 | + + Scenario: Lists as parameters: + Given an empty graph + And parameters are: + | a | [1, 2, 3] | + When executing query: + """ + RETURN $a + """ + Then the result should be: + | $a | + | [1, 2, 3] | + + Scenario: Parameters in match: + Given an empty graph + And having executed: + """ + CREATE (a {x : 10}) + """ + And parameters are: + | a | 10 | + When executing query: + """ + MATCH (a {x : $a}) RETURN a.x + """ + Then the result should be: + | a.x | + | 10 | diff --git a/tests/unit/database_transaction_timeout.cpp b/tests/unit/database_transaction_timeout.cpp index d30808efc..dbb19989b 100644 --- a/tests/unit/database_transaction_timeout.cpp +++ b/tests/unit/database_transaction_timeout.cpp @@ -15,19 +15,19 @@ TEST(TransactionTimeout, TransactionTimeout) { { ResultStreamFaker stream; auto dba1 = dbms.active(); - engine.Run("MATCH (n) RETURN n", *dba1, stream); + engine.Run("MATCH (n) RETURN n", *dba1, stream, {}); } { ResultStreamFaker stream; auto dba2 = dbms.active(); std::this_thread::sleep_for(std::chrono::seconds(5)); - ASSERT_THROW(engine.Run("MATCH (n) RETURN n", *dba2, stream), + ASSERT_THROW(engine.Run("MATCH (n) RETURN n", *dba2, stream, {}), query::HintedAbortError); } { ResultStreamFaker stream; auto dba3 = dbms.active(); - engine.Run("MATCH (n) RETURN n", *dba3, stream); + engine.Run("MATCH (n) RETURN n", *dba3, stream, {}); } } diff --git a/tests/unit/query_engine.cpp b/tests/unit/query_engine.cpp index 3f82421a1..6f1f4f017 100644 --- a/tests/unit/query_engine.cpp +++ b/tests/unit/query_engine.cpp @@ -1,9 +1,12 @@ #include "communication/result_stream_faker.hpp" -#include "database/graph_db_accessor.hpp" #include "database/dbms.hpp" +#include "database/graph_db_accessor.hpp" #include "gmock/gmock.h" #include "gtest/gtest.h" #include "query/engine.hpp" +#include "query/exceptions.hpp" +#include "query/typed_value.hpp" +#include "query_common.hpp" // TODO: This is not a unit test, but tests/integration dir is chaotic at the // moment. After tests refactoring is done, move/rename this. @@ -18,7 +21,7 @@ TEST(QueryEngine, AstCache) { { ResultStreamFaker stream; auto dba = dbms.active(); - engine.Run("RETURN 2 + 3", *dba, stream); + engine.Run("RETURN 2 + 3", *dba, stream, {}); ASSERT_EQ(stream.GetHeader().size(), 1U); EXPECT_EQ(stream.GetHeader()[0], "2 + 3"); ASSERT_EQ(stream.GetResults().size(), 1U); @@ -29,7 +32,7 @@ TEST(QueryEngine, AstCache) { // Cached ast, different literals. ResultStreamFaker stream; auto dba = dbms.active(); - engine.Run("RETURN 5 + 4", *dba, stream); + engine.Run("RETURN 5 + 4", *dba, stream, {}); ASSERT_EQ(stream.GetResults().size(), 1U); ASSERT_EQ(stream.GetResults()[0].size(), 1U); ASSERT_EQ(stream.GetResults()[0][0].Value<int64_t>(), 9); @@ -38,7 +41,7 @@ TEST(QueryEngine, AstCache) { // Different ast (because of different types). ResultStreamFaker stream; auto dba = dbms.active(); - engine.Run("RETURN 5.5 + 4", *dba, stream); + engine.Run("RETURN 5.5 + 4", *dba, stream, {}); ASSERT_EQ(stream.GetResults().size(), 1U); ASSERT_EQ(stream.GetResults()[0].size(), 1U); ASSERT_EQ(stream.GetResults()[0][0].Value<double>(), 9.5); @@ -47,7 +50,7 @@ TEST(QueryEngine, AstCache) { // Cached ast, same literals. ResultStreamFaker stream; auto dba = dbms.active(); - engine.Run("RETURN 2 + 3", *dba, stream); + engine.Run("RETURN 2 + 3", *dba, stream, {}); ASSERT_EQ(stream.GetResults().size(), 1U); ASSERT_EQ(stream.GetResults()[0].size(), 1U); ASSERT_EQ(stream.GetResults()[0][0].Value<int64_t>(), 5); @@ -56,7 +59,7 @@ TEST(QueryEngine, AstCache) { // Cached ast, different literals. ResultStreamFaker stream; auto dba = dbms.active(); - engine.Run("RETURN 10.5 + 1", *dba, stream); + engine.Run("RETURN 10.5 + 1", *dba, stream, {}); ASSERT_EQ(stream.GetResults().size(), 1U); ASSERT_EQ(stream.GetResults()[0].size(), 1U); ASSERT_EQ(stream.GetResults()[0][0].Value<double>(), 11.5); @@ -65,7 +68,7 @@ TEST(QueryEngine, AstCache) { // Cached ast, same literals, different whitespaces. ResultStreamFaker stream; auto dba = dbms.active(); - engine.Run("RETURN 10.5 + 1", *dba, stream); + engine.Run("RETURN 10.5 + 1", *dba, stream, {}); ASSERT_EQ(stream.GetResults().size(), 1U); ASSERT_EQ(stream.GetResults()[0].size(), 1U); ASSERT_EQ(stream.GetResults()[0][0].Value<double>(), 11.5); @@ -74,7 +77,7 @@ TEST(QueryEngine, AstCache) { // Cached ast, same literals, different named header. ResultStreamFaker stream; auto dba = dbms.active(); - engine.Run("RETURN 10.5+1", *dba, stream); + engine.Run("RETURN 10.5+1", *dba, stream, {}); ASSERT_EQ(stream.GetHeader().size(), 1U); EXPECT_EQ(stream.GetHeader()[0], "10.5+1"); ASSERT_EQ(stream.GetResults().size(), 1U); @@ -82,4 +85,68 @@ TEST(QueryEngine, AstCache) { ASSERT_EQ(stream.GetResults()[0][0].Value<double>(), 11.5); } } + +// Run query with same ast multiple times with different parameters. +TEST(QueryEngine, Parameters) { + QueryEngine<ResultStreamFaker> engine; + Dbms dbms; + { + ResultStreamFaker stream; + auto dba = dbms.active(); + engine.Run("RETURN $2 + $`a b`", *dba, stream, {{"2", 10}, {"a b", 15}}); + ASSERT_EQ(stream.GetHeader().size(), 1U); + EXPECT_EQ(stream.GetHeader()[0], "$2 + $`a b`"); + ASSERT_EQ(stream.GetResults().size(), 1U); + ASSERT_EQ(stream.GetResults()[0].size(), 1U); + ASSERT_EQ(stream.GetResults()[0][0].Value<int64_t>(), 25); + } + { + // Not needed parameter. + ResultStreamFaker stream; + auto dba = dbms.active(); + engine.Run("RETURN $2 + $`a b`", *dba, stream, + {{"2", 10}, {"a b", 15}, {"c", 10}}); + ASSERT_EQ(stream.GetHeader().size(), 1U); + EXPECT_EQ(stream.GetHeader()[0], "$2 + $`a b`"); + ASSERT_EQ(stream.GetResults().size(), 1U); + ASSERT_EQ(stream.GetResults()[0].size(), 1U); + ASSERT_EQ(stream.GetResults()[0][0].Value<int64_t>(), 25); + } + { + // Cached ast, different parameters. + ResultStreamFaker stream; + auto dba = dbms.active(); + engine.Run("RETURN $2 + $`a b`", *dba, stream, + {{"2", "da"}, {"a b", "ne"}}); + ASSERT_EQ(stream.GetResults().size(), 1U); + ASSERT_EQ(stream.GetResults()[0].size(), 1U); + ASSERT_EQ(stream.GetResults()[0][0].Value<std::string>(), "dane"); + } + { + // Non-primitive literal. + ResultStreamFaker stream; + auto dba = dbms.active(); + engine.Run("RETURN $2", *dba, stream, + {{"2", std::vector<query::TypedValue>{5, 2, 3}}}); + ASSERT_EQ(stream.GetResults().size(), 1U); + ASSERT_EQ(stream.GetResults()[0].size(), 1U); + auto result = query::test_common::ToInt64List( + stream.GetResults()[0][0].Value<std::vector<query::TypedValue>>()); + ASSERT_THAT(result, testing::ElementsAre(5, 2, 3)); + } + { + // Cached ast, unprovided parameter. + ResultStreamFaker stream; + auto dba = dbms.active(); + ASSERT_THROW(engine.Run("RETURN $2 + $`a b`", *dba, stream, + {{"2", "da"}, {"ab", "ne"}}), + query::UnprovidedParameterError); + } +} +} + +int main(int argc, char **argv) { + google::InitGoogleLogging(argv[0]); + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); } diff --git a/tests/unit/stripped.cpp b/tests/unit/stripped.cpp index 2364be565..1ce59ae6b 100644 --- a/tests/unit/stripped.cpp +++ b/tests/unit/stripped.cpp @@ -287,4 +287,16 @@ TEST(QueryStripper, ReturnListsAndFunctionCalls) { UnorderedElementsAre(Pair(2, "[1,2,[3, 4] , 5]"), Pair(30, "f(1, 2)"), Pair(44, "3"))); } + +TEST(QueryStripper, Parameters) { + StrippedQuery stripped("RETURN $123, $pero, $`mirko ``slavko`"); + EXPECT_EQ(stripped.literals().size(), 0); + EXPECT_EQ(stripped.query(), "return $123 , $pero , $`mirko ``slavko`"); + EXPECT_THAT(stripped.parameters(), + UnorderedElementsAre(Pair(2, "123"), Pair(7, "pero"), + Pair(12, "mirko `slavko"))); + EXPECT_THAT(stripped.named_expressions(), + UnorderedElementsAre(Pair(2, "$123"), Pair(7, "$pero"), + Pair(12, "$`mirko ``slavko`"))); +} }