Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -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
9 changes: 9 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -131,6 +139,7 @@ target_link_libraries(mgconsole
PRIVATE
${GFLAGS_LIBRARY}
utils
params
${MGCLIENT_LIBRARY}
${OPENSSL_LIBRARIES})
if(MGCONSOLE_ON_WINDOWS)
Expand Down
76 changes: 75 additions & 1 deletion src/interactive.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,78 @@

#include "interactive.hpp"

#include <sstream>
#include <thread>

#include <gflags/gflags.h>

#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>(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();
Expand Down Expand Up @@ -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");
Expand All @@ -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);
}
Expand Down
99 changes: 99 additions & 0 deletions src/parameters.cpp
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

#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 <name> <expression>'"};
}

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>(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<std::string> ParamStore::Names() const {
std::vector<std::string> 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>(mg_map_make_empty(static_cast<uint32_t>(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
79 changes: 79 additions & 0 deletions src/parameters.hpp
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.

#pragma once

#include <cstddef>
#include <map>
#include <optional>
#include <string>
#include <vector>

#include "utils/mg_memory.hpp"

namespace query::params {

/// A parsed `:param` / `:params` interactive command.
struct ParamCommand {
enum class Kind {
kSet, ///< `:param <name> <expression>`
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<ParamCommand> 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<std::string> 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<std::string, mg_memory::MgValuePtr> params_;
};

} // namespace query::params
14 changes: 14 additions & 0 deletions src/utils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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=<INSTALL_DIR>"
"-DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE}"
"-DCMAKE_CXX_COMPILER=${CMAKE_CXX_COMPILER}"
Expand Down
9 changes: 8 additions & 1 deletion src/utils/constants.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name> <expression>\t Set a query parameter to the value of a "
"Cypher expression (e.g. ':param age 21 * 2'); use it in queries as "
"$<name>\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 "
Expand All @@ -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";
Expand Down
Loading
Loading