Add GraphQL transpilation compatibility (#1018)
* Add callable mappings feature * Implement mgps.validate (void procedure) * Make '_' a valid variable name
This commit is contained in:
parent
57fe3463f2
commit
210bea83d4
4
init
4
init
@ -93,6 +93,10 @@ setup_virtualenv tests/stress
|
||||
setup_virtualenv tests/integration/ldap
|
||||
|
||||
# Setup tests dependencies.
|
||||
# NOTE: This is commented out because of the build order (at the time of
|
||||
# execution mgclient is not built yet) which makes this setup to fail. mgclient
|
||||
# is built during the make phase. The tests/setup.sh is called under GHA CI
|
||||
# jobs.
|
||||
# cd tests
|
||||
# ./setup.sh
|
||||
# cd ..
|
||||
|
@ -53,6 +53,7 @@
|
||||
#include "query/frontend/ast/ast.hpp"
|
||||
#include "query/interpreter.hpp"
|
||||
#include "query/plan/operator.hpp"
|
||||
#include "query/procedure/callable_alias_mapper.hpp"
|
||||
#include "query/procedure/module.hpp"
|
||||
#include "query/procedure/py_module.hpp"
|
||||
#include "requests/requests.hpp"
|
||||
@ -355,6 +356,12 @@ DEFINE_VALIDATED_string(query_modules_directory, "",
|
||||
return true;
|
||||
});
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(query_callable_mappings_path, "",
|
||||
"The path to mappings that describes aliases to callables in cypher queries in the form of key-value "
|
||||
"pairs in a json file. With this option query module procedures that do not exist in memgraph can be "
|
||||
"mapped to ones that exist.");
|
||||
|
||||
// Logging flags
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_HIDDEN_bool(also_log_to_stderr, false, "Log messages go to stderr in addition to logfiles");
|
||||
@ -569,6 +576,7 @@ class BoltSession final : public memgraph::communication::bolt::Session<memgraph
|
||||
if (user_) {
|
||||
username = &user_->username();
|
||||
}
|
||||
|
||||
#ifdef MG_ENTERPRISE
|
||||
if (memgraph::license::global_license_checker.IsEnterpriseValidFast()) {
|
||||
audit_log_->Record(endpoint_.address().to_string(), user_ ? *username : "", query,
|
||||
@ -954,6 +962,7 @@ int main(int argc, char **argv) {
|
||||
|
||||
memgraph::query::procedure::gModuleRegistry.SetModulesDirectory(query_modules_directories, FLAGS_data_directory);
|
||||
memgraph::query::procedure::gModuleRegistry.UnloadAndLoadModulesFromDirectories();
|
||||
memgraph::query::procedure::gCallableAliasMapper.LoadMapping(FLAGS_query_callable_mappings_path);
|
||||
|
||||
memgraph::glue::AuthQueryHandler auth_handler(&auth, FLAGS_auth_user_or_role_name_regex);
|
||||
memgraph::glue::AuthChecker auth_checker{&auth};
|
||||
|
@ -27,6 +27,7 @@ set(mg_query_sources
|
||||
procedure/mg_procedure_helpers.cpp
|
||||
procedure/module.cpp
|
||||
procedure/py_module.cpp
|
||||
procedure/callable_alias_mapper.cpp
|
||||
serialization/property_value.cpp
|
||||
stream/streams.cpp
|
||||
stream/sources.cpp
|
||||
|
@ -2253,6 +2253,7 @@ class CallProcedure : public memgraph::query::Clause {
|
||||
memgraph::query::Expression *memory_limit_{nullptr};
|
||||
size_t memory_scale_{1024U};
|
||||
bool is_write_;
|
||||
bool void_procedure_{false};
|
||||
|
||||
CallProcedure *Clone(AstStorage *storage) const override {
|
||||
CallProcedure *object = storage->Create<CallProcedure>();
|
||||
@ -2269,6 +2270,7 @@ class CallProcedure : public memgraph::query::Clause {
|
||||
object->memory_limit_ = memory_limit_ ? memory_limit_->Clone(storage) : nullptr;
|
||||
object->memory_scale_ = memory_scale_;
|
||||
object->is_write_ = is_write_;
|
||||
object->void_procedure_ = void_procedure_;
|
||||
return object;
|
||||
}
|
||||
|
||||
|
@ -35,6 +35,7 @@
|
||||
#include "query/frontend/ast/ast_visitor.hpp"
|
||||
#include "query/frontend/parsing.hpp"
|
||||
#include "query/interpret/awesome_memgraph_functions.hpp"
|
||||
#include "query/procedure/callable_alias_mapper.hpp"
|
||||
#include "query/procedure/module.hpp"
|
||||
#include "query/stream/common.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
@ -1188,13 +1189,26 @@ antlrcpp::Any CypherMainVisitor::visitCallProcedure(MemgraphCypher::CallProcedur
|
||||
const auto &maybe_found =
|
||||
procedure::FindProcedure(procedure::gModuleRegistry, call_proc->procedure_name_, utils::NewDeleteResource());
|
||||
if (!maybe_found) {
|
||||
throw SemanticException("There is no procedure named '{}'.", call_proc->procedure_name_);
|
||||
// TODO remove this once void procedures are supported,
|
||||
// this will not be needed anymore.
|
||||
const auto mg_specific_name = procedure::gCallableAliasMapper.FindAlias(call_proc->procedure_name_);
|
||||
const bool void_procedure_required = (mg_specific_name && *mg_specific_name == "mgps.validate");
|
||||
if (void_procedure_required) {
|
||||
// This is a special case. Since void procedures currently are not supported,
|
||||
// we have to make sure that the non-memgraph native, void procedures that are
|
||||
// possibly used against a memgraph instance are handled correctly. As of now
|
||||
// this is the only known such case. This should be more generic, but the most
|
||||
// generic solution would be to implement void procedures.
|
||||
call_proc->void_procedure_ = true;
|
||||
} else {
|
||||
throw SemanticException("There is no procedure named '{}'.", call_proc->procedure_name_);
|
||||
}
|
||||
}
|
||||
call_proc->is_write_ = maybe_found->second->info.is_write;
|
||||
|
||||
auto *yield_ctx = ctx->yieldProcedureResults();
|
||||
if (!yield_ctx) {
|
||||
if (!maybe_found->second->results.empty()) {
|
||||
if (!maybe_found->second->results.empty() && !call_proc->void_procedure_) {
|
||||
throw SemanticException(
|
||||
"CALL without YIELD may only be used on procedures which do not "
|
||||
"return any result fields.");
|
||||
|
@ -407,4 +407,5 @@ cypherKeyword : ALL
|
||||
symbolicName : UnescapedSymbolicName
|
||||
| EscapedSymbolicName
|
||||
| cypherKeyword
|
||||
| UNDERSCORE
|
||||
;
|
||||
|
@ -179,6 +179,7 @@ EscapedSymbolicName : ( '`' ~[`]* '`' )+ ;
|
||||
*/
|
||||
IdentifierStart : ID_Start | Pc ;
|
||||
IdentifierPart : ID_Continue | Sc ;
|
||||
UNDERSCORE : '_' ;
|
||||
|
||||
/* Hack for case-insensitive reserved words */
|
||||
fragment A : 'A' | 'a' ;
|
||||
|
@ -23,8 +23,6 @@ lexer grammar MemgraphCypherLexer ;
|
||||
|
||||
import CypherLexer ;
|
||||
|
||||
UNDERSCORE : '_' ;
|
||||
|
||||
AFTER : A F T E R ;
|
||||
ALTER : A L T E R ;
|
||||
ANALYZE : A N A L Y Z E ;
|
||||
|
@ -28,6 +28,7 @@
|
||||
#include "query/typed_value.hpp"
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/temporal.hpp"
|
||||
#include "utils/uuid.hpp"
|
||||
|
||||
namespace memgraph::query {
|
||||
namespace {
|
||||
@ -419,6 +420,10 @@ TypedValue Properties(const TypedValue *args, int64_t nargs, const FunctionConte
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue RandomUuid(const TypedValue * /*args*/, int64_t /*nargs*/, const FunctionContext &ctx) {
|
||||
return TypedValue(utils::GenerateUUID(), ctx.memory);
|
||||
}
|
||||
|
||||
TypedValue Size(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
||||
FType<Or<Null, List, String, Map, Path>>("size", args, nargs);
|
||||
const auto &value = args[0];
|
||||
@ -1258,6 +1263,7 @@ std::function<TypedValue(const TypedValue *, int64_t, const FunctionContext &ctx
|
||||
if (function_name == kId) return Id;
|
||||
if (function_name == "LAST") return Last;
|
||||
if (function_name == "PROPERTIES") return Properties;
|
||||
if (function_name == "RANDOMUUID") return RandomUuid;
|
||||
if (function_name == "SIZE") return Size;
|
||||
if (function_name == "STARTNODE") return StartNode;
|
||||
if (function_name == "TIMESTAMP") return Timestamp;
|
||||
|
@ -12,6 +12,7 @@
|
||||
#include "query/plan/operator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
@ -51,6 +52,7 @@
|
||||
#include "utils/event_counter.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
#include "utils/fnv.hpp"
|
||||
#include "utils/java_string_formatter.hpp"
|
||||
#include "utils/likely.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/memory.hpp"
|
||||
@ -4453,7 +4455,7 @@ UniqueCursorPtr OutputTableStream::MakeCursor(utils::MemoryResource *mem) const
|
||||
|
||||
CallProcedure::CallProcedure(std::shared_ptr<LogicalOperator> input, std::string name, std::vector<Expression *> args,
|
||||
std::vector<std::string> fields, std::vector<Symbol> symbols, Expression *memory_limit,
|
||||
size_t memory_scale, bool is_write)
|
||||
size_t memory_scale, bool is_write, bool void_procedure)
|
||||
: input_(input ? input : std::make_shared<Once>()),
|
||||
procedure_name_(name),
|
||||
arguments_(args),
|
||||
@ -4461,7 +4463,8 @@ CallProcedure::CallProcedure(std::shared_ptr<LogicalOperator> input, std::string
|
||||
result_symbols_(symbols),
|
||||
memory_limit_(memory_limit),
|
||||
memory_scale_(memory_scale),
|
||||
is_write_(is_write) {}
|
||||
is_write_(is_write),
|
||||
void_procedure_(void_procedure) {}
|
||||
|
||||
ACCEPT_WITH_INPUT(CallProcedure);
|
||||
|
||||
@ -4697,10 +4700,68 @@ class CallProcedureCursor : public Cursor {
|
||||
}
|
||||
};
|
||||
|
||||
class CallValidateProcedureCursor : public Cursor {
|
||||
const CallProcedure *self_;
|
||||
UniqueCursorPtr input_cursor_;
|
||||
|
||||
public:
|
||||
CallValidateProcedureCursor(const CallProcedure *self, utils::MemoryResource *mem)
|
||||
: self_(self), input_cursor_(self_->input_->MakeCursor(mem)) {}
|
||||
|
||||
bool Pull(Frame &frame, ExecutionContext &context) override {
|
||||
SCOPED_PROFILE_OP("CallValidateProcedureCursor");
|
||||
|
||||
AbortCheck(context);
|
||||
if (!input_cursor_->Pull(frame, context)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
|
||||
storage::View::NEW);
|
||||
|
||||
const auto args = self_->arguments_;
|
||||
MG_ASSERT(args.size() == 3U);
|
||||
|
||||
const auto predicate = args[0]->Accept(evaluator);
|
||||
const bool predicate_val = predicate.ValueBool();
|
||||
|
||||
if (predicate_val) [[unlikely]] {
|
||||
const auto &message = args[1]->Accept(evaluator);
|
||||
const auto &message_args = args[2]->Accept(evaluator);
|
||||
|
||||
using TString = std::remove_cvref_t<decltype(message.ValueString())>;
|
||||
using TElement = std::remove_cvref_t<decltype(message_args.ValueList()[0])>;
|
||||
|
||||
utils::JStringFormatter<TString, TElement> formatter;
|
||||
|
||||
try {
|
||||
const auto &msg = formatter.FormatString(message.ValueString(), message_args.ValueList());
|
||||
throw QueryRuntimeException(msg);
|
||||
} catch (const utils::JStringFormatException &e) {
|
||||
throw QueryRuntimeException(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void Reset() override { input_cursor_->Reset(); }
|
||||
|
||||
void Shutdown() override {}
|
||||
};
|
||||
|
||||
UniqueCursorPtr CallProcedure::MakeCursor(utils::MemoryResource *mem) const {
|
||||
memgraph::metrics::IncrementCounter(memgraph::metrics::CallProcedureOperator);
|
||||
CallProcedure::IncrementCounter(procedure_name_);
|
||||
|
||||
if (void_procedure_) {
|
||||
// Currently we do not support Call procedures that do not return
|
||||
// anything. This cursor is way too specific, but it provides a workaround
|
||||
// to ensure GraphQL compatibility until we start supporting truly void
|
||||
// procedures.
|
||||
return MakeUniqueCursorPtr<CallValidateProcedureCursor>(mem, this, mem);
|
||||
}
|
||||
|
||||
return MakeUniqueCursorPtr<CallProcedureCursor>(mem, this, mem);
|
||||
}
|
||||
|
||||
|
@ -2178,7 +2178,7 @@ class CallProcedure : public memgraph::query::plan::LogicalOperator {
|
||||
CallProcedure() = default;
|
||||
CallProcedure(std::shared_ptr<LogicalOperator> input, std::string name, std::vector<Expression *> arguments,
|
||||
std::vector<std::string> fields, std::vector<Symbol> symbols, Expression *memory_limit,
|
||||
size_t memory_scale, bool is_write);
|
||||
size_t memory_scale, bool is_write, bool void_procedure = false);
|
||||
|
||||
bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override;
|
||||
UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override;
|
||||
@ -2200,6 +2200,7 @@ class CallProcedure : public memgraph::query::plan::LogicalOperator {
|
||||
Expression *memory_limit_{nullptr};
|
||||
size_t memory_scale_{1024U};
|
||||
bool is_write_;
|
||||
bool void_procedure_;
|
||||
mutable utils::MonotonicBufferResource monotonic_memory{1024UL * 1024UL};
|
||||
utils::MemoryResource *memory_resource = &monotonic_memory;
|
||||
|
||||
@ -2216,6 +2217,7 @@ class CallProcedure : public memgraph::query::plan::LogicalOperator {
|
||||
object->memory_limit_ = memory_limit_ ? memory_limit_->Clone(storage) : nullptr;
|
||||
object->memory_scale_ = memory_scale_;
|
||||
object->is_write_ = is_write_;
|
||||
object->void_procedure_ = void_procedure_;
|
||||
return object;
|
||||
}
|
||||
|
||||
|
@ -221,7 +221,8 @@ class RuleBasedPlanner {
|
||||
// storage::View::NEW.
|
||||
input_op = std::make_unique<plan::CallProcedure>(
|
||||
std::move(input_op), call_proc->procedure_name_, call_proc->arguments_, call_proc->result_fields_,
|
||||
result_symbols, call_proc->memory_limit_, call_proc->memory_scale_, call_proc->is_write_);
|
||||
result_symbols, call_proc->memory_limit_, call_proc->memory_scale_, call_proc->is_write_,
|
||||
call_proc->void_procedure_);
|
||||
} else if (auto *load_csv = utils::Downcast<query::LoadCsv>(clause)) {
|
||||
const auto &row_sym = context.symbol_table->at(*load_csv->row_var_);
|
||||
context.bound_symbols.insert(row_sym);
|
||||
|
62
src/query/procedure/callable_alias_mapper.cpp
Normal file
62
src/query/procedure/callable_alias_mapper.cpp
Normal file
@ -0,0 +1,62 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "callable_alias_mapper.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <json/json.hpp>
|
||||
|
||||
#include "utils/logging.hpp"
|
||||
|
||||
namespace memgraph::query::procedure {
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
CallableAliasMapper gCallableAliasMapper;
|
||||
|
||||
void CallableAliasMapper::LoadMapping(const std::filesystem::path &path) {
|
||||
using json = nlohmann::json;
|
||||
if (path.empty()) {
|
||||
spdlog::info("Path to callable mappings was not set.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (std::filesystem::exists(path)) {
|
||||
const bool is_regular_file = std::filesystem::is_regular_file(path);
|
||||
const bool has_json_extension = (path.extension() == ".json");
|
||||
if (is_regular_file && has_json_extension) {
|
||||
std::ifstream mapping_file(path);
|
||||
try {
|
||||
json mapping_data = json::parse(mapping_file);
|
||||
mapping_ = mapping_data.get<std::unordered_map<std::string, std::string>>();
|
||||
} catch (...) {
|
||||
MG_ASSERT(false, "Parsing callable mapping was unsuccesful. Make sure it is in correct json format.");
|
||||
}
|
||||
} else {
|
||||
MG_ASSERT(false, "Path to callable mappings is not a regular file or does not have .json extension.");
|
||||
}
|
||||
} else {
|
||||
MG_ASSERT(false, "Path to callable mappings was set, but the path does not exist.");
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string_view> CallableAliasMapper::FindAlias(const std::string &name) const noexcept {
|
||||
if (!mapping_.contains(name)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return mapping_.at(name);
|
||||
}
|
||||
|
||||
} // namespace memgraph::query::procedure
|
45
src/query/procedure/callable_alias_mapper.hpp
Normal file
45
src/query/procedure/callable_alias_mapper.hpp
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace memgraph::query::procedure {
|
||||
|
||||
class CallableAliasMapper final {
|
||||
public:
|
||||
CallableAliasMapper() = default;
|
||||
CallableAliasMapper(const CallableAliasMapper &) = delete;
|
||||
CallableAliasMapper &operator=(const CallableAliasMapper &) = delete;
|
||||
CallableAliasMapper(CallableAliasMapper &&) = delete;
|
||||
CallableAliasMapper &operator=(CallableAliasMapper &&) = delete;
|
||||
~CallableAliasMapper() = default;
|
||||
|
||||
void LoadMapping(const std::filesystem::path &);
|
||||
[[nodiscard]] std::optional<std::string_view> FindAlias(const std::string &) const noexcept;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::string> mapping_;
|
||||
};
|
||||
|
||||
/// Single, global alias mapper.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
extern CallableAliasMapper gCallableAliasMapper;
|
||||
|
||||
} // namespace memgraph::query::procedure
|
@ -22,6 +22,7 @@ extern "C" {
|
||||
#include <unistd.h>
|
||||
|
||||
#include "py/py.hpp"
|
||||
#include "query/procedure/callable_alias_mapper.hpp"
|
||||
#include "query/procedure/mg_procedure_helpers.hpp"
|
||||
#include "query/procedure/py_module.hpp"
|
||||
#include "utils/file.hpp"
|
||||
@ -1326,10 +1327,25 @@ std::optional<std::pair<ModulePtr, const T *>> MakePairIfPropFound(const ModuleR
|
||||
}
|
||||
};
|
||||
auto result = FindModuleNameAndProp(module_registry, fully_qualified_name, memory);
|
||||
if (!result) return std::nullopt;
|
||||
if (!result) {
|
||||
return std::nullopt;
|
||||
}
|
||||
auto [module_name, prop_name] = *result;
|
||||
auto module = module_registry.GetModuleNamed(module_name);
|
||||
if (!module) return std::nullopt;
|
||||
if (!module) {
|
||||
// Check for possible callable aliases.
|
||||
const auto maybe_valid_alias = gCallableAliasMapper.FindAlias(std::string(fully_qualified_name));
|
||||
if (maybe_valid_alias) {
|
||||
result = FindModuleNameAndProp(module_registry, *maybe_valid_alias, memory);
|
||||
auto [module_name, prop_name] = *result;
|
||||
module = module_registry.GetModuleNamed(module_name);
|
||||
if (!module) {
|
||||
return std::nullopt;
|
||||
}
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
auto *prop = prop_fun(module);
|
||||
const auto &prop_it = prop->find(prop_name);
|
||||
if (prop_it == prop->end()) return std::nullopt;
|
||||
|
101
src/utils/java_string_formatter.hpp
Normal file
101
src/utils/java_string_formatter.hpp
Normal file
@ -0,0 +1,101 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "utils/exceptions.hpp"
|
||||
#include "utils/pmr/string.hpp"
|
||||
#include "utils/pmr/vector.hpp"
|
||||
|
||||
namespace memgraph::utils {
|
||||
|
||||
class JStringFormatException final : public BasicException {
|
||||
public:
|
||||
explicit JStringFormatException(const std::string &what) noexcept : BasicException(what) {}
|
||||
|
||||
template <class... Args>
|
||||
explicit JStringFormatException(fmt::format_string<Args...> fmt, Args &&...args) noexcept
|
||||
: JStringFormatException(fmt::format(fmt, std::forward<Args>(args)...)) {}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept TTypedValueLike = requires(T t) {
|
||||
{ t.ValueInt() } -> std::convertible_to<int>;
|
||||
{ t.ValueDouble() } -> std::convertible_to<double>;
|
||||
{ t.ValueString() } -> std::convertible_to<pmr::string>;
|
||||
};
|
||||
|
||||
template <typename TString, typename TTypedValueLike>
|
||||
class JStringFormatter final {
|
||||
public:
|
||||
[[nodiscard]] TString FormatString(TString str, const pmr::vector<TTypedValueLike> &format_args) const {
|
||||
std::size_t found{0U};
|
||||
std::size_t arg_index{0U};
|
||||
|
||||
while (true) {
|
||||
found = str.find('%', found);
|
||||
if (found == std::string::npos) {
|
||||
break;
|
||||
}
|
||||
|
||||
const bool ends_with_percentile = (found == str.size() - 1U);
|
||||
if (ends_with_percentile) {
|
||||
break;
|
||||
}
|
||||
|
||||
const auto format_specifier = str.at(found + 1U);
|
||||
if (!std::isalpha(format_specifier)) {
|
||||
++found;
|
||||
continue;
|
||||
}
|
||||
const bool does_argument_list_overflow = (format_args.size() < arg_index + 1U) && (arg_index > 0U);
|
||||
if (does_argument_list_overflow) {
|
||||
throw JStringFormatException(
|
||||
"There are more format specifiers in the CALL procedure error message, then arguments provided.");
|
||||
}
|
||||
const bool arg_count_exceeds_format_spec_count = (arg_index > format_args.size() - 1U);
|
||||
if (arg_count_exceeds_format_spec_count) {
|
||||
break;
|
||||
}
|
||||
|
||||
ReplaceFormatSpecifier(str, found, format_specifier, format_args.at(arg_index));
|
||||
++arg_index;
|
||||
++found;
|
||||
}
|
||||
|
||||
str.shrink_to_fit();
|
||||
return str;
|
||||
}
|
||||
|
||||
private:
|
||||
void ReplaceFormatSpecifier(TString &str, std::size_t pos, char format_specifier, TTypedValueLike current_arg) const {
|
||||
std::string replacement_str;
|
||||
switch (format_specifier) {
|
||||
case 'd':
|
||||
replacement_str = std::to_string(current_arg.ValueInt());
|
||||
break;
|
||||
case 'f':
|
||||
replacement_str = std::to_string(current_arg.ValueDouble());
|
||||
break;
|
||||
case 's':
|
||||
replacement_str = current_arg.ValueString();
|
||||
break;
|
||||
default:
|
||||
throw JStringFormatException("Format specifier %'{}', in CALL procedure is not supported.", format_specifier);
|
||||
}
|
||||
|
||||
str.replace(pos, 2U, replacement_str);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace memgraph::utils
|
@ -55,6 +55,7 @@ add_subdirectory(python_query_modules_reloading)
|
||||
add_subdirectory(analyze_graph)
|
||||
add_subdirectory(transaction_queue)
|
||||
add_subdirectory(mock_api)
|
||||
add_subdirectory(graphql)
|
||||
add_subdirectory(disk_storage)
|
||||
add_subdirectory(load_csv)
|
||||
add_subdirectory(init_file_flags)
|
||||
|
@ -192,4 +192,9 @@ startup_config_dict = {
|
||||
"false",
|
||||
"Restore replication state on startup, e.g. recover replica",
|
||||
),
|
||||
"query_callable_mappings_path": (
|
||||
"",
|
||||
"",
|
||||
"The path to mappings that describes aliases to callables in cypher queries in the form of key-value pairs in a json file. With this option query module procedures that do not exist in memgraph can be mapped to ones that exist.",
|
||||
),
|
||||
}
|
||||
|
4
tests/e2e/graphql/.gitignore
vendored
Normal file
4
tests/e2e/graphql/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
node_modules/
|
||||
# Because the parent folder ignores *.json
|
||||
!callable_alias_mapping.json
|
||||
!package.json
|
10
tests/e2e/graphql/CMakeLists.txt
Normal file
10
tests/e2e/graphql/CMakeLists.txt
Normal file
@ -0,0 +1,10 @@
|
||||
function(copy_graphql_e2e_python_files FILE_NAME)
|
||||
copy_e2e_python_files(graphql ${FILE_NAME})
|
||||
endfunction()
|
||||
|
||||
copy_graphql_e2e_python_files(graphql_crud.py)
|
||||
copy_graphql_e2e_python_files(graphql_server.py)
|
||||
copy_graphql_e2e_python_files(callable_alias_mapping.json)
|
||||
|
||||
add_subdirectory(graphql_library_config)
|
||||
add_subdirectory(temporary_procedures)
|
4
tests/e2e/graphql/callable_alias_mapping.json
Normal file
4
tests/e2e/graphql/callable_alias_mapping.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"dbms.components": "mgps.components",
|
||||
"apoc.util.validate": "mgps.validate"
|
||||
}
|
105
tests/e2e/graphql/graphql_crud.py
Normal file
105
tests/e2e/graphql/graphql_crud.py
Normal file
@ -0,0 +1,105 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
from graphql_server import *
|
||||
|
||||
|
||||
def test_create_query(query_server):
|
||||
query = 'mutation{createUsers(input:[{name:"John Doe"}]){users{id name}}}'
|
||||
gotten = query_server.send_query(query)
|
||||
expected_result = (
|
||||
'{"data":{"createUsers":{"users":[{"id":"e2d65187-d522-47bf-9791-6c66dd8fd672","name":"John Doe"}]}}}'
|
||||
)
|
||||
assert server_returned_expected(expected_result, gotten)
|
||||
|
||||
|
||||
def test_nested_create_query(query_server):
|
||||
query = """
|
||||
mutation {
|
||||
createUsers(input: [
|
||||
{
|
||||
name: "John Doe"
|
||||
posts: {
|
||||
create: [
|
||||
{
|
||||
node: {
|
||||
content: "Hi, my name is John!"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]) {
|
||||
users {
|
||||
id
|
||||
name
|
||||
posts {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
expected_result = '{"data":{"createUsers":{"users":[{"id": "361004b7-f92d-4df0-9f96-5b43602c0f25","name": "John Doe","posts":[{"id":"e8d2033f-c15e-4529-a4f8-ca2ae09a066b", "content": "Hi, my name is John!"}]}]}}}'
|
||||
gotten_response = query_server.send_query(query)
|
||||
assert server_returned_expected(expected_result, gotten_response)
|
||||
|
||||
|
||||
def test_delete_node_query(query_server):
|
||||
created_node_uuid = create_node_query(query_server)
|
||||
|
||||
delete_query = 'mutation{deleteUsers(where:{id:"' + created_node_uuid + '"}){nodesDeleted relationshipsDeleted}}'
|
||||
expected_delete_response = '{"data":{"deleteUsers":{"nodesDeleted":1,"relationshipsDeleted":0}}}\n'
|
||||
|
||||
gotten = query_server.send_query(delete_query)
|
||||
assert expected_delete_response == str(gotten.text)
|
||||
|
||||
|
||||
def test_nested_delete_node_query(query_server):
|
||||
node_uuids = create_related_nodes_query(query_server)
|
||||
created_user_uuid = node_uuids[0]
|
||||
|
||||
delete_query = (
|
||||
'mutation {deleteUsers(where: {id: "'
|
||||
+ created_user_uuid
|
||||
+ '"},delete: {posts: {where: {}}}) {nodesDeleted relationshipsDeleted}}'
|
||||
)
|
||||
expected_delete_response = '{"data":{"deleteUsers":{"nodesDeleted":2,"relationshipsDeleted":1}}}\n'
|
||||
|
||||
gotten = query_server.send_query(delete_query)
|
||||
assert expected_delete_response == str(gotten.text)
|
||||
|
||||
|
||||
def test_update_node(query_server):
|
||||
node_uuids = create_related_nodes_query(query_server)
|
||||
created_post_uuid = node_uuids[1]
|
||||
|
||||
update_query = (
|
||||
'mutation {updatePosts(where: {id: "'
|
||||
+ created_post_uuid
|
||||
+ '"}update: {content: "Some new content for this Post!"}) {posts {content}}}'
|
||||
)
|
||||
expected_update_response = '{"data":{"updatePosts":{"posts":[{"content":"Some new content for this Post!"}]}}}\n'
|
||||
|
||||
gotten = query_server.send_query(update_query)
|
||||
assert expected_update_response == str(gotten.text)
|
||||
|
||||
|
||||
def test_connect_or_create(query_server):
|
||||
created_user_uuid = create_node_query(query_server)
|
||||
|
||||
connect_or_create_query = (
|
||||
'mutation {updateUsers(update: {posts: {connectOrCreate: {where: { node: { id: "1234" } }onCreate: { node: { content: "Some content" } }}}},where: { id: "'
|
||||
+ created_user_uuid
|
||||
+ '" }) {info {nodesCreated}}}'
|
||||
)
|
||||
expected_response = '{"data":{"updateUsers":{"info":{"nodesCreated":1}}}}\n'
|
||||
|
||||
gotten = query_server.send_query(connect_or_create_query)
|
||||
assert expected_response == str(gotten.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-rA"]))
|
1
tests/e2e/graphql/graphql_library_config/CMakeLists.txt
Normal file
1
tests/e2e/graphql/graphql_library_config/CMakeLists.txt
Normal file
@ -0,0 +1 @@
|
||||
copy_graphql_e2e_python_files(crud.js)
|
34
tests/e2e/graphql/graphql_library_config/crud.js
Normal file
34
tests/e2e/graphql/graphql_library_config/crud.js
Normal file
@ -0,0 +1,34 @@
|
||||
const { Neo4jGraphQL } = require("@neo4j/graphql");
|
||||
const { ApolloServer, gql } = require("apollo-server");
|
||||
const neo4j = require("neo4j-driver");
|
||||
|
||||
const typeDefs = gql`
|
||||
type Post {
|
||||
id: ID! @id
|
||||
content: String!
|
||||
creator: User! @relationship(type: "HAS_POST", direction: IN)
|
||||
}
|
||||
|
||||
type User {
|
||||
id: ID! @id
|
||||
name: String
|
||||
posts: [Post!]! @relationship(type: "HAS_POST", direction: OUT)
|
||||
}
|
||||
`;
|
||||
|
||||
const driver = neo4j.driver(
|
||||
"bolt://localhost:7687",
|
||||
neo4j.auth.basic("", "")
|
||||
);
|
||||
|
||||
const neoSchema = new Neo4jGraphQL({ typeDefs, driver });
|
||||
|
||||
neoSchema.getSchema().then((schema) => {
|
||||
const server = new ApolloServer({
|
||||
schema,
|
||||
});
|
||||
|
||||
server.listen().then(({ url }) => {
|
||||
console.log(`🚀 Server ready at ${url}`);
|
||||
});
|
||||
})
|
149
tests/e2e/graphql/graphql_server.py
Normal file
149
tests/e2e/graphql/graphql_server.py
Normal file
@ -0,0 +1,149 @@
|
||||
import atexit
|
||||
import collections.abc
|
||||
import json
|
||||
import os.path
|
||||
import socket
|
||||
import subprocess
|
||||
import time
|
||||
from uuid import UUID
|
||||
|
||||
import pytest
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
class GraphQLServer:
|
||||
def __init__(self, config_file_path: str):
|
||||
self.url = "http://127.0.0.1:4000"
|
||||
|
||||
self.graphql_lib = subprocess.Popen(["node", config_file_path], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
|
||||
self.__wait_process_to_init(7687)
|
||||
self.__wait_process_to_init(4000)
|
||||
atexit.register(self.__shut_down)
|
||||
|
||||
def send_query(self, query: str, timeout=5.0) -> requests.Response:
|
||||
try:
|
||||
response = requests.post(self.url, json={"query": query}, timeout=timeout)
|
||||
except requests.exceptions.Timeout as err:
|
||||
print("Request to GraphQL server has timed out. Details:", err)
|
||||
else:
|
||||
return response
|
||||
|
||||
def __wait_process_to_init(self, port):
|
||||
host = "127.0.0.1"
|
||||
try:
|
||||
while True:
|
||||
# Create a socket object
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
||||
s.settimeout(5)
|
||||
result = s.connect_ex((host, port))
|
||||
if result == 0:
|
||||
break
|
||||
|
||||
except socket.error as e:
|
||||
print(f"Error occurred while checking port {port}: {e}")
|
||||
return False
|
||||
|
||||
def __shut_down(self):
|
||||
self.graphql_lib.kill()
|
||||
ls = subprocess.Popen(("lsof", "-t", "-i:4000"), stdout=subprocess.PIPE)
|
||||
subprocess.check_output(("xargs", "-r", "kill"), stdin=ls.stdout)
|
||||
ls.wait()
|
||||
|
||||
|
||||
def _ordered(obj: any) -> any:
|
||||
if isinstance(obj, dict):
|
||||
return sorted((k, _ordered(v)) for k, v in obj.items())
|
||||
if isinstance(obj, list):
|
||||
return sorted(_ordered(x) for x in obj)
|
||||
else:
|
||||
return obj
|
||||
|
||||
|
||||
def _flatten(x: any) -> list:
|
||||
result = []
|
||||
for el in x:
|
||||
if isinstance(x, collections.abc.Iterable) and not isinstance(el, str):
|
||||
result.extend(_flatten(el))
|
||||
else:
|
||||
result.append(el)
|
||||
return result
|
||||
|
||||
|
||||
def _valid_uuid(uuid_to_test: any, version: int = 4) -> any:
|
||||
try:
|
||||
uuid_obj = UUID(uuid_to_test, version=version)
|
||||
except ValueError:
|
||||
return False
|
||||
return str(uuid_obj) == uuid_to_test
|
||||
|
||||
|
||||
def server_returned_expected(expected_string: str, server_response: requests.Response) -> bool:
|
||||
expected_json = json.loads(expected_string)
|
||||
server_response_json = json.loads(server_response.text)
|
||||
|
||||
expected = _flatten(_ordered(expected_json))
|
||||
actual = _flatten(_ordered(server_response_json))
|
||||
|
||||
for expected_item, actual_item in zip(expected, actual):
|
||||
if expected_item != actual_item and not (_valid_uuid(expected_item)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def get_uuid_from_response(response: requests.Response) -> list:
|
||||
response_json = json.loads(response.text)
|
||||
flattened_response = _flatten(_ordered(response_json))
|
||||
uuids = []
|
||||
for item in flattened_response:
|
||||
if _valid_uuid(item):
|
||||
uuids.append(str(item))
|
||||
return uuids
|
||||
|
||||
|
||||
def create_node_query(server: GraphQLServer):
|
||||
query = 'mutation{createUsers(input:[{name:"John Doe"}]){users{id name}}}'
|
||||
gotten = server.send_query(query)
|
||||
uuids = get_uuid_from_response(gotten)
|
||||
return uuids[0]
|
||||
|
||||
|
||||
def create_related_nodes_query(server: GraphQLServer):
|
||||
query = """
|
||||
mutation {
|
||||
createUsers(input: [
|
||||
{
|
||||
name: "John Doe"
|
||||
posts: {
|
||||
create: [
|
||||
{
|
||||
node: {
|
||||
content: "Hi, my name is John!"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]) {
|
||||
users {
|
||||
id
|
||||
name
|
||||
posts {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
gotten_response = server.send_query(query)
|
||||
return get_uuid_from_response(gotten_response)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def query_server() -> GraphQLServer:
|
||||
path = os.path.join("graphql/graphql_library_config/crud.js")
|
||||
return GraphQLServer(path)
|
9
tests/e2e/graphql/package.json
Normal file
9
tests/e2e/graphql/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@apollo/server": "^4.8.1",
|
||||
"@neo4j/graphql": "^3.24.0",
|
||||
"apollo-server": "^3.12.0",
|
||||
"graphql": "^16.7.1",
|
||||
"neo4j-driver": "^5.10.0"
|
||||
}
|
||||
}
|
9
tests/e2e/graphql/setup.sh
Executable file
9
tests/e2e/graphql/setup.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
set -Eeuo pipefail
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
source "$SCRIPT_DIR/../../util.sh"
|
||||
setup_node
|
||||
|
||||
npm i
|
1
tests/e2e/graphql/temporary_procedures/CMakeLists.txt
Normal file
1
tests/e2e/graphql/temporary_procedures/CMakeLists.txt
Normal file
@ -0,0 +1 @@
|
||||
copy_graphql_e2e_python_files(mgps.py)
|
8
tests/e2e/graphql/temporary_procedures/mgps.py
Normal file
8
tests/e2e/graphql/temporary_procedures/mgps.py
Normal file
@ -0,0 +1,8 @@
|
||||
import typing
|
||||
|
||||
import mgp
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def components(context: mgp.ProcCtx) -> mgp.Record(versions=list, edition=str):
|
||||
return mgp.Record(versions=["4.3"], edition="4.3.2")
|
35
tests/e2e/graphql/workloads.yaml
Normal file
35
tests/e2e/graphql/workloads.yaml
Normal file
@ -0,0 +1,35 @@
|
||||
args: &args
|
||||
- "--bolt-port"
|
||||
- "7687"
|
||||
- "--log-level"
|
||||
- "TRACE"
|
||||
- "--query-callable-mappings-path"
|
||||
- "graphql/callable_alias_mapping.json"
|
||||
|
||||
in_memory_cluster: &in_memory_cluster
|
||||
cluster:
|
||||
main:
|
||||
args: *args
|
||||
log_file: "graphql-e2e.log"
|
||||
setup_queries: []
|
||||
validation_queries: []
|
||||
|
||||
disk_cluster: &disk_cluster
|
||||
cluster:
|
||||
main:
|
||||
args: *args
|
||||
log_file: "graphql-e2e.log"
|
||||
setup_queries: ["STORAGE MODE ON_DISK_TRANSACTIONAL"]
|
||||
validation_queries: []
|
||||
|
||||
workloads:
|
||||
- name: "GraphQL crud"
|
||||
binary: "tests/e2e/pytest_runner.sh"
|
||||
proc: "tests/e2e/graphql/temporary_procedures/"
|
||||
args: ["graphql/graphql_crud.py"]
|
||||
<<: *in_memory_cluster
|
||||
- name: "Disk GraphQL crud"
|
||||
binary: "tests/e2e/pytest_runner.sh"
|
||||
proc: "tests/e2e/graphql/temporary_procedures/"
|
||||
args: ["graphql/graphql_crud.py"]
|
||||
<<: *disk_cluster
|
@ -1,4 +1,6 @@
|
||||
#!/bin/bash
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$SCRIPT_DIR"
|
||||
# TODO(gitbuda): Setup mgclient and pymgclient properly.
|
||||
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:../../libs/mgclient/lib
|
||||
|
||||
@ -17,6 +19,9 @@ check_license() {
|
||||
fi
|
||||
}
|
||||
|
||||
source "$SCRIPT_DIR/../util.sh"
|
||||
setup_node
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
check_license
|
||||
# NOTE: If you want to run all tests under specific folder/section just
|
||||
|
@ -1,10 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
# shellcheck disable=1091
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$DIR"
|
||||
|
||||
PIP_DEPS=(
|
||||
"behave==1.2.6"
|
||||
"ldap3==2.6"
|
||||
@ -19,14 +19,11 @@ PIP_DEPS=(
|
||||
"networkx==2.4"
|
||||
"gqlalchemy==1.3.3"
|
||||
)
|
||||
cd "$DIR"
|
||||
|
||||
# Remove old virtualenv.
|
||||
# Remove old and create a new virtualenv.
|
||||
if [ -d ve3 ]; then
|
||||
rm -rf ve3
|
||||
fi
|
||||
|
||||
# Create new virtualenv.
|
||||
virtualenv -p python3 ve3
|
||||
set +u
|
||||
source "ve3/bin/activate"
|
||||
@ -37,7 +34,6 @@ PYTHON_MINOR=$(python3 -c 'import sys; print(sys.version_info[:][1])')
|
||||
|
||||
# install pulsar-client
|
||||
pip --timeout 1000 install "pulsar-client==3.1.0"
|
||||
|
||||
for pkg in "${PIP_DEPS[@]}"; do
|
||||
pip --timeout 1000 install "$pkg"
|
||||
done
|
||||
@ -51,3 +47,5 @@ CFLAGS="-std=c99" python3 setup.py install
|
||||
popd > /dev/null
|
||||
|
||||
deactivate
|
||||
|
||||
"$DIR"/e2e/graphql/setup.sh
|
||||
|
@ -287,6 +287,9 @@ target_link_libraries(${test_prefix}utils_settings mg-utils mg-settings)
|
||||
add_unit_test(utils_temporal utils_temporal.cpp)
|
||||
target_link_libraries(${test_prefix}utils_temporal mg-utils)
|
||||
|
||||
add_unit_test(utils_java_string_formatter.cpp)
|
||||
target_link_libraries(${test_prefix}utils_java_string_formatter mg-utils)
|
||||
|
||||
# Test mg-storage-v2
|
||||
add_unit_test(commit_log_v2.cpp)
|
||||
target_link_libraries(${test_prefix}commit_log_v2 gflags mg-utils mg-storage-v2)
|
||||
|
221
tests/unit/utils_java_string_formatter.cpp
Normal file
221
tests/unit/utils_java_string_formatter.cpp
Normal file
@ -0,0 +1,221 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include <iostream>
|
||||
#include <string_view>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <utils/java_string_formatter.hpp>
|
||||
#include <utils/pmr/string.hpp>
|
||||
|
||||
using TString = memgraph::utils::pmr::string;
|
||||
|
||||
namespace {
|
||||
|
||||
class DummyTypedValue {
|
||||
public:
|
||||
template <typename T>
|
||||
DummyTypedValue(T &&val) : data_(std::forward<decltype(val)>(val)) {}
|
||||
|
||||
int ValueInt() const noexcept {
|
||||
auto *value = std::get_if<int>(&data_);
|
||||
MG_ASSERT(value);
|
||||
return *value;
|
||||
}
|
||||
double ValueDouble() const noexcept {
|
||||
auto *value = std::get_if<double>(&data_);
|
||||
MG_ASSERT(value);
|
||||
return *value;
|
||||
}
|
||||
TString ValueString() const noexcept {
|
||||
auto *value = std::get_if<TString>(&data_);
|
||||
MG_ASSERT(value);
|
||||
return *value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::variant<int, double, TString> data_;
|
||||
};
|
||||
|
||||
auto GetVector() { return memgraph::utils::pmr::vector<DummyTypedValue>(memgraph::utils::NewDeleteResource()); }
|
||||
auto GetString() { return TString(memgraph::utils::NewDeleteResource()); }
|
||||
|
||||
memgraph::utils::JStringFormatter<TString, DummyTypedValue> gFormatter;
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST(JavaStringFormatter, FormatesOneIntegerCharacter) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
str = "\%d";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "1");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatesOneDoubleCharacter) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1.0);
|
||||
str = "\%f";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "1.000000");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatesOneSimpleStringCharacter) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
auto replacement_str = GetString();
|
||||
replacement_str = "x";
|
||||
args.emplace_back(replacement_str);
|
||||
str = "\%s";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "x");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatesOneComplexStringCharacter) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
auto replacement_str = GetString();
|
||||
replacement_str = "moja najdraža boja je zelena";
|
||||
args.emplace_back(replacement_str);
|
||||
str = "\%s";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "moja najdraža boja je zelena");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatesMoreCharacters) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
args.emplace_back(2);
|
||||
args.emplace_back(3);
|
||||
str = "\%d\%d\%d";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "123");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatesMoreCharactersInSentence) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
args.emplace_back(10);
|
||||
args.emplace_back(10.0);
|
||||
str = "The chances of picking \%d out of \%d matches is \%f%";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "The chances of picking 1 out of 10 matches is 10.000000%");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormateSinglePecrentile) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
str = "%";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "%");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatPercineleBeforeFormatString) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
str = "%\%d";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "\%1");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatPercineleAfterFormatString) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
str = "\%d%";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "1\%");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatPercineleInBetweenFormatStrings) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
str = "%\%d%";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "\%1\%");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, FormatManyPercentilesOneFormatString) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
str = "Some% random% strings here% and there wit% h \%da bunch of percent% iles and one format%-string.";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "Some% random% strings here% and there wit% h 1a bunch of percent% iles and one format%-string.");
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, ThrowOnNonExistentFormatSpecifier) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
args.emplace_back(1);
|
||||
str = "\%x";
|
||||
|
||||
ASSERT_THROW(gFormatter.FormatString(str, args), memgraph::utils::JStringFormatException);
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, ThrowOnLessFormatSpecifiersThanSlots) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
auto replacement_str = GetString();
|
||||
replacement_str = "format specifiers";
|
||||
args.emplace_back(3);
|
||||
args.emplace_back(replacement_str);
|
||||
str = "There is \%d \%s in this sentence but only \%d specified in the argument list";
|
||||
|
||||
ASSERT_THROW(gFormatter.FormatString(str, args), memgraph::utils::JStringFormatException);
|
||||
}
|
||||
|
||||
TEST(JavaStringFormatter, DoNotThrowOnMoreFormatSpecifiersThanSlots) {
|
||||
auto args = GetVector();
|
||||
auto str = GetString();
|
||||
auto replacement_str = GetString();
|
||||
replacement_str = "format specifiers";
|
||||
args.emplace_back(3);
|
||||
args.emplace_back(replacement_str);
|
||||
args.emplace_back(123);
|
||||
args.emplace_back(123);
|
||||
str = "There is \%d \%s in this sentence but only \%d specified in the argument list";
|
||||
|
||||
auto result = gFormatter.FormatString(str, args);
|
||||
|
||||
ASSERT_EQ(result, "There is 3 format specifiers in this sentence but only 123 specified in the argument list");
|
||||
}
|
28
tests/util.sh
Normal file
28
tests/util.sh
Normal file
@ -0,0 +1,28 @@
|
||||
#!/bin/bash
|
||||
|
||||
setup_node() {
|
||||
if [ -f "$HOME/.nvm/nvm.sh" ]; then
|
||||
. "$HOME/.nvm/nvm.sh"
|
||||
nvm install 14
|
||||
nvm use 14
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null; then
|
||||
echo "Could NOT node. Make sure node is installed."
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v npm >/dev/null; then
|
||||
echo "Could NOT npm. Make sure npm is installed."
|
||||
exit 1
|
||||
fi
|
||||
node_version=$(node --version)
|
||||
npm_version=$(npm --version)
|
||||
echo "NODE VERSION: $node_version"
|
||||
echo "NPM VERSION: $npm_version"
|
||||
node_major_version=${node_version##v}
|
||||
node_major_version=${node_major_version%%.*}
|
||||
if [ ! "$node_major_version" -ge 14 ]; then
|
||||
echo "ERROR: It's required to have node >= 14."
|
||||
exit 1
|
||||
fi
|
||||
}
|
Loading…
Reference in New Issue
Block a user