Skip to content

Commit 5fa6c5e

Browse files
authored
feat: Query parameters in interactive mode (:param) (#98)
* feat: Add :param command for query parameters in interactive mode Add cypher-shell-style query parameters to the interactive REPL: :param <name> <expression> set a parameter to a server-evaluated value :params list all set parameters :params clear remove all parameters Expressions are evaluated server-side via RETURN, giving full Cypher type support, and may reference previously set parameters. Set parameters are passed to every subsequent query via mg_session_run. The parser (ParseParamCommand) and parameter store (ParamStore) live in a new mgclient-only `params` library and are covered by unit tests. mg_memory is extracted into its own header so the library and its test don't depend on replxx. The REPL wiring (dispatch, server-eval, ExecuteQuery params arg) is thin glue verified manually. * fix: Stop replxx aborting when navigating multi-line buffers mgconsole assembles multi-line queries itself via the continuation prompt and does not use replxx's in-buffer multiline editing, which is buggy in the pinned release-0.0.4. history_previous() calls prev_newline_position(_pos - 1) without the `_pos > 0` guard its sibling history_next() has; with a newline at buffer position 0 (navigating up through a recalled multi-line history entry) it passes -1 and trips an assertion that aborts the process. Separately, in-buffer multiline redraws clear to end of screen and erase already-printed output. Patch replxx (replxx-patches/, applied via the replxx-proj PATCH_COMMAND) to add the missing bounds guard to history_previous. This fixes the abort for any newline-bearing buffer, including multi-line entries decoded from the history file. Rebind Ctrl-J (line feed, 0x0A) to commit_line so a bare newline submits the current line like Enter instead of feeding replxx's NEW_LINE action. A multi-line paste then submits one physical line at a time, which is what GetQuery expects, and keeps pasted or typed newlines out of the buffer so the redraw corruption stays unreachable for the common paste case. It does not cover newlines that arrive from history, which is why the bounds guard is the actual crash fix.
1 parent d622d39 commit 5fa6c5e

15 files changed

Lines changed: 699 additions & 47 deletions

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# git apply needs LF context lines to match an LF-checked-out source tree;
2+
# Windows autocrlf would otherwise rewrite these to CRLF and break the patch.
3+
*.patch text eol=lf

src/CMakeLists.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ if(MGCONSOLE_ON_WINDOWS)
119119
add_compile_options(-Wno-narrowing)
120120
endif()
121121

122+
# Parameter handling (`:param`/`:params`). Kept as its own library with a
123+
# minimal dependency surface (mgclient only) so it can be unit tested.
124+
add_library(params STATIC parameters.cpp)
125+
add_dependencies(params mgclient)
126+
target_compile_definitions(params PUBLIC MGCLIENT_STATIC_DEFINE)
127+
target_include_directories(params PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ${MGCLIENT_INCLUDE_DIRS})
128+
target_link_libraries(params ${MGCLIENT_LIBRARY})
129+
122130
add_executable(mgconsole main.cpp interactive.cpp serial_import.cpp batch_import.cpp parsing.cpp)
123131
target_compile_definitions(mgconsole PRIVATE MGCLIENT_STATIC_DEFINE)
124132
target_include_directories(mgconsole
@@ -131,6 +139,7 @@ target_link_libraries(mgconsole
131139
PRIVATE
132140
${GFLAGS_LIBRARY}
133141
utils
142+
params
134143
${MGCLIENT_LIBRARY}
135144
${OPENSSL_LIBRARIES})
136145
if(MGCONSOLE_ON_WINDOWS)

src/interactive.cpp

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,78 @@
1515

1616
#include "interactive.hpp"
1717

18+
#include <sstream>
1819
#include <thread>
1920

2021
#include <gflags/gflags.h>
2122

23+
#include "parameters.hpp"
2224
#include "utils/constants.hpp"
2325

2426
namespace mode::interactive {
2527

2628
using namespace std::string_literals;
2729

30+
namespace {
31+
32+
namespace params = query::params;
33+
34+
// Evaluates a Cypher expression server-side and returns a copy of the resulting
35+
// value. Existing parameters are made available to the expression.
36+
mg_memory::MgValuePtr EvaluateParamExpression(mg_session *session, const std::string &expression,
37+
const params::ParamStore &store) {
38+
auto result = query::ExecuteQuery(session, "RETURN " + expression, store.AsMap().get());
39+
if (result.records.empty() || mg_list_size(result.records.front().get()) == 0) {
40+
throw utils::ClientQueryException("expression did not produce a value");
41+
}
42+
const mg_value *value = mg_list_at(result.records.front().get(), 0);
43+
return mg_memory::MakeCustomUnique<mg_value>(mg_value_copy(value));
44+
}
45+
46+
void ListParams(const params::ParamStore &store) {
47+
if (store.Empty()) {
48+
console::EchoInfo("No parameters set");
49+
return;
50+
}
51+
for (const auto &name : store.Names()) {
52+
std::ostringstream os;
53+
os << name << ": ";
54+
utils::PrintValue(os, store.Get(name));
55+
console::EchoInfo(os.str());
56+
}
57+
}
58+
59+
// Handles a `:param`/`:params` command line. Query-level failures (e.g. a bad
60+
// expression) are reported without aborting the shell; fatal connection
61+
// failures propagate to the reconnect logic in Run.
62+
void HandleParamCommand(mg_session *session, params::ParamStore &store, const std::string &line) {
63+
const auto parsed = params::ParseParamCommand(line);
64+
if (!parsed.command) {
65+
console::EchoFailure("Invalid parameter command", parsed.error);
66+
return;
67+
}
68+
switch (parsed.command->kind) {
69+
case params::ParamCommand::Kind::kSet:
70+
try {
71+
auto value = EvaluateParamExpression(session, parsed.command->expression, store);
72+
store.Set(parsed.command->name, value.get());
73+
console::EchoInfo("Set parameter '" + parsed.command->name + "'");
74+
} catch (const utils::ClientQueryException &e) {
75+
console::EchoFailure("Failed to evaluate parameter expression", e.what());
76+
}
77+
break;
78+
case params::ParamCommand::Kind::kList:
79+
ListParams(store);
80+
break;
81+
case params::ParamCommand::Kind::kClear:
82+
store.Clear();
83+
console::EchoInfo("Cleared all parameters");
84+
break;
85+
}
86+
}
87+
88+
} // namespace
89+
2890
int Run(utils::bolt::Config &bolt_config, const std::string &history, bool no_history,
2991
bool verbose_execution_info, const format::CsvOptions &csv_opts, const format::OutputOptions &output_opts) {
3092
Replxx *replxx_instance = InitAndSetupReplxx();
@@ -97,6 +159,9 @@ int Run(utils::bolt::Config &bolt_config, const std::string &history, bool no_hi
97159
return 1;
98160
}
99161

162+
// Query parameters set via `:param`, passed to every executed query.
163+
params::ParamStore param_store;
164+
100165
console::EchoInfo("mgconsole "s + gflags::VersionString());
101166
console::EchoInfo("Connected to 'memgraph://" + bolt_config.host + ":" + std::to_string(bolt_config.port) + "'");
102167
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
113178
}
114179

115180
try {
116-
auto ret = query::ExecuteQuery(session.get(), query->query);
181+
if (query->is_param_command) {
182+
HandleParamCommand(session.get(), param_store, query->query);
183+
auto history_ret = save_history();
184+
if (history_ret != 0) {
185+
cleanup_resources();
186+
return history_ret;
187+
}
188+
continue;
189+
}
190+
auto ret = query::ExecuteQuery(session.get(), query->query, param_store.AsMap().get());
117191
if (ret.records.size() > 0) {
118192
Output(ret.header, ret.records, output_opts, csv_opts);
119193
}

src/parameters.cpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com]
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
#include "parameters.hpp"
17+
18+
namespace query::params {
19+
20+
namespace {
21+
22+
constexpr const char *kWhitespace = " \t\r\n\f\v";
23+
24+
std::string Trim(const std::string &s) {
25+
const auto begin = s.find_first_not_of(kWhitespace);
26+
if (begin == std::string::npos) return "";
27+
const auto end = s.find_last_not_of(kWhitespace);
28+
return s.substr(begin, end - begin + 1);
29+
}
30+
31+
} // namespace
32+
33+
ParamParse ParseParamCommand(const std::string &line) {
34+
const std::string trimmed = Trim(line);
35+
const auto head_end = trimmed.find_first_of(kWhitespace);
36+
const std::string head = trimmed.substr(0, head_end);
37+
const std::string rest = head_end == std::string::npos ? "" : Trim(trimmed.substr(head_end));
38+
39+
if (head == ":params") {
40+
if (rest.empty()) {
41+
ParamCommand command;
42+
command.kind = ParamCommand::Kind::kList;
43+
return {.is_param_command = true, .command = command, .error = ""};
44+
}
45+
if (rest == "clear") {
46+
ParamCommand command;
47+
command.kind = ParamCommand::Kind::kClear;
48+
return {.is_param_command = true, .command = command, .error = ""};
49+
}
50+
return {.is_param_command = true, .command = std::nullopt, .error = "expected ':params' or ':params clear'"};
51+
}
52+
53+
if (head != ":param") return {}; // not a parameter command
54+
55+
const auto name_end = rest.find_first_of(kWhitespace);
56+
ParamCommand command;
57+
command.kind = ParamCommand::Kind::kSet;
58+
command.name = rest.substr(0, name_end);
59+
command.expression = name_end == std::string::npos ? "" : Trim(rest.substr(name_end));
60+
61+
if (command.name.empty() || command.expression.empty()) {
62+
return {.is_param_command = true, .command = std::nullopt, .error = "expected ':param <name> <expression>'"};
63+
}
64+
65+
return {.is_param_command = true, .command = command, .error = ""};
66+
}
67+
68+
bool ParamStore::Empty() const { return params_.empty(); }
69+
70+
std::size_t ParamStore::Size() const { return params_.size(); }
71+
72+
void ParamStore::Set(const std::string &name, const mg_value *value) {
73+
params_.insert_or_assign(name, mg_memory::MakeCustomUnique<mg_value>(mg_value_copy(value)));
74+
}
75+
76+
const mg_value *ParamStore::Get(const std::string &name) const {
77+
const auto it = params_.find(name);
78+
return it == params_.end() ? nullptr : it->second.get();
79+
}
80+
81+
std::vector<std::string> ParamStore::Names() const {
82+
std::vector<std::string> names;
83+
names.reserve(params_.size());
84+
for (const auto &[name, value] : params_) names.push_back(name);
85+
return names; // std::map keeps keys sorted
86+
}
87+
88+
void ParamStore::Clear() { params_.clear(); }
89+
90+
mg_memory::MgMapPtr ParamStore::AsMap() const {
91+
auto map = mg_memory::MakeCustomUnique<mg_map>(mg_map_make_empty(static_cast<uint32_t>(params_.size())));
92+
for (const auto &[name, value] : params_) {
93+
// mg_map_insert copies the key and takes ownership of the value copy.
94+
mg_map_insert(map.get(), name.c_str(), mg_value_copy(value.get()));
95+
}
96+
return map;
97+
}
98+
99+
} // namespace query::params

src/parameters.hpp

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright (C) 2016-2023 Memgraph Ltd. [https://memgraph.com]
2+
//
3+
// This program is free software: you can redistribute it and/or modify
4+
// it under the terms of the GNU General Public License as published by
5+
// the Free Software Foundation, either version 3 of the License, or
6+
// (at your option) any later version.
7+
//
8+
// This program is distributed in the hope that it will be useful,
9+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
10+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+
// GNU General Public License for more details.
12+
//
13+
// You should have received a copy of the GNU General Public License
14+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
15+
16+
#pragma once
17+
18+
#include <cstddef>
19+
#include <map>
20+
#include <optional>
21+
#include <string>
22+
#include <vector>
23+
24+
#include "utils/mg_memory.hpp"
25+
26+
namespace query::params {
27+
28+
/// A parsed `:param` / `:params` interactive command.
29+
struct ParamCommand {
30+
enum class Kind {
31+
kSet, ///< `:param <name> <expression>`
32+
kList, ///< `:params`
33+
kClear, ///< `:params clear`
34+
};
35+
36+
Kind kind;
37+
std::string name; ///< populated for kSet
38+
std::string expression; ///< populated for kSet
39+
};
40+
41+
/// Result of attempting to parse a line as a parameter command.
42+
struct ParamParse {
43+
/// True if the line is a `:param`/`:params` command (well-formed or not).
44+
bool is_param_command{false};
45+
/// Set iff the command parsed successfully.
46+
std::optional<ParamCommand> command{std::nullopt};
47+
/// Human-readable reason, set iff `is_param_command && !command`.
48+
std::string error{};
49+
};
50+
51+
/// Parses a single line as a parameter command.
52+
///
53+
/// Returns `is_param_command == false` for anything that is not a
54+
/// `:param`/`:params` command, so other command handlers can take over.
55+
ParamParse ParseParamCommand(const std::string &line);
56+
57+
/// Holds the query parameters set via `:param`, owning a copy of each value,
58+
/// and exposes them as an `mg_map` for `mg_session_run`.
59+
class ParamStore {
60+
public:
61+
bool Empty() const;
62+
std::size_t Size() const;
63+
64+
/// Stores a copy of `value` under `name`, overwriting any existing entry.
65+
void Set(const std::string &name, const mg_value *value);
66+
/// Returns the value stored under `name`, or nullptr if none.
67+
const mg_value *Get(const std::string &name) const;
68+
/// Returns all parameter names in sorted order.
69+
std::vector<std::string> Names() const;
70+
/// Removes all parameters.
71+
void Clear();
72+
/// Builds an `mg_map` copy of all parameters, suitable for `mg_session_run`.
73+
mg_memory::MgMapPtr AsMap() const;
74+
75+
private:
76+
std::map<std::string, mg_memory::MgValuePtr> params_;
77+
};
78+
79+
} // namespace query::params

src/utils/CMakeLists.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,24 @@ else()
99
endif()
1010

1111
get_filename_component(REPLXX_LIB_PATH "${REPLXX_PREFIX}/${MG_INSTALL_LIB_DIR}/libreplxx${REPLXX_LIB_POSTFIX}.a" ABSOLUTE)
12+
13+
# Local fixes applied on top of the pinned replxx tag.
14+
find_package(Git REQUIRED)
1215
ExternalProject_Add(replxx-proj
1316
PREFIX ${REPLXX_PREFIX}
1417
GIT_REPOSITORY https://github.com/AmokHuginnsson/replxx.git
1518
GIT_TAG release-0.0.4
19+
# Force an LF checkout so the LF patch below applies. Windows/MSYS2 git
20+
# defaults to core.autocrlf=true, which rewrites the sources with CRLF
21+
# endings and makes the patch fail with "patch does not apply" (the
22+
# trailing-CR context lines no longer match). --config persists into the
23+
# cloned repo, so the update-step checkout stays LF too.
24+
GIT_CONFIG core.autocrlf=false
25+
# --3way makes this idempotent: the patch step chains off the git
26+
# update step and can re-run, and --3way no-ops cleanly when the fix is
27+
# already present (plain `git apply` would error "patch does not apply").
28+
PATCH_COMMAND ${GIT_EXECUTABLE} apply --3way
29+
"${CMAKE_CURRENT_SOURCE_DIR}/replxx-patches/0001-history_previous-bounds-check.patch"
1630
CMAKE_ARGS "-DCMAKE_INSTALL_PREFIX=<INSTALL_DIR>"
1731
"-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
1832
"-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}"

src/utils/constants.hpp

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,12 @@ constexpr const std::string_view kInteractiveUsage =
1919
"are printed out.\n\n"
2020
"The following interactive commands are supported:\n\n"
2121
"\t:help\t Print out usage for interactive mode\n"
22-
"\t:quit\t Exit the shell\n";
22+
"\t:quit\t Exit the shell\n"
23+
"\t:param <name> <expression>\t Set a query parameter to the value of a "
24+
"Cypher expression (e.g. ':param age 21 * 2'); use it in queries as "
25+
"$<name>\n"
26+
"\t:params\t List all currently set query parameters\n"
27+
"\t:params clear\t Remove all query parameters\n";
2328

2429
constexpr const std::string_view kDocs =
2530
"If you are new to Memgraph or the Cypher query language, check out these "
@@ -33,6 +38,8 @@ constexpr const std::string_view kDocs =
3338
constexpr const std::string_view kCommandQuit = ":quit";
3439
constexpr const std::string_view kCommandHelp = ":help";
3540
constexpr const std::string_view kCommandDocs = ":docs";
41+
constexpr const std::string_view kCommandParam = ":param";
42+
constexpr const std::string_view kCommandParams = ":params";
3643

3744
// Supported formats.
3845
constexpr const std::string_view kCsvFormat = "csv";

0 commit comments

Comments
 (0)