diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..1dbcb90f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# git apply needs LF context lines to match an LF-checked-out source tree; +# Windows autocrlf would otherwise rewrite these to CRLF and break the patch. +*.patch text eol=lf diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c45a642f..5fc08611 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -119,6 +119,14 @@ if(MGCONSOLE_ON_WINDOWS) add_compile_options(-Wno-narrowing) endif() +# Parameter handling (`:param`/`:params`). Kept as its own library with a +# minimal dependency surface (mgclient only) so it can be unit tested. +add_library(params STATIC parameters.cpp) +add_dependencies(params mgclient) +target_compile_definitions(params PUBLIC MGCLIENT_STATIC_DEFINE) +target_include_directories(params PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${MGCLIENT_INCLUDE_DIRS}) +target_link_libraries(params ${MGCLIENT_LIBRARY}) + add_executable(mgconsole main.cpp interactive.cpp serial_import.cpp batch_import.cpp parsing.cpp) target_compile_definitions(mgconsole PRIVATE MGCLIENT_STATIC_DEFINE) target_include_directories(mgconsole @@ -131,6 +139,7 @@ target_link_libraries(mgconsole PRIVATE ${GFLAGS_LIBRARY} utils + params ${MGCLIENT_LIBRARY} ${OPENSSL_LIBRARIES}) if(MGCONSOLE_ON_WINDOWS) diff --git a/src/interactive.cpp b/src/interactive.cpp index ad076b25..c533d09a 100644 --- a/src/interactive.cpp +++ b/src/interactive.cpp @@ -15,16 +15,78 @@ #include "interactive.hpp" +#include #include #include +#include "parameters.hpp" #include "utils/constants.hpp" namespace mode::interactive { using namespace std::string_literals; +namespace { + +namespace params = query::params; + +// Evaluates a Cypher expression server-side and returns a copy of the resulting +// value. Existing parameters are made available to the expression. +mg_memory::MgValuePtr EvaluateParamExpression(mg_session *session, const std::string &expression, + const params::ParamStore &store) { + auto result = query::ExecuteQuery(session, "RETURN " + expression, store.AsMap().get()); + if (result.records.empty() || mg_list_size(result.records.front().get()) == 0) { + throw utils::ClientQueryException("expression did not produce a value"); + } + const mg_value *value = mg_list_at(result.records.front().get(), 0); + return mg_memory::MakeCustomUnique(mg_value_copy(value)); +} + +void ListParams(const params::ParamStore &store) { + if (store.Empty()) { + console::EchoInfo("No parameters set"); + return; + } + for (const auto &name : store.Names()) { + std::ostringstream os; + os << name << ": "; + utils::PrintValue(os, store.Get(name)); + console::EchoInfo(os.str()); + } +} + +// Handles a `:param`/`:params` command line. Query-level failures (e.g. a bad +// expression) are reported without aborting the shell; fatal connection +// failures propagate to the reconnect logic in Run. +void HandleParamCommand(mg_session *session, params::ParamStore &store, const std::string &line) { + const auto parsed = params::ParseParamCommand(line); + if (!parsed.command) { + console::EchoFailure("Invalid parameter command", parsed.error); + return; + } + switch (parsed.command->kind) { + case params::ParamCommand::Kind::kSet: + try { + auto value = EvaluateParamExpression(session, parsed.command->expression, store); + store.Set(parsed.command->name, value.get()); + console::EchoInfo("Set parameter '" + parsed.command->name + "'"); + } catch (const utils::ClientQueryException &e) { + console::EchoFailure("Failed to evaluate parameter expression", e.what()); + } + break; + case params::ParamCommand::Kind::kList: + ListParams(store); + break; + case params::ParamCommand::Kind::kClear: + store.Clear(); + console::EchoInfo("Cleared all parameters"); + break; + } +} + +} // namespace + int Run(utils::bolt::Config &bolt_config, const std::string &history, bool no_history, bool verbose_execution_info, const format::CsvOptions &csv_opts, const format::OutputOptions &output_opts) { Replxx *replxx_instance = InitAndSetupReplxx(); @@ -97,6 +159,9 @@ int Run(utils::bolt::Config &bolt_config, const std::string &history, bool no_hi return 1; } + // Query parameters set via `:param`, passed to every executed query. + params::ParamStore param_store; + console::EchoInfo("mgconsole "s + gflags::VersionString()); console::EchoInfo("Connected to 'memgraph://" + bolt_config.host + ":" + std::to_string(bolt_config.port) + "'"); console::EchoInfo("Type :help for shell usage"); @@ -113,7 +178,16 @@ int Run(utils::bolt::Config &bolt_config, const std::string &history, bool no_hi } try { - auto ret = query::ExecuteQuery(session.get(), query->query); + if (query->is_param_command) { + HandleParamCommand(session.get(), param_store, query->query); + auto history_ret = save_history(); + if (history_ret != 0) { + cleanup_resources(); + return history_ret; + } + continue; + } + auto ret = query::ExecuteQuery(session.get(), query->query, param_store.AsMap().get()); if (ret.records.size() > 0) { Output(ret.header, ret.records, output_opts, csv_opts); } diff --git a/src/parameters.cpp b/src/parameters.cpp new file mode 100644 index 00000000..5c57248e --- /dev/null +++ b/src/parameters.cpp @@ -0,0 +1,99 @@ +// Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com] +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "parameters.hpp" + +namespace query::params { + +namespace { + +constexpr const char *kWhitespace = " \t\r\n"; + +std::string Trim(const std::string &s) { + const auto begin = s.find_first_not_of(kWhitespace); + if (begin == std::string::npos) return ""; + const auto end = s.find_last_not_of(kWhitespace); + return s.substr(begin, end - begin + 1); +} + +} // namespace + +ParamParse ParseParamCommand(const std::string &line) { + const std::string trimmed = Trim(line); + const auto head_end = trimmed.find_first_of(kWhitespace); + const std::string head = trimmed.substr(0, head_end); + const std::string rest = head_end == std::string::npos ? "" : Trim(trimmed.substr(head_end)); + + if (head == ":params") { + if (rest.empty()) { + ParamCommand command; + command.kind = ParamCommand::Kind::kList; + return {.is_param_command = true, .command = command, .error = ""}; + } + if (rest == "clear") { + ParamCommand command; + command.kind = ParamCommand::Kind::kClear; + return {.is_param_command = true, .command = command, .error = ""}; + } + return {.is_param_command = true, .command = std::nullopt, .error = "expected ':params' or ':params clear'"}; + } + + if (head != ":param") return {}; // not a parameter command + + const auto name_end = rest.find_first_of(kWhitespace); + ParamCommand command; + command.kind = ParamCommand::Kind::kSet; + command.name = rest.substr(0, name_end); + command.expression = name_end == std::string::npos ? "" : Trim(rest.substr(name_end)); + + if (command.name.empty() || command.expression.empty()) { + return {.is_param_command = true, .command = std::nullopt, .error = "expected ':param '"}; + } + + return {.is_param_command = true, .command = command, .error = ""}; +} + +bool ParamStore::Empty() const { return params_.empty(); } + +std::size_t ParamStore::Size() const { return params_.size(); } + +void ParamStore::Set(const std::string &name, const mg_value *value) { + params_.insert_or_assign(name, mg_memory::MakeCustomUnique(mg_value_copy(value))); +} + +const mg_value *ParamStore::Get(const std::string &name) const { + const auto it = params_.find(name); + return it == params_.end() ? nullptr : it->second.get(); +} + +std::vector ParamStore::Names() const { + std::vector names; + names.reserve(params_.size()); + for (const auto &[name, value] : params_) names.push_back(name); + return names; // std::map keeps keys sorted +} + +void ParamStore::Clear() { params_.clear(); } + +mg_memory::MgMapPtr ParamStore::AsMap() const { + auto map = mg_memory::MakeCustomUnique(mg_map_make_empty(static_cast(params_.size()))); + for (const auto &[name, value] : params_) { + // mg_map_insert copies the key and takes ownership of the value copy. + mg_map_insert(map.get(), name.c_str(), mg_value_copy(value.get())); + } + return map; +} + +} // namespace query::params diff --git a/src/parameters.hpp b/src/parameters.hpp new file mode 100644 index 00000000..07fc8a46 --- /dev/null +++ b/src/parameters.hpp @@ -0,0 +1,79 @@ +// Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com] +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include +#include +#include +#include +#include + +#include "utils/mg_memory.hpp" + +namespace query::params { + +/// A parsed `:param` / `:params` interactive command. +struct ParamCommand { + enum class Kind { + kSet, ///< `:param ` + kList, ///< `:params` + kClear, ///< `:params clear` + }; + + Kind kind; + std::string name; ///< populated for kSet + std::string expression; ///< populated for kSet +}; + +/// Result of attempting to parse a line as a parameter command. +struct ParamParse { + /// True if the line is a `:param`/`:params` command (well-formed or not). + bool is_param_command{false}; + /// Set iff the command parsed successfully. + std::optional command{std::nullopt}; + /// Human-readable reason, set iff `is_param_command && !command`. + std::string error{}; +}; + +/// Parses a single line as a parameter command. +/// +/// Returns `is_param_command == false` for anything that is not a +/// `:param`/`:params` command, so other command handlers can take over. +ParamParse ParseParamCommand(const std::string &line); + +/// Holds the query parameters set via `:param`, owning a copy of each value, +/// and exposes them as an `mg_map` for `mg_session_run`. +class ParamStore { + public: + bool Empty() const; + std::size_t Size() const; + + /// Stores a copy of `value` under `name`, overwriting any existing entry. + void Set(const std::string &name, const mg_value *value); + /// Returns the value stored under `name`, or nullptr if none. + const mg_value *Get(const std::string &name) const; + /// Returns all parameter names in sorted order. + std::vector Names() const; + /// Removes all parameters. + void Clear(); + /// Builds an `mg_map` copy of all parameters, suitable for `mg_session_run`. + mg_memory::MgMapPtr AsMap() const; + + private: + std::map params_; +}; + +} // namespace query::params diff --git a/src/utils/CMakeLists.txt b/src/utils/CMakeLists.txt index 3a0fd788..59648e77 100644 --- a/src/utils/CMakeLists.txt +++ b/src/utils/CMakeLists.txt @@ -9,10 +9,24 @@ else() endif() get_filename_component(REPLXX_LIB_PATH "${REPLXX_PREFIX}/${MG_INSTALL_LIB_DIR}/libreplxx${REPLXX_LIB_POSTFIX}.a" ABSOLUTE) + +# Local fixes applied on top of the pinned replxx tag. +find_package(Git REQUIRED) ExternalProject_Add(replxx-proj PREFIX ${REPLXX_PREFIX} GIT_REPOSITORY https://github.com/AmokHuginnsson/replxx.git GIT_TAG release-0.0.4 + # Force an LF checkout so the LF patch below applies. Windows/MSYS2 git + # defaults to core.autocrlf=true, which rewrites the sources with CRLF + # endings and makes the patch fail with "patch does not apply" (the + # trailing-CR context lines no longer match). --config persists into the + # cloned repo, so the update-step checkout stays LF too. + GIT_CONFIG core.autocrlf=false + # --3way makes this idempotent: the patch step chains off the git + # update step and can re-run, and --3way no-ops cleanly when the fix is + # already present (plain `git apply` would error "patch does not apply"). + PATCH_COMMAND ${GIT_EXECUTABLE} apply --3way + "${CMAKE_CURRENT_SOURCE_DIR}/replxx-patches/0001-history_previous-bounds-check.patch" CMAKE_ARGS "-DCMAKE_INSTALL_PREFIX=" "-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}" "-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}" diff --git a/src/utils/constants.hpp b/src/utils/constants.hpp index eab7b948..0c1d4b7a 100644 --- a/src/utils/constants.hpp +++ b/src/utils/constants.hpp @@ -19,7 +19,12 @@ constexpr const std::string_view kInteractiveUsage = "are printed out.\n\n" "The following interactive commands are supported:\n\n" "\t:help\t Print out usage for interactive mode\n" - "\t:quit\t Exit the shell\n"; + "\t:quit\t Exit the shell\n" + "\t:param \t Set a query parameter to the value of a " + "Cypher expression (e.g. ':param age 21 * 2'); use it in queries as " + "$\n" + "\t:params\t List all currently set query parameters\n" + "\t:params clear\t Remove all query parameters\n"; constexpr const std::string_view kDocs = "If you are new to Memgraph or the Cypher query language, check out these " @@ -33,6 +38,8 @@ constexpr const std::string_view kDocs = constexpr const std::string_view kCommandQuit = ":quit"; constexpr const std::string_view kCommandHelp = ":help"; constexpr const std::string_view kCommandDocs = ":docs"; +constexpr const std::string_view kCommandParam = ":param"; +constexpr const std::string_view kCommandParams = ":params"; // Supported formats. constexpr const std::string_view kCsvFormat = "csv"; diff --git a/src/utils/mg_memory.hpp b/src/utils/mg_memory.hpp new file mode 100644 index 00000000..3479d743 --- /dev/null +++ b/src/utils/mg_memory.hpp @@ -0,0 +1,68 @@ +// Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com] +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +#include + +#include "mgclient.h" + +namespace mg_memory { +/// Unique pointers with custom deleters for automatic memory management of +/// mg_values. + +template +inline void CustomDelete(T *); + +template <> +inline void CustomDelete(mg_session *session) { + mg_session_destroy(session); +} + +template <> +inline void CustomDelete(mg_session_params *session_params) { + mg_session_params_destroy(session_params); +} + +template <> +inline void CustomDelete(mg_value *value) { + mg_value_destroy(value); +} + +template <> +inline void CustomDelete(mg_list *list) { + mg_list_destroy(list); +} + +template <> +inline void CustomDelete(mg_map *map) { + mg_map_destroy(map); +} + +template +using CustomUniquePtr = std::unique_ptr; + +template +CustomUniquePtr MakeCustomUnique(T *ptr) { + return CustomUniquePtr(ptr, CustomDelete); +} + +using MgSessionPtr = CustomUniquePtr; +using MgSessionParamsPtr = CustomUniquePtr; +using MgValuePtr = CustomUniquePtr; +using MgListPtr = CustomUniquePtr; +using MgMapPtr = CustomUniquePtr; + +} // namespace mg_memory diff --git a/src/utils/replxx-patches/0001-history_previous-bounds-check.patch b/src/utils/replxx-patches/0001-history_previous-bounds-check.patch new file mode 100644 index 00000000..d6b115ab --- /dev/null +++ b/src/utils/replxx-patches/0001-history_previous-bounds-check.patch @@ -0,0 +1,24 @@ +Add the missing bounds guard to ReplxxImpl::history_previous. + +When the edit buffer contains newlines and the cursor sits on a newline at +position 0, history_previous indexed prev_newline_position(_pos - 1), that is +prev_newline_position(-1), tripping the `pos_ >= 0` assertion and aborting the +process. history_next already guards the symmetric case with +`_pos > 0 ? ... : -1`; apply the same guard so navigating up through a +multi-line buffer falls through to history_move instead of aborting. + +Not fixed in the pinned upstream tag. + +diff --git a/src/replxx_impl.cxx b/src/replxx_impl.cxx +index 22ee748..24a63b8 100644 +--- a/src/replxx_impl.cxx ++++ b/src/replxx_impl.cxx +@@ -1831,7 +1831,7 @@ Replxx::ACTION_RESULT Replxx::ReplxxImpl::history_previous( char32_t ) { + } + int prevNewlinePosition( prev_newline_position( _pos ) ); + if ( prevNewlinePosition == _pos ) { +- prevNewlinePosition = prev_newline_position( _pos - 1 ); ++ prevNewlinePosition = _pos > 0 ? prev_newline_position( _pos - 1 ) : -1; + } + if ( prevNewlinePosition < 0 ) { + break; diff --git a/src/utils/utils.cpp b/src/utils/utils.cpp index b35f9c14..71f398a8 100644 --- a/src/utils/utils.cpp +++ b/src/utils/utils.cpp @@ -901,6 +901,12 @@ std::optional GetQuery(Replxx *replxx_instance, bool collect_info) { } else if (trimmed_line == constants::kCommandDocs) { console::PrintDocs(); return Query{}; + } else if (trimmed_line == constants::kCommandParams || trimmed_line == constants::kCommandParam || + trimmed_line.rfind(std::string(constants::kCommandParam) + " ", 0) == 0 || + trimmed_line.rfind(std::string(constants::kCommandParams) + " ", 0) == 0) { + // Parameter commands need the session/param store, so hand the raw + // line up to the interactive loop instead of handling it here. + return Query{.query = trimmed_line, .is_param_command = true}; } else { console::EchoFailure("Unsupported command", trimmed_line); console::PrintHelp(); @@ -941,8 +947,8 @@ void PrintQueryInfo(const Query &query) { std::cout << "line: " << query.line_number << " index: " << query.index << " query: " << query.query << std::endl; } -QueryResult ExecuteQuery(mg_session *session, const std::string &query) { - int status = mg_session_run(session, query.c_str(), nullptr, nullptr, nullptr, nullptr); +QueryResult ExecuteQuery(mg_session *session, const std::string &query, const mg_map *params) { + int status = mg_session_run(session, query.c_str(), params, nullptr, nullptr, nullptr); auto start = std::chrono::system_clock::now(); if (status != 0) { if (mg_session_status(session) == MG_SESSION_BAD) { @@ -1318,6 +1324,18 @@ Replxx *InitAndSetupReplxx() { replxx_set_unique_history(replxx_instance, 1); replxx_set_completion_callback(replxx_instance, CompletionHook, nullptr); + // Treat a bare line feed (Ctrl-J / 0x0A) like Enter: commit the current line + // instead of feeding replxx's NEW_LINE action. mgconsole assembles multi-line + // queries itself via the continuation prompt, so a multi-line paste should + // submit one physical line at a time rather than accumulate in replxx's edit + // buffer, whose in-buffer multiline redraw clears to end of screen and erases + // already-printed output. + // + // This only keeps typed and pasted newlines out of the buffer. A recalled + // multi-line history entry still contains them, so replxx's multiline cursor + // navigation has to stay correct independently of this bind. + replxx_bind_key_internal(replxx_instance, REPLXX_KEY_CONTROL('J'), "commit_line"); + // ToDo(the-joksim): // - syntax highlighting disabled for now - figure out a smarter way of // picking the right colors depending on the user's terminal settings diff --git a/src/utils/utils.hpp b/src/utils/utils.hpp index a824f5fe..62faaf17 100644 --- a/src/utils/utils.hpp +++ b/src/utils/utils.hpp @@ -31,52 +31,11 @@ #include "mgclient.h" #include "replxx.h" +#include "mg_memory.hpp" #include "query_type.hpp" namespace fs = std::filesystem; -namespace mg_memory { -/// Unique pointers with custom deleters for automatic memory management of -/// mg_values. - -template -inline void CustomDelete(T *); - -template <> -inline void CustomDelete(mg_session *session) { - mg_session_destroy(session); -} - -template <> -inline void CustomDelete(mg_session_params *session_params) { - mg_session_params_destroy(session_params); -} - -template <> -inline void CustomDelete(mg_list *list) { - mg_list_destroy(list); -} - -template <> -inline void CustomDelete(mg_map *map) { - mg_map_destroy(map); -} - -template -using CustomUniquePtr = std::unique_ptr; - -template -CustomUniquePtr MakeCustomUnique(T *ptr) { - return CustomUniquePtr(ptr, CustomDelete); -} - -using MgSessionPtr = CustomUniquePtr; -using MgSessionParamsPtr = CustomUniquePtr; -using MgListPtr = CustomUniquePtr; -using MgMapPtr = CustomUniquePtr; - -} // namespace mg_memory - namespace utils { class ClientFatalException : public std::exception { @@ -283,6 +242,8 @@ struct Query { int64_t index{0}; std::string query{""}; std::optional info{std::nullopt}; + /// True if `query` is a `:param`/`:params` command rather than Cypher. + bool is_param_command{false}; }; void PrintQueryInfo(const Query &); @@ -321,7 +282,7 @@ struct BatchResult { // The extra part is preserved for the next GetQuery call std::optional GetQuery(Replxx *replxx_instance, bool collect_info = false); -QueryResult ExecuteQuery(mg_session *session, const std::string &query); +QueryResult ExecuteQuery(mg_session *session, const std::string &query, const mg_map *params = nullptr); BatchResult ExecuteBatch(mg_session *session, const Batch &batch); } // namespace query diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ef06a167..9629daf9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -15,3 +15,4 @@ # along with this program. If not, see . add_subdirectory(input_output) +add_subdirectory(unit) diff --git a/tests/unit/CMakeLists.txt b/tests/unit/CMakeLists.txt new file mode 100644 index 00000000..b5d6691e --- /dev/null +++ b/tests/unit/CMakeLists.txt @@ -0,0 +1,20 @@ +# mgconsole - console client for Memgraph database +# Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com] +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +add_executable(parameters_test parameters_test.cpp) +target_link_libraries(parameters_test PRIVATE params) +target_include_directories(parameters_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +add_test(NAME parameters-unit-test COMMAND parameters_test) diff --git a/tests/unit/check.hpp b/tests/unit/check.hpp new file mode 100644 index 00000000..287cea43 --- /dev/null +++ b/tests/unit/check.hpp @@ -0,0 +1,72 @@ +// Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com] +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#pragma once + +// Minimal dependency-free check harness for mgconsole unit tests. +// +// Usage: +// #include "check.hpp" +// void my_test() { CHECK(1 + 1 == 2); CHECK_EQ(answer, 42); } +// int main() { RUN(my_test); return check::summary(); } + +#include +#include + +namespace check { + +inline int &failures() { + static int n = 0; + return n; +} + +inline const char *¤t_test() { + static const char *name = ""; + return name; +} + +inline void fail(const char *file, int line, const std::string &expr) { + std::printf(" FAIL [%s] %s:%d: %s\n", current_test(), file, line, expr.c_str()); + ++failures(); +} + +inline int summary() { + if (failures() == 0) { + std::printf("All checks passed.\n"); + return 0; + } + std::printf("%d check(s) failed.\n", failures()); + return 1; +} + +} // namespace check + +#define CHECK(cond) \ + do { \ + if (!(cond)) ::check::fail(__FILE__, __LINE__, #cond); \ + } while (0) + +#define CHECK_EQ(a, b) \ + do { \ + auto &&_a = (a); \ + auto &&_b = (b); \ + if (!(_a == _b)) ::check::fail(__FILE__, __LINE__, #a " == " #b); \ + } while (0) + +#define RUN(test_fn) \ + do { \ + ::check::current_test() = #test_fn; \ + test_fn(); \ + } while (0) diff --git a/tests/unit/parameters_test.cpp b/tests/unit/parameters_test.cpp new file mode 100644 index 00000000..e831b12f --- /dev/null +++ b/tests/unit/parameters_test.cpp @@ -0,0 +1,200 @@ +// Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com] +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#include "parameters.hpp" + +#include "check.hpp" + +using query::params::ParamCommand; +using query::params::ParamStore; +using query::params::ParseParamCommand; + +// `:param ` is parsed into a Set command carrying the +// parameter name and the (verbatim) Cypher expression. +void set_command_carries_name_and_expression() { + auto result = ParseParamCommand(":param x 1 + 2"); + CHECK(result.is_param_command); + CHECK(result.command.has_value()); + if (result.command) { + CHECK(result.command->kind == ParamCommand::Kind::kSet); + CHECK_EQ(result.command->name, std::string{"x"}); + CHECK_EQ(result.command->expression, std::string{"1 + 2"}); + } +} + +// `:params` lists all currently-set parameters. +void params_lists_all() { + auto result = ParseParamCommand(":params"); + CHECK(result.is_param_command); + CHECK(result.command.has_value()); + if (result.command) { + CHECK(result.command->kind == ParamCommand::Kind::kList); + } +} + +// `:params clear` removes all parameters. +void params_clear_clears_all() { + auto result = ParseParamCommand(":params clear"); + CHECK(result.is_param_command); + CHECK(result.command.has_value()); + if (result.command) { + CHECK(result.command->kind == ParamCommand::Kind::kClear); + } +} + +// A `:param` with no name, or a name but no expression, is a +// recognised-but-malformed command. +void set_without_name_or_expression_is_an_error() { + auto no_name = ParseParamCommand(":param"); + CHECK(no_name.is_param_command); + CHECK(!no_name.command.has_value()); + CHECK(!no_name.error.empty()); + + auto no_expression = ParseParamCommand(":param x"); + CHECK(no_expression.is_param_command); + CHECK(!no_expression.command.has_value()); + CHECK(!no_expression.error.empty()); +} + +// An unknown `:params` argument is a recognised-but-malformed command. +void params_with_unknown_argument_is_an_error() { + auto result = ParseParamCommand(":params bogus"); + CHECK(result.is_param_command); + CHECK(!result.command.has_value()); + CHECK(!result.error.empty()); +} + +// Lines that are not parameter commands are left for other handlers, including +// look-alikes such as `:paramfoo` that share the `:param` prefix. +void non_param_lines_are_ignored() { + for (const char *line : {":help", ":quit", "MATCH (n) RETURN n;", ":paramfoo", ""}) { + auto result = ParseParamCommand(line); + CHECK(!result.is_param_command); + CHECK(!result.command.has_value()); + } +} + +// Surrounding whitespace is trimmed from the name and expression, while +// whitespace inside the expression is preserved verbatim. +void whitespace_around_name_and_expression_is_trimmed() { + auto result = ParseParamCommand(" :param x 1 + 2 "); + CHECK(result.command.has_value()); + if (result.command) { + CHECK_EQ(result.command->name, std::string{"x"}); + CHECK_EQ(result.command->expression, std::string{"1 + 2"}); + } +} + +// A freshly constructed store holds no parameters. +void new_store_is_empty() { + ParamStore store; + CHECK(store.Empty()); + CHECK_EQ(store.Size(), std::size_t{0}); +} + +namespace { +mg_memory::MgValuePtr IntValue(int64_t n) { + return mg_memory::MakeCustomUnique(mg_value_make_integer(n)); +} +} // namespace + +// Setting a parameter stores a value retrievable by name. +void set_then_get_returns_equal_value() { + ParamStore store; + auto value = IntValue(42); + store.Set("x", value.get()); + CHECK(!store.Empty()); + CHECK_EQ(store.Size(), std::size_t{1}); + const mg_value *got = store.Get("x"); + CHECK(got != nullptr); + if (got) { + CHECK(mg_value_get_type(got) == MG_VALUE_TYPE_INTEGER); + CHECK_EQ(mg_value_integer(got), int64_t{42}); + } +} + +// Setting an existing name replaces its value rather than adding a duplicate. +void set_overwrites_existing_value() { + ParamStore store; + store.Set("x", IntValue(1).get()); + store.Set("x", IntValue(2).get()); + CHECK_EQ(store.Size(), std::size_t{1}); + CHECK(store.Get("x") != nullptr); + if (store.Get("x")) { + CHECK_EQ(mg_value_integer(store.Get("x")), int64_t{2}); + } +} + +// Parameter names are listed in sorted order (for stable `:params` output). +void names_are_returned_sorted() { + ParamStore store; + store.Set("b", IntValue(1).get()); + store.Set("a", IntValue(2).get()); + store.Set("c", IntValue(3).get()); + CHECK(store.Names() == (std::vector{"a", "b", "c"})); +} + +// Clear removes every parameter. +void clear_empties_store() { + ParamStore store; + store.Set("x", IntValue(1).get()); + store.Set("y", IntValue(2).get()); + store.Clear(); + CHECK(store.Empty()); + CHECK_EQ(store.Size(), std::size_t{0}); +} + +// AsMap builds an mg_map carrying every stored parameter, keyed by name. +void as_map_contains_all_parameters() { + ParamStore store; + store.Set("x", IntValue(42).get()); + store.Set("y", IntValue(7).get()); + + auto map = store.AsMap(); + CHECK(map != nullptr); + if (map) { + CHECK_EQ(mg_map_size(map.get()), uint32_t{2}); + const mg_value *x = mg_map_at(map.get(), "x"); + const mg_value *y = mg_map_at(map.get(), "y"); + CHECK(x != nullptr && mg_value_integer(x) == 42); + CHECK(y != nullptr && mg_value_integer(y) == 7); + } +} + +// An empty store still produces a usable (empty) mg_map. +void as_map_of_empty_store_is_empty() { + ParamStore store; + auto map = store.AsMap(); + CHECK(map != nullptr); + if (map) CHECK_EQ(mg_map_size(map.get()), uint32_t{0}); +} + +int main() { + RUN(set_command_carries_name_and_expression); + RUN(params_lists_all); + RUN(params_clear_clears_all); + RUN(set_without_name_or_expression_is_an_error); + RUN(params_with_unknown_argument_is_an_error); + RUN(non_param_lines_are_ignored); + RUN(whitespace_around_name_and_expression_is_trimmed); + RUN(new_store_is_empty); + RUN(set_then_get_returns_equal_value); + RUN(set_overwrites_existing_value); + RUN(names_are_returned_sorted); + RUN(clear_empties_store); + RUN(as_map_contains_all_parameters); + RUN(as_map_of_empty_store_is_empty); + return check::summary(); +}