Merge branch 'master' into text-search-integration-poc

This commit is contained in:
Ante Pušić 2024-01-16 16:26:45 +01:00 committed by GitHub
commit 605a425bb3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 537 additions and 58 deletions

32
ADRs/001_tantivy.md Normal file
View File

@ -0,0 +1,32 @@
# Tantivy ADR
**Author**
Marko Budiselic (github.com/gitbuda)
**Status**
APPROVED
**Date**
January 5, 2024
**Problem**
For some of Memgraph workloads, text search is a required feature. We don't
want to build a new text search engine because that's not Memgraph's core
value.
**Criteria**
- easy integration with our C++ codebase
- ability to operate in-memory and on-disk
- sufficient features (regex, full-text search, fuzzy search, aggregations over
text data)
- production-ready
**Decision**
All known C++ libraries are not production-ready. Recent Rust libraries, in
particular [Tantivy](https://github.com/quickwit-oss/tantivy), seem to provide
much more features, it is production ready. The way how we'll integrate Tantivy
into the current Memgraph codebase is via
[cxx](https://github.com/dtolnay/cxx). **We select Tantivy.**

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -36,14 +36,22 @@ namespace memgraph::memory {
void QueriesMemoryControl::UpdateThreadToTransactionId(const std::thread::id &thread_id, uint64_t transaction_id) {
auto accessor = thread_id_to_transaction_id.access();
accessor.insert({thread_id, transaction_id});
auto elem = accessor.find(thread_id);
if (elem == accessor.end()) {
accessor.insert({thread_id, {transaction_id, 1}});
} else {
elem->transaction_id.cnt++;
}
}
void QueriesMemoryControl::EraseThreadToTransactionId(const std::thread::id &thread_id, uint64_t transaction_id) {
auto accessor = thread_id_to_transaction_id.access();
auto elem = accessor.find(thread_id);
MG_ASSERT(elem != accessor.end() && elem->transaction_id == transaction_id);
accessor.remove(thread_id);
elem->transaction_id.cnt--;
if (elem->transaction_id.cnt == 0) {
accessor.remove(thread_id);
}
}
void QueriesMemoryControl::TrackAllocOnCurrentThread(size_t size) {

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -78,9 +78,20 @@ class QueriesMemoryControl {
bool IsThreadTracked();
private:
struct TransactionId {
uint64_t id;
uint64_t cnt;
bool operator<(const TransactionId &other) const { return id < other.id; }
bool operator==(const TransactionId &other) const { return id == other.id; }
bool operator<(uint64_t other) const { return id < other; }
bool operator==(uint64_t other) const { return id == other; }
};
struct ThreadIdToTransactionId {
std::thread::id thread_id;
uint64_t transaction_id;
TransactionId transaction_id;
bool operator<(const ThreadIdToTransactionId &other) const { return thread_id < other.thread_id; }
bool operator==(const ThreadIdToTransactionId &other) const { return thread_id == other.thread_id; }
@ -98,6 +109,9 @@ class QueriesMemoryControl {
bool operator<(uint64_t other) const { return transaction_id < other; }
bool operator==(uint64_t other) const { return transaction_id == other; }
bool operator<(TransactionId other) const { return transaction_id < other.id; }
bool operator==(TransactionId other) const { return transaction_id == other.id; }
};
utils::SkipList<ThreadIdToTransactionId> thread_id_to_transaction_id;

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -12,7 +12,6 @@
#include "query/cypher_query_interpreter.hpp"
#include "query/frontend/ast/cypher_main_visitor.hpp"
#include "query/frontend/opencypher/parser.hpp"
#include "utils/synchronized.hpp"
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
DEFINE_bool(query_cost_planner, true, "Use the cost-estimating query planner.");
@ -80,7 +79,7 @@ ParsedQuery ParseQuery(const std::string &query_string, const std::map<std::stri
// Convert the ANTLR4 parse tree into an AST.
AstStorage ast_storage;
frontend::ParsingContext context{.is_query_cached = true};
frontend::CypherMainVisitor visitor(context, &ast_storage);
frontend::CypherMainVisitor visitor(context, &ast_storage, &parameters);
visitor.visit(parser->tree());

View File

@ -293,4 +293,7 @@ constexpr utils::TypeInfo query::ShowDatabasesQuery::kType{utils::TypeId::AST_SH
constexpr utils::TypeInfo query::EdgeImportModeQuery::kType{utils::TypeId::AST_EDGE_IMPORT_MODE_QUERY,
"EdgeImportModeQuery", &query::Query::kType};
constexpr utils::TypeInfo query::PatternComprehension::kType{utils::TypeId::AST_PATTERN_COMPREHENSION,
"PatternComprehension", &query::Expression::kType};
} // namespace memgraph

View File

@ -3529,6 +3529,65 @@ class Exists : public memgraph::query::Expression {
friend class AstStorage;
};
class PatternComprehension : public memgraph::query::Expression {
public:
static const utils::TypeInfo kType;
const utils::TypeInfo &GetTypeInfo() const override { return kType; }
PatternComprehension() = default;
DEFVISITABLE(ExpressionVisitor<TypedValue>);
DEFVISITABLE(ExpressionVisitor<TypedValue *>);
DEFVISITABLE(ExpressionVisitor<void>);
bool Accept(HierarchicalTreeVisitor &visitor) override {
if (visitor.PreVisit(*this)) {
if (variable_) {
variable_->Accept(visitor);
}
pattern_->Accept(visitor);
if (filter_) {
filter_->Accept(visitor);
}
resultExpr_->Accept(visitor);
}
return visitor.PostVisit(*this);
}
PatternComprehension *MapTo(const Symbol &symbol) {
symbol_pos_ = symbol.position();
return this;
}
// The variable name.
Identifier *variable_{nullptr};
// The pattern to match.
Pattern *pattern_{nullptr};
// Optional WHERE clause for filtering.
Where *filter_{nullptr};
// The projection expression.
Expression *resultExpr_{nullptr};
/// Symbol table position of the symbol this Aggregation is mapped to.
int32_t symbol_pos_{-1};
PatternComprehension *Clone(AstStorage *storage) const override {
PatternComprehension *object = storage->Create<PatternComprehension>();
object->pattern_ = pattern_ ? pattern_->Clone(storage) : nullptr;
object->filter_ = filter_ ? filter_->Clone(storage) : nullptr;
object->resultExpr_ = resultExpr_ ? resultExpr_->Clone(storage) : nullptr;
object->symbol_pos_ = symbol_pos_;
return object;
}
protected:
PatternComprehension(Identifier *variable, Pattern *pattern) : variable_(variable), pattern_(pattern) {}
private:
friend class AstStorage;
};
class CallSubquery : public memgraph::query::Clause {
public:
static const utils::TypeInfo kType;

View File

@ -107,6 +107,7 @@ class Exists;
class MultiDatabaseQuery;
class ShowDatabasesQuery;
class EdgeImportModeQuery;
class PatternComprehension;
using TreeCompositeVisitor = utils::CompositeVisitor<
SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator,
@ -116,7 +117,7 @@ using TreeCompositeVisitor = utils::CompositeVisitor<
MapProjectionLiteral, PropertyLookup, AllPropertiesLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce,
Extract, All, Single, Any, None, CallProcedure, Create, Match, Return, With, Pattern, NodeAtom, EdgeAtom, Delete,
Where, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, Merge, Unwind, RegexMatch, LoadCsv,
Foreach, Exists, CallSubquery, CypherQuery>;
Foreach, Exists, CallSubquery, CypherQuery, PatternComprehension>;
using TreeLeafVisitor = utils::LeafVisitor<Identifier, PrimitiveLiteral, ParameterLookup>;
@ -137,7 +138,7 @@ class ExpressionVisitor
ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator,
ListLiteral, MapLiteral, MapProjectionLiteral, PropertyLookup, AllPropertiesLookup,
LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, None,
ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch, Exists> {};
ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch, Exists, PatternComprehension> {};
template <class TResult>
class QueryVisitor

View File

@ -1844,7 +1844,15 @@ antlrcpp::Any CypherMainVisitor::visitNodePattern(MemgraphCypher::NodePatternCon
antlrcpp::Any CypherMainVisitor::visitNodeLabels(MemgraphCypher::NodeLabelsContext *ctx) {
std::vector<LabelIx> labels;
for (auto *node_label : ctx->nodeLabel()) {
labels.push_back(AddLabel(std::any_cast<std::string>(node_label->accept(this))));
if (node_label->labelName()->symbolicName()) {
labels.emplace_back(AddLabel(std::any_cast<std::string>(node_label->accept(this))));
} else {
// If we have a parameter, we have to resolve it.
const auto *param_lookup = std::any_cast<ParameterLookup *>(node_label->accept(this));
const auto label_name = parameters_->AtTokenPosition(param_lookup->token_position_).ValueString();
labels.emplace_back(storage_->GetLabelIx(label_name));
query_info_.is_cacheable = false; // We can't cache queries with label parameters.
}
}
return labels;
}
@ -1980,6 +1988,18 @@ antlrcpp::Any CypherMainVisitor::visitPatternElement(MemgraphCypher::PatternElem
return pattern;
}
antlrcpp::Any CypherMainVisitor::visitRelationshipsPattern(MemgraphCypher::RelationshipsPatternContext *ctx) {
auto *pattern = storage_->Create<Pattern>();
pattern->atoms_.push_back(std::any_cast<NodeAtom *>(ctx->nodePattern()->accept(this)));
for (auto *pattern_element_chain : ctx->patternElementChain()) {
auto element = std::any_cast<std::pair<PatternAtom *, PatternAtom *>>(pattern_element_chain->accept(this));
pattern->atoms_.push_back(element.first);
pattern->atoms_.push_back(element.second);
}
anonymous_identifiers.push_back(&pattern->identifier_);
return pattern;
}
antlrcpp::Any CypherMainVisitor::visitPatternElementChain(MemgraphCypher::PatternElementChainContext *ctx) {
return std::pair<PatternAtom *, PatternAtom *>(std::any_cast<EdgeAtom *>(ctx->relationshipPattern()->accept(this)),
std::any_cast<NodeAtom *>(ctx->nodePattern()->accept(this)));
@ -2474,6 +2494,8 @@ antlrcpp::Any CypherMainVisitor::visitAtom(MemgraphCypher::AtomContext *ctx) {
return static_cast<Expression *>(storage_->Create<Extract>(ident, list, expr));
} else if (ctx->existsExpression()) {
return std::any_cast<Expression *>(ctx->existsExpression()->accept(this));
} else if (ctx->patternComprehension()) {
return std::any_cast<Expression *>(ctx->patternComprehension()->accept(this));
}
// TODO: Implement this. We don't support comprehensions, filtering... at
@ -2534,6 +2556,19 @@ antlrcpp::Any CypherMainVisitor::visitExistsExpression(MemgraphCypher::ExistsExp
return static_cast<Expression *>(exists);
}
antlrcpp::Any CypherMainVisitor::visitPatternComprehension(MemgraphCypher::PatternComprehensionContext *ctx) {
auto *comprehension = storage_->Create<PatternComprehension>();
if (ctx->variable()) {
comprehension->variable_ = storage_->Create<Identifier>(std::any_cast<std::string>(ctx->variable()->accept(this)));
}
comprehension->pattern_ = std::any_cast<Pattern *>(ctx->relationshipsPattern()->accept(this));
if (ctx->where()) {
comprehension->filter_ = std::any_cast<Where *>(ctx->where()->accept(this));
}
comprehension->resultExpr_ = std::any_cast<Expression *>(ctx->expression()->accept(this));
return static_cast<Expression *>(comprehension);
}
antlrcpp::Any CypherMainVisitor::visitParenthesizedExpression(MemgraphCypher::ParenthesizedExpressionContext *ctx) {
return std::any_cast<Expression *>(ctx->expression()->accept(this));
}

View File

@ -17,6 +17,7 @@
#include "query/frontend/ast/ast.hpp"
#include "query/frontend/opencypher/generated/MemgraphCypherBaseVisitor.h"
#include "query/parameters.hpp"
#include "utils/exceptions.hpp"
#include "utils/logging.hpp"
@ -30,7 +31,8 @@ struct ParsingContext {
class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
public:
explicit CypherMainVisitor(ParsingContext context, AstStorage *storage) : context_(context), storage_(storage) {}
explicit CypherMainVisitor(ParsingContext context, AstStorage *storage, Parameters *parameters)
: context_(context), storage_(storage), parameters_(parameters) {}
private:
Expression *CreateBinaryOperatorByToken(size_t token, Expression *e1, Expression *e2) {
@ -686,6 +688,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
*/
antlrcpp::Any visitPatternElement(MemgraphCypher::PatternElementContext *ctx) override;
/**
* @return Pattern*
*/
antlrcpp::Any visitRelationshipsPattern(MemgraphCypher::RelationshipsPatternContext *ctx) override;
/**
* @return vector<pair<EdgeAtom*, NodeAtom*>>
*/
@ -851,6 +858,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
*/
antlrcpp::Any visitExistsExpression(MemgraphCypher::ExistsExpressionContext *ctx) override;
/**
* @return pattern comprehension (Expression)
*/
antlrcpp::Any visitPatternComprehension(MemgraphCypher::PatternComprehensionContext *ctx) override;
/**
* @return Expression*
*/
@ -1032,6 +1044,8 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
// return.
bool in_with_ = false;
Parameters *parameters_;
QueryInfo query_info_;
};
} // namespace memgraph::query::frontend

View File

@ -73,6 +73,7 @@ class ExpressionPrettyPrinter : public ExpressionVisitor<void> {
void Visit(ParameterLookup &op) override;
void Visit(NamedExpression &op) override;
void Visit(RegexMatch &op) override;
void Visit(PatternComprehension &op) override;
private:
std::ostream *out_;
@ -323,6 +324,10 @@ void ExpressionPrettyPrinter::Visit(NamedExpression &op) {
void ExpressionPrettyPrinter::Visit(RegexMatch &op) { PrintOperator(out_, "=~", op.string_expr_, op.regex_); }
void ExpressionPrettyPrinter::Visit(PatternComprehension &op) {
PrintOperator(out_, "Pattern Comprehension", op.variable_, op.pattern_, op.filter_, op.resultExpr_);
}
} // namespace
void PrintExpression(Expression *expr, std::ostream *out) {

View File

@ -193,7 +193,7 @@ nodeLabels : nodeLabel ( nodeLabel )* ;
nodeLabel : ':' labelName ;
labelName : symbolicName ;
labelName : symbolicName | parameter;
relTypeName : symbolicName ;
@ -296,7 +296,7 @@ functionName : symbolicName ( '.' symbolicName )* ;
listComprehension : '[' filterExpression ( '|' expression )? ']' ;
patternComprehension : '[' ( variable '=' )? relationshipsPattern ( WHERE expression )? '|' expression ']' ;
patternComprehension : '[' ( variable '=' )? relationshipsPattern ( where )? '|' resultExpr=expression ']' ;
propertyLookup : '.' ( propertyKeyName ) ;

View File

@ -249,6 +249,7 @@ class PropertyLookupEvaluationModeVisitor : public ExpressionVisitor<void> {
void Visit(ParameterLookup &op) override{};
void Visit(NamedExpression &op) override { op.expression_->Accept(*this); };
void Visit(RegexMatch &op) override{};
void Visit(PatternComprehension &op) override{};
void Visit(PropertyLookup & /*property_lookup*/) override;

View File

@ -52,6 +52,9 @@ class SymbolTable final {
const Symbol &at(const NamedExpression &nexpr) const { return table_.at(nexpr.symbol_pos_); }
const Symbol &at(const Aggregation &aggr) const { return table_.at(aggr.symbol_pos_); }
const Symbol &at(const Exists &exists) const { return table_.at(exists.symbol_pos_); }
const Symbol &at(const PatternComprehension &pattern_comprehension) const {
return table_.at(pattern_comprehension.symbol_pos_);
}
// TODO: Remove these since members are public
int32_t max_position() const { return static_cast<int32_t>(table_.size()); }

View File

@ -957,7 +957,7 @@ TypedValue ToString(const TypedValue *args, int64_t nargs, const FunctionContext
return TypedValue(std::to_string(arg.ValueInt()), ctx.memory);
}
if (arg.IsDouble()) {
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
return TypedValue(memgraph::utils::DoubleToString(arg.ValueDouble()), ctx.memory);
}
if (arg.IsDate()) {
return TypedValue(arg.ValueDate().ToString(), ctx.memory);

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -101,6 +101,7 @@ class ReferenceExpressionEvaluator : public ExpressionVisitor<TypedValue *> {
UNSUCCESSFUL_VISIT(ParameterLookup);
UNSUCCESSFUL_VISIT(RegexMatch);
UNSUCCESSFUL_VISIT(Exists);
UNSUCCESSFUL_VISIT(PatternComprehension);
#undef UNSUCCESSFUL_VISIT
@ -170,6 +171,7 @@ class PrimitiveLiteralExpressionEvaluator : public ExpressionVisitor<TypedValue>
INVALID_VISIT(Identifier)
INVALID_VISIT(RegexMatch)
INVALID_VISIT(Exists)
INVALID_VISIT(PatternComprehension)
#undef INVALID_VISIT
private:
@ -1090,6 +1092,10 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
}
}
TypedValue Visit(PatternComprehension & /*pattern_comprehension*/) override {
throw utils::NotYetImplemented("Expression evaluator can not handle pattern comprehension.");
}
private:
template <class TRecordAccessor>
std::map<storage::PropertyId, storage::PropertyValue> GetAllProperties(const TRecordAccessor &record_accessor) {

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -14,6 +14,7 @@
#include "query/frontend/ast/ast.hpp"
#include "query/parameters.hpp"
#include "query/plan/operator.hpp"
#include "query/plan/rewrite/index_lookup.hpp"
#include "query/typed_value.hpp"
#include "utils/algorithm.hpp"
#include "utils/math.hpp"
@ -46,6 +47,11 @@ struct CostEstimation {
double cardinality;
};
struct PlanCost {
double cost;
bool use_index_hints;
};
/**
* Query plan execution time cost estimator, for comparing and choosing optimal
* execution plans.
@ -109,11 +115,13 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
using HierarchicalLogicalOperatorVisitor::PostVisit;
using HierarchicalLogicalOperatorVisitor::PreVisit;
CostEstimator(TDbAccessor *db_accessor, const SymbolTable &table, const Parameters &parameters)
: db_accessor_(db_accessor), table_(table), parameters(parameters), scopes_{Scope()} {}
CostEstimator(TDbAccessor *db_accessor, const SymbolTable &table, const Parameters &parameters,
const IndexHints &index_hints)
: db_accessor_(db_accessor), table_(table), parameters(parameters), scopes_{Scope()}, index_hints_(index_hints) {}
CostEstimator(TDbAccessor *db_accessor, const SymbolTable &table, const Parameters &parameters, Scope scope)
: db_accessor_(db_accessor), table_(table), parameters(parameters), scopes_{scope} {}
CostEstimator(TDbAccessor *db_accessor, const SymbolTable &table, const Parameters &parameters, Scope scope,
const IndexHints &index_hints)
: db_accessor_(db_accessor), table_(table), parameters(parameters), scopes_{scope}, index_hints_(index_hints) {}
bool PostVisit(ScanAll &) override {
cardinality_ *= db_accessor_->VerticesCount();
@ -129,7 +137,10 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
}
cardinality_ *= db_accessor_->VerticesCount(scan_all_by_label.label_);
// ScanAll performs some work for every element that is produced
if (index_hints_.HasLabelIndex(db_accessor_, scan_all_by_label.label_)) {
use_index_hints_ = true;
}
IncrementCost(CostParam::kScanAllByLabel);
return true;
}
@ -154,6 +165,10 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
cardinality_ *= factor;
if (index_hints_.HasLabelPropertyIndex(db_accessor_, logical_op.label_, logical_op.property_)) {
use_index_hints_ = true;
}
// ScanAll performs some work for every element that is produced
IncrementCost(CostParam::MakeScanAllByLabelPropertyValue);
return true;
@ -184,6 +199,10 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
cardinality_ *= factor;
if (index_hints_.HasLabelPropertyIndex(db_accessor_, logical_op.label_, logical_op.property_)) {
use_index_hints_ = true;
}
// ScanAll performs some work for every element that is produced
IncrementCost(CostParam::MakeScanAllByLabelPropertyRange);
return true;
@ -197,6 +216,10 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
const auto factor = db_accessor_->VerticesCount(logical_op.label_, logical_op.property_);
cardinality_ *= factor;
if (index_hints_.HasLabelPropertyIndex(db_accessor_, logical_op.label_, logical_op.property_)) {
use_index_hints_ = true;
}
IncrementCost(CostParam::MakeScanAllByLabelProperty);
return true;
}
@ -375,6 +398,7 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
auto cost() const { return cost_; }
auto cardinality() const { return cardinality_; }
auto use_index_hints() const { return use_index_hints_; }
private:
// cost estimation that gets accumulated as the visitor
@ -390,17 +414,19 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
const SymbolTable &table_;
const Parameters &parameters;
std::vector<Scope> scopes_;
IndexHints index_hints_;
bool use_index_hints_{false};
void IncrementCost(double param) { cost_ += param * cardinality_; }
CostEstimation EstimateCostOnBranch(std::shared_ptr<LogicalOperator> *branch) {
CostEstimator<TDbAccessor> cost_estimator(db_accessor_, table_, parameters);
CostEstimator<TDbAccessor> cost_estimator(db_accessor_, table_, parameters, index_hints_);
(*branch)->Accept(cost_estimator);
return CostEstimation{.cost = cost_estimator.cost(), .cardinality = cost_estimator.cardinality()};
}
CostEstimation EstimateCostOnBranch(std::shared_ptr<LogicalOperator> *branch, Scope scope) {
CostEstimator<TDbAccessor> cost_estimator(db_accessor_, table_, parameters, scope);
CostEstimator<TDbAccessor> cost_estimator(db_accessor_, table_, parameters, scope, index_hints_);
(*branch)->Accept(cost_estimator);
return CostEstimation{.cost = cost_estimator.cost(), .cardinality = cost_estimator.cardinality()};
}
@ -450,11 +476,11 @@ class CostEstimator : public HierarchicalLogicalOperatorVisitor {
/** Returns the estimated cost of the given plan. */
template <class TDbAccessor>
double EstimatePlanCost(TDbAccessor *db, const SymbolTable &table, const Parameters &parameters,
LogicalOperator &plan) {
CostEstimator<TDbAccessor> estimator(db, table, parameters);
PlanCost EstimatePlanCost(TDbAccessor *db, const SymbolTable &table, const Parameters &parameters,
LogicalOperator &plan, const IndexHints &index_hints) {
CostEstimator<TDbAccessor> estimator(db, table, parameters, index_hints);
plan.Accept(estimator);
return estimator.cost();
return PlanCost{.cost = estimator.cost(), .use_index_hints = estimator.use_index_hints()};
}
} // namespace memgraph::query::plan

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -59,9 +59,9 @@ class PostProcessor final {
}
template <class TVertexCounts>
double EstimatePlanCost(const std::unique_ptr<LogicalOperator> &plan, TVertexCounts *vertex_counts,
const SymbolTable &table) {
return query::plan::EstimatePlanCost(vertex_counts, table, parameters_, *plan);
PlanCost EstimatePlanCost(const std::unique_ptr<LogicalOperator> &plan, TVertexCounts *vertex_counts,
const SymbolTable &table) {
return query::plan::EstimatePlanCost(vertex_counts, table, parameters_, *plan, index_hints_);
}
};
@ -99,6 +99,7 @@ auto MakeLogicalPlan(TPlanningContext *context, TPlanPostProcess *post_process,
auto query_parts = CollectQueryParts(*context->symbol_table, *context->ast_storage, context->query);
auto &vertex_counts = *context->db;
double total_cost = std::numeric_limits<double>::max();
bool curr_uses_index_hint = false;
using ProcessedPlan = typename TPlanPostProcess::ProcessedPlan;
ProcessedPlan plan_with_least_cost;
@ -110,16 +111,28 @@ auto MakeLogicalPlan(TPlanningContext *context, TPlanPostProcess *post_process,
// Plans are generated lazily and the current plan will disappear, so
// it's ok to move it.
auto rewritten_plan = post_process->Rewrite(std::move(plan), context);
double cost = post_process->EstimatePlanCost(rewritten_plan, &vertex_counts, *context->symbol_table);
if (!curr_plan || cost < total_cost) {
auto plan_cost = post_process->EstimatePlanCost(rewritten_plan, &vertex_counts, *context->symbol_table);
// if we have a plan that uses index hints, we reject all the plans that don't use index hinting because we want
// to force the plan using the index hints to be executed
if (curr_uses_index_hint && !plan_cost.use_index_hints) continue;
// if a plan uses index hints, and there is currently not yet a plan that utilizes it, we will take it regardless
if (plan_cost.use_index_hints && !curr_uses_index_hint) {
curr_uses_index_hint = plan_cost.use_index_hints;
curr_plan.emplace(std::move(rewritten_plan));
total_cost = cost;
total_cost = plan_cost.cost;
continue;
}
// if both plans either use or don't use index hints, we want to use the one with the least cost
if (!curr_plan || plan_cost.cost < total_cost) {
curr_uses_index_hint = plan_cost.use_index_hints;
curr_plan.emplace(std::move(rewritten_plan));
total_cost = plan_cost.cost;
}
}
} else {
auto plan = MakeLogicalPlanForSingleQuery<RuleBasedPlanner>(query_parts, context);
auto rewritten_plan = post_process->Rewrite(std::move(plan), context);
total_cost = post_process->EstimatePlanCost(rewritten_plan, &vertex_counts, *context->symbol_table);
total_cost = post_process->EstimatePlanCost(rewritten_plan, &vertex_counts, *context->symbol_table).cost;
curr_plan.emplace(std::move(rewritten_plan));
}

View File

@ -230,6 +230,7 @@ class PatternFilterVisitor : public ExpressionVisitor<void> {
void Visit(ParameterLookup &op) override{};
void Visit(NamedExpression &op) override{};
void Visit(RegexMatch &op) override{};
void Visit(PatternComprehension &op) override{};
std::vector<FilterMatching> getMatchings() { return matchings_; }

View File

@ -28,6 +28,7 @@
#include "query/plan/operator.hpp"
#include "query/plan/preprocess.hpp"
#include "storage/v2/id_types.hpp"
DECLARE_int64(query_vertex_count_to_expand_existing);
@ -59,6 +60,29 @@ struct IndexHints {
}
}
template <class TDbAccessor>
bool HasLabelIndex(TDbAccessor *db, storage::LabelId label) const {
for (const auto &[index_type, label_hint, _] : label_index_hints_) {
auto label_id = db->NameToLabel(label_hint.name);
if (label_id == label) {
return true;
}
}
return false;
}
template <class TDbAccessor>
bool HasLabelPropertyIndex(TDbAccessor *db, storage::LabelId label, storage::PropertyId property) const {
for (const auto &[index_type, label_hint, property_hint] : label_property_index_hints_) {
auto label_id = db->NameToLabel(label_hint.name);
auto property_id = db->NameToProperty(property_hint->name);
if (label_id == label && property_id == property) {
return true;
}
}
return false;
}
std::vector<IndexHint> label_index_hints_{};
std::vector<IndexHint> label_property_index_hints_{};
};

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -373,12 +373,12 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
return true;
}
bool Visit(ParameterLookup &) override {
bool Visit(ParameterLookup & /*unused*/) override {
has_aggregation_.emplace_back(false);
return true;
}
bool PostVisit(RegexMatch &regex_match) override {
bool PostVisit(RegexMatch & /*unused*/) override {
MG_ASSERT(has_aggregation_.size() >= 2U, "Expected 2 has_aggregation_ flags for RegexMatch arguments");
bool has_aggr = has_aggregation_.back();
has_aggregation_.pop_back();
@ -386,6 +386,10 @@ class ReturnBodyContext : public HierarchicalTreeVisitor {
return true;
}
bool PostVisit(PatternComprehension & /*unused*/) override {
throw utils::NotYetImplemented("Planner can not handle pattern comprehension.");
}
// Creates NamedExpression with an Identifier for each user declared symbol.
// This should be used when body.all_identifiers is true, to generate
// expressions for Produce operator.

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -380,6 +380,7 @@ class RuleBasedPlanner {
if (pattern.identifier_->user_declared_) {
std::vector<Symbol> path_elements;
for (const PatternAtom *atom : pattern.atoms_) path_elements.emplace_back(symbol_table.at(*atom->identifier_));
bound_symbols.insert(symbol_table.at(*pattern.identifier_));
last_op = std::make_unique<ConstructNamedPath>(std::move(last_op), symbol_table.at(*pattern.identifier_),
path_elements);
}

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -17,6 +17,7 @@
#include <charconv>
#include <cstdint>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <random>
@ -459,4 +460,30 @@ inline std::string_view Substr(const std::string_view string, size_t pos = 0, si
return string.substr(pos, len);
}
/**
* Convert a double value to a string representation.
* Precision of converted value is 16.
* Function also removes trailing zeros after the dot.
*
* @param value The double value to be converted.
*
* @return The string representation of the double value.
*
* @throws None
*/
inline std::string DoubleToString(const double value) {
static const int PRECISION = 15;
std::stringstream ss;
ss << std::setprecision(PRECISION) << std::fixed << value;
auto sv = ss.view();
// Because of setprecision and fixed manipulator we are guaranteed to have the dot
sv = sv.substr(0, sv.find_last_not_of('0') + 1);
if (sv.ends_with('.')) {
sv = sv.substr(0, sv.size() - 1);
}
return std::string(sv);
}
} // namespace memgraph::utils

View File

@ -190,6 +190,7 @@ enum class TypeId : uint64_t {
AST_MULTI_DATABASE_QUERY,
AST_SHOW_DATABASES,
AST_EDGE_IMPORT_MODE_QUERY,
AST_PATTERN_COMPREHENSION,
// Symbol
SYMBOL,
};

View File

@ -23,6 +23,7 @@
// variable of the same name, EOF.
// This hides the definition of the macro which causes
// the compilation to fail.
#include "query/parameters.hpp"
#include "query/plan/planner.hpp"
//////////////////////////////////////////////////////
#include "communication/result_stream_faker.hpp"
@ -119,10 +120,11 @@ static void AddTree(memgraph::storage::Storage *db, int vertex_count) {
static memgraph::query::CypherQuery *ParseCypherQuery(const std::string &query_string,
memgraph::query::AstStorage *ast) {
memgraph::query::frontend::ParsingContext parsing_context;
memgraph::query::Parameters parameters;
parsing_context.is_query_cached = false;
memgraph::query::frontend::opencypher::Parser parser(query_string);
// Convert antlr4 AST into Memgraph AST.
memgraph::query::frontend::CypherMainVisitor cypher_visitor(parsing_context, ast);
memgraph::query::frontend::CypherMainVisitor cypher_visitor(parsing_context, ast, &parameters);
cypher_visitor.visit(parser.tree());
return memgraph::utils::Downcast<memgraph::query::CypherQuery>(cypher_visitor.query());
};

View File

@ -16,6 +16,7 @@
#include "query/frontend/semantic/symbol_generator.hpp"
#include "query/plan/cost_estimator.hpp"
#include "query/plan/planner.hpp"
#include "query/plan/rewrite/index_lookup.hpp"
#include "query/plan/vertex_count_cache.hpp"
#include "storage/v2/inmemory/storage.hpp"
@ -136,7 +137,8 @@ static void BM_PlanAndEstimateIndexedMatching(benchmark::State &state) {
auto plans = memgraph::query::plan::MakeLogicalPlanForSingleQuery<memgraph::query::plan::VariableStartPlanner>(
query_parts, &ctx);
for (auto plan : plans) {
memgraph::query::plan::EstimatePlanCost(&dba, symbol_table, parameters, *plan);
memgraph::query::plan::EstimatePlanCost(&dba, symbol_table, parameters, *plan,
memgraph::query::plan::IndexHints());
}
}
}
@ -166,7 +168,8 @@ static void BM_PlanAndEstimateIndexedMatchingWithCachedCounts(benchmark::State &
auto plans = memgraph::query::plan::MakeLogicalPlanForSingleQuery<memgraph::query::plan::VariableStartPlanner>(
query_parts, &ctx);
for (auto plan : plans) {
memgraph::query::plan::EstimatePlanCost(&vertex_counts, symbol_table, parameters, *plan);
memgraph::query::plan::EstimatePlanCost(&vertex_counts, symbol_table, parameters, *plan,
memgraph::query::plan::IndexHints());
}
}
}

View File

@ -478,5 +478,44 @@ def test_nonexistent_label_property_index(memgraph):
assert False
def test_index_hint_on_expand(memgraph):
# Prefer expanding from the node with the given hint even if estimator estimates higher cost for that plan
memgraph.execute("FOREACH (i IN range(1, 1000) | CREATE (n:Label1 {id: i}));")
memgraph.execute("FOREACH (i IN range(1, 10) | CREATE (n:Label2 {id: i}));")
memgraph.execute("CREATE INDEX ON :Label1;")
memgraph.execute("CREATE INDEX ON :Label2;")
expected_explain_without_hint = [
" * Produce {n, m}",
" * Filter (n :Label1)",
" * Expand (m)<-[anon1:rel]-(n)",
" * ScanAllByLabel (m :Label2)",
" * Once",
]
expected_explain_with_hint = [
" * Produce {n, m}",
" * Filter (m :Label2)",
" * Expand (n)-[anon1:rel]->(m)",
" * ScanAllByLabel (n :Label1)",
" * Once",
]
explain_without_hint = [
row["QUERY PLAN"]
for row in memgraph.execute_and_fetch("EXPLAIN MATCH (n:Label1)-[:rel]->(m:Label2) RETURN n, m;")
]
explain_with_hint = [
row["QUERY PLAN"]
for row in memgraph.execute_and_fetch(
"EXPLAIN USING INDEX :Label1 MATCH (n:Label1)-[:rel]->(m:Label2) RETURN n, m;"
)
]
assert explain_without_hint == expected_explain_without_hint and explain_with_hint == expected_explain_with_hint
if __name__ == "__main__":
sys.exit(pytest.main([__file__, "-rA"]))

View File

@ -279,3 +279,15 @@ Feature: List operators
Then the result should be:
| o |
| (:Node {Status: 'This is the status'}) |
Scenario: Simple list pattern comprehension
Given graph "graph_keanu"
When executing query:
"""
MATCH (keanu:Person {name: 'Keanu Reeves'})
RETURN [(keanu)-->(b:Movie) WHERE b.title CONTAINS 'Matrix' | b.released] AS years
"""
Then an error should be raised
# Then the result should be:
# | years |
# | [2021,2003,2003,1999] |

View File

@ -771,3 +771,17 @@ Feature: Match
Then the result should be:
| path |
| <(:label1 {id: 1})-[:type1 {id: 1}]->(:label2 {id: 2})-[:type1 {id: 2}]->(:label3 {id: 3})> |
Scenario: Using path indentifier from CREATE in MERGE
Given an empty graph
And having executed:
"""
CREATE p0=()-[:T0]->() MERGE ({k:(size(p0))});
"""
When executing query:
"""
MATCH (n {k: 1}) RETURN n;
"""
Then the result should be:
| n |
| ({k: 1}) |

View File

@ -52,7 +52,7 @@ Feature: Parameters
| [1, 2, 3] |
Scenario: Parameters in match:
Given an empty graph
Given an empty graph
And having executed:
"""
CREATE (a {x : 10})
@ -66,3 +66,90 @@ Feature: Parameters
Then the result should be:
| a.x |
| 10 |
Scenario: Label parameters in match:
Given an empty graph
And having executed:
"""
CREATE (a:Label1 {x : 10})
"""
And parameters are:
| a | 10 |
| label | Label1 |
When executing query:
"""
MATCH (a:$label {x : $a}) RETURN a
"""
Then the result should be:
| a |
| (:Label1{x: 10}) |
Scenario: Label parameters in create and match
Given an empty graph
And parameters are:
| a | 10 |
| label | Label1 |
When executing query:
"""
CREATE (a:$label {x: $a})
"""
When executing query:
"""
MATCH (a:$label {x: $a}) RETURN a
"""
Then the result should be:
| a |
| (:Label1{x: 10}) |
Scenario: Label parameters in merge
Given an empty graph
And parameters are:
| a | 10 |
| label | Label1 |
When executing query:
"""
MERGE (a:$label {x: $a}) RETURN a
"""
Then the result should be:
| a |
| (:Label1{x: 10}) |
Scenario: Label parameters in set label
Given an empty graph
And having executed:
"""
CREATE (a:Label1 {x : 10})
"""
And parameters are:
| new_label | Label2 |
When executing query:
"""
MATCH (a:Label1 {x: 10}) SET a:$new_label
"""
When executing query:
"""
MATCH (a:Label1:Label1 {x: 10}) RETURN a
"""
Then the result should be:
| a |
| (:Label1:Label2 {x: 10}) |
Scenario: Label parameters in remove label
Given an empty graph
And having executed:
"""
CREATE (a:Label1:LabelToRemove {x : 10})
"""
And parameters are:
| label_to_remove | LabelToRemove |
When executing query:
"""
MATCH (a {x: 10}) REMOVE a:$label_to_remove
"""
When executing query:
"""
MATCH (a {x: 10}) RETURN a
"""
Then the result should be:
| a |
| (:Label1 {x: 10}) |

View File

@ -0,0 +1,16 @@
CREATE
(keanu:Person {name: 'Keanu Reeves'}),
(johnnyMnemonic:Movie {title: 'Johnny Mnemonic', released: 1995}),
(theMatrixRevolutions:Movie {title: 'The Matrix Revolutions', released: 2003}),
(theMatrixReloaded:Movie {title: 'The Matrix Reloaded', released: 2003}),
(theReplacements:Movie {title: 'The Replacements', released: 2000}),
(theMatrix:Movie {title: 'The Matrix', released: 1999}),
(theDevilsAdvocate:Movie {title: 'The Devils Advocate', released: 1997}),
(theMatrixResurrections:Movie {title: 'The Matrix Resurrections', released: 2021}),
(keanu)-[:ACTED_IN]->(johnnyMnemonic),
(keanu)-[:ACTED_IN]->(theMatrixRevolutions),
(keanu)-[:ACTED_IN]->(theMatrixReloaded),
(keanu)-[:ACTED_IN]->(theReplacements),
(keanu)-[:ACTED_IN]->(theMatrix),
(keanu)-[:ACTED_IN]->(theDevilsAdvocate),
(keanu)-[:ACTED_IN]->(theMatrixResurrections);

View File

@ -1,4 +1,4 @@
// Copyright 2022 Memgraph Ltd.
// 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
@ -17,6 +17,7 @@
#include "query/frontend/ast/cypher_main_visitor.hpp"
#include "query/frontend/ast/pretty_print.hpp"
#include "query/frontend/opencypher/parser.hpp"
#include "query/parameters.hpp"
std::string AssembleQueryString(const std::string &expression_string) {
return "return " + expression_string + " as expr";
@ -24,8 +25,9 @@ std::string AssembleQueryString(const std::string &expression_string) {
memgraph::query::Query *ParseQuery(const std::string &query_string, memgraph::query::AstStorage *ast_storage) {
memgraph::query::frontend::ParsingContext context;
memgraph::query::Parameters parameters;
memgraph::query::frontend::opencypher::Parser parser(query_string);
memgraph::query::frontend::CypherMainVisitor visitor(context, ast_storage);
memgraph::query::frontend::CypherMainVisitor visitor(context, ast_storage, &parameters);
visitor.visit(parser.tree());
return visitor.query();

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -434,11 +434,12 @@ void ExaminePlans(memgraph::query::DbAccessor *dba, const memgraph::query::Symbo
memgraph::query::Query *MakeAst(const std::string &query, memgraph::query::AstStorage *storage) {
memgraph::query::frontend::ParsingContext parsing_context;
memgraph::query::Parameters parameters;
parsing_context.is_query_cached = false;
// query -> AST
auto parser = std::make_unique<memgraph::query::frontend::opencypher::Parser>(query);
// AST -> high level tree
memgraph::query::frontend::CypherMainVisitor visitor(parsing_context, storage);
memgraph::query::frontend::CypherMainVisitor visitor(parsing_context, storage, &parameters);
visitor.visit(parser->tree());
return visitor.query();
}
@ -462,7 +463,7 @@ auto MakeLogicalPlans(memgraph::query::CypherQuery *query, memgraph::query::AstS
memgraph::query::AstStorage ast_copy;
auto unoptimized_plan = plan->Clone(&ast_copy);
auto rewritten_plan = post_process.Rewrite(std::move(plan), &ctx);
double cost = post_process.EstimatePlanCost(rewritten_plan, dba, symbol_table);
double cost = post_process.EstimatePlanCost(rewritten_plan, dba, symbol_table).cost;
interactive_plans.push_back(
InteractivePlan{std::move(unoptimized_plan), std::move(ast_copy), std::move(rewritten_plan), cost});
}

View File

@ -37,6 +37,7 @@
#include "query/frontend/ast/cypher_main_visitor.hpp"
#include "query/frontend/opencypher/parser.hpp"
#include "query/frontend/stripped.hpp"
#include "query/parameters.hpp"
#include "query/procedure/cypher_types.hpp"
#include "query/procedure/mg_procedure_impl.hpp"
#include "query/procedure/module.hpp"
@ -118,7 +119,8 @@ class AstGenerator : public Base {
public:
Query *ParseQuery(const std::string &query_string) override {
::frontend::opencypher::Parser parser(query_string);
CypherMainVisitor visitor(context_, &ast_storage_);
Parameters parameters;
CypherMainVisitor visitor(context_, &ast_storage_, &parameters);
visitor.visit(parser.tree());
return visitor.query();
}
@ -151,6 +153,7 @@ class ClonedAstGenerator : public Base {
public:
Query *ParseQuery(const std::string &query_string) override {
::frontend::opencypher::Parser parser(query_string);
Parameters parameters;
AstStorage tmp_storage;
{
// Add a label, property and edge type into temporary storage so
@ -159,7 +162,7 @@ class ClonedAstGenerator : public Base {
tmp_storage.GetPropertyIx("fdjakfjdklfjdaslk");
tmp_storage.GetEdgeTypeIx("fdjkalfjdlkajfdkla");
}
CypherMainVisitor visitor(context_, &tmp_storage);
CypherMainVisitor visitor(context_, &tmp_storage, &parameters);
visitor.visit(parser.tree());
return visitor.query()->Clone(&ast_storage_);
}
@ -182,8 +185,9 @@ class CachedAstGenerator : public Base {
StrippedQuery stripped(query_string);
parameters_ = stripped.literals();
::frontend::opencypher::Parser parser(stripped.query());
Parameters parameters;
AstStorage tmp_storage;
CypherMainVisitor visitor(context_, &tmp_storage);
CypherMainVisitor visitor(context_, &tmp_storage, &parameters);
visitor.visit(parser.tree());
return visitor.query()->Clone(&ast_storage_);
}

View File

@ -17,6 +17,7 @@
#include "query/frontend/semantic/symbol_table.hpp"
#include "query/plan/cost_estimator.hpp"
#include "query/plan/operator.hpp"
#include "query/plan/rewrite/index_lookup.hpp"
#include "storage/v2/inmemory/storage.hpp"
#include "storage/v2/storage.hpp"
@ -83,7 +84,8 @@ class QueryCostEstimator : public ::testing::Test {
}
auto Cost() {
CostEstimator<memgraph::query::DbAccessor> cost_estimator(&*dba, symbol_table_, parameters_);
CostEstimator<memgraph::query::DbAccessor> cost_estimator(&*dba, symbol_table_, parameters_,
memgraph::query::plan::IndexHints());
last_op_->Accept(cost_estimator);
return cost_estimator.cost();
}

View File

@ -2075,9 +2075,10 @@ TYPED_TEST(FunctionTest, ToStringInteger) {
}
TYPED_TEST(FunctionTest, ToStringDouble) {
EXPECT_EQ(this->EvaluateFunction("TOSTRING", -42.42).ValueString(), "-42.420000");
EXPECT_EQ(this->EvaluateFunction("TOSTRING", 0.0).ValueString(), "0.000000");
EXPECT_EQ(this->EvaluateFunction("TOSTRING", 238910.2313217).ValueString(), "238910.231322");
EXPECT_EQ(this->EvaluateFunction("TOSTRING", -42.42).ValueString(), "-42.420000000000002");
EXPECT_EQ(this->EvaluateFunction("TOSTRING", 0.0).ValueString(), "0");
EXPECT_EQ(this->EvaluateFunction("TOSTRING", 238910.2313217).ValueString(), "238910.231321700004628");
EXPECT_EQ(this->EvaluateFunction("TOSTRING", 238910.23132171234).ValueString(), "238910.231321712344652");
}
TYPED_TEST(FunctionTest, ToStringBool) {

View File

@ -1,4 +1,4 @@
// Copyright 2023 Memgraph Ltd.
// Copyright 2024 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
@ -171,3 +171,22 @@ TEST(String, Substr) {
EXPECT_EQ(Substr(string, string.size() - 1, 1), string.substr(string.size() - 1, 1));
EXPECT_EQ(Substr(string, string.size() - 1, 2), string.substr(string.size() - 1, 2));
}
TEST(String, DoubleToString) {
EXPECT_EQ(DoubleToString(0), "0");
EXPECT_EQ(DoubleToString(1), "1");
EXPECT_EQ(DoubleToString(1234567890123456), "1234567890123456");
EXPECT_EQ(DoubleToString(static_cast<double>(12345678901234567)), "12345678901234568");
EXPECT_EQ(DoubleToString(0.5), "0.5");
EXPECT_EQ(DoubleToString(1.0), "1");
EXPECT_EQ(DoubleToString(5.8), "5.8");
EXPECT_EQ(DoubleToString(1.01234000), "1.01234");
EXPECT_EQ(DoubleToString(1.036837585345), "1.036837585345");
EXPECT_EQ(DoubleToString(103.6837585345), "103.683758534500001");
EXPECT_EQ(DoubleToString(1.01234567890123456789), "1.012345678901235");
EXPECT_EQ(DoubleToString(1234567.01234567891234567), "1234567.012345678871498");
EXPECT_EQ(DoubleToString(0.00001), "0.00001");
EXPECT_EQ(DoubleToString(0.00000000000001), "0.00000000000001");
EXPECT_EQ(DoubleToString(0.000000000000001), "0.000000000000001");
EXPECT_EQ(DoubleToString(0.0000000000000001), "0");
}