diff --git a/ADRs/001_tantivy.md b/ADRs/001_tantivy.md
new file mode 100644
index 000000000..ac887c861
--- /dev/null
+++ b/ADRs/001_tantivy.md
@@ -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.**
diff --git a/src/memory/query_memory_control.cpp b/src/memory/query_memory_control.cpp
index 91730c900..5e569bd13 100644
--- a/src/memory/query_memory_control.cpp
+++ b/src/memory/query_memory_control.cpp
@@ -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) {
diff --git a/src/memory/query_memory_control.hpp b/src/memory/query_memory_control.hpp
index 901917757..3852027a5 100644
--- a/src/memory/query_memory_control.hpp
+++ b/src/memory/query_memory_control.hpp
@@ -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;
diff --git a/src/query/cypher_query_interpreter.cpp b/src/query/cypher_query_interpreter.cpp
index d3ddc22c4..30966119b 100644
--- a/src/query/cypher_query_interpreter.cpp
+++ b/src/query/cypher_query_interpreter.cpp
@@ -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());
 
diff --git a/src/query/frontend/ast/ast.cpp b/src/query/frontend/ast/ast.cpp
index c5e4c84c4..6a9f05bad 100644
--- a/src/query/frontend/ast/ast.cpp
+++ b/src/query/frontend/ast/ast.cpp
@@ -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
diff --git a/src/query/frontend/ast/ast.hpp b/src/query/frontend/ast/ast.hpp
index b42f13c5c..d8ff325cb 100644
--- a/src/query/frontend/ast/ast.hpp
+++ b/src/query/frontend/ast/ast.hpp
@@ -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;
diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp
index 793c15a95..ff1586fe4 100644
--- a/src/query/frontend/ast/ast_visitor.hpp
+++ b/src/query/frontend/ast/ast_visitor.hpp
@@ -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
diff --git a/src/query/frontend/ast/cypher_main_visitor.cpp b/src/query/frontend/ast/cypher_main_visitor.cpp
index 57c8c9d4e..fe202d430 100644
--- a/src/query/frontend/ast/cypher_main_visitor.cpp
+++ b/src/query/frontend/ast/cypher_main_visitor.cpp
@@ -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));
 }
diff --git a/src/query/frontend/ast/cypher_main_visitor.hpp b/src/query/frontend/ast/cypher_main_visitor.hpp
index 9591cc89e..030689392 100644
--- a/src/query/frontend/ast/cypher_main_visitor.hpp
+++ b/src/query/frontend/ast/cypher_main_visitor.hpp
@@ -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
diff --git a/src/query/frontend/ast/pretty_print.cpp b/src/query/frontend/ast/pretty_print.cpp
index ef45afd7d..61bd23797 100644
--- a/src/query/frontend/ast/pretty_print.cpp
+++ b/src/query/frontend/ast/pretty_print.cpp
@@ -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) {
diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4
index b39a49e69..4fef32cb0 100644
--- a/src/query/frontend/opencypher/grammar/Cypher.g4
+++ b/src/query/frontend/opencypher/grammar/Cypher.g4
@@ -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 ) ;
 
diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp
index 207bbddbd..f9e6468f6 100644
--- a/src/query/frontend/semantic/symbol_generator.hpp
+++ b/src/query/frontend/semantic/symbol_generator.hpp
@@ -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;
 
diff --git a/src/query/frontend/semantic/symbol_table.hpp b/src/query/frontend/semantic/symbol_table.hpp
index 0b521356c..cf462c437 100644
--- a/src/query/frontend/semantic/symbol_table.hpp
+++ b/src/query/frontend/semantic/symbol_table.hpp
@@ -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()); }
diff --git a/src/query/interpret/awesome_memgraph_functions.cpp b/src/query/interpret/awesome_memgraph_functions.cpp
index 7e1a7290f..ece0aec78 100644
--- a/src/query/interpret/awesome_memgraph_functions.cpp
+++ b/src/query/interpret/awesome_memgraph_functions.cpp
@@ -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);
diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp
index f4f3126cd..017dc9101 100644
--- a/src/query/interpret/eval.hpp
+++ b/src/query/interpret/eval.hpp
@@ -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) {
diff --git a/src/query/plan/cost_estimator.hpp b/src/query/plan/cost_estimator.hpp
index 47da0a23b..ede4a89fc 100644
--- a/src/query/plan/cost_estimator.hpp
+++ b/src/query/plan/cost_estimator.hpp
@@ -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
diff --git a/src/query/plan/planner.hpp b/src/query/plan/planner.hpp
index 10318e6b9..e8ca80e39 100644
--- a/src/query/plan/planner.hpp
+++ b/src/query/plan/planner.hpp
@@ -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));
   }
 
diff --git a/src/query/plan/preprocess.hpp b/src/query/plan/preprocess.hpp
index 8e1955907..322da545a 100644
--- a/src/query/plan/preprocess.hpp
+++ b/src/query/plan/preprocess.hpp
@@ -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_; }
 
diff --git a/src/query/plan/rewrite/index_lookup.hpp b/src/query/plan/rewrite/index_lookup.hpp
index 590bad5f4..407c32ba0 100644
--- a/src/query/plan/rewrite/index_lookup.hpp
+++ b/src/query/plan/rewrite/index_lookup.hpp
@@ -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_{};
 };
diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp
index f3d0c1487..bf5e66158 100644
--- a/src/query/plan/rule_based_planner.cpp
+++ b/src/query/plan/rule_based_planner.cpp
@@ -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.
diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp
index 074bd1c88..bdd7bdbd9 100644
--- a/src/query/plan/rule_based_planner.hpp
+++ b/src/query/plan/rule_based_planner.hpp
@@ -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);
     }
diff --git a/src/utils/string.hpp b/src/utils/string.hpp
index 833c158c8..8593fc57f 100644
--- a/src/utils/string.hpp
+++ b/src/utils/string.hpp
@@ -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
diff --git a/src/utils/typeinfo.hpp b/src/utils/typeinfo.hpp
index 682b5ac55..944d35fab 100644
--- a/src/utils/typeinfo.hpp
+++ b/src/utils/typeinfo.hpp
@@ -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,
 };
diff --git a/tests/benchmark/query/execution.cpp b/tests/benchmark/query/execution.cpp
index aa72e21d7..750dd5564 100644
--- a/tests/benchmark/query/execution.cpp
+++ b/tests/benchmark/query/execution.cpp
@@ -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());
 };
diff --git a/tests/benchmark/query/planner.cpp b/tests/benchmark/query/planner.cpp
index 52cb44490..b64c4c39f 100644
--- a/tests/benchmark/query/planner.cpp
+++ b/tests/benchmark/query/planner.cpp
@@ -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());
     }
   }
 }
diff --git a/tests/e2e/index_hints/index_hints.py b/tests/e2e/index_hints/index_hints.py
index 70d3ce6b6..b59d60103 100644
--- a/tests/e2e/index_hints/index_hints.py
+++ b/tests/e2e/index_hints/index_hints.py
@@ -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"]))
diff --git a/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature b/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature
index bfe6b6225..8c5538d6b 100644
--- a/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature
+++ b/tests/gql_behave/tests/memgraph_V1/features/list_operations.feature
@@ -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] |
diff --git a/tests/gql_behave/tests/memgraph_V1/features/match.feature b/tests/gql_behave/tests/memgraph_V1/features/match.feature
index 227ad9ad6..0d0477ad9 100644
--- a/tests/gql_behave/tests/memgraph_V1/features/match.feature
+++ b/tests/gql_behave/tests/memgraph_V1/features/match.feature
@@ -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}) |
diff --git a/tests/gql_behave/tests/memgraph_V1/features/parameters.feature b/tests/gql_behave/tests/memgraph_V1/features/parameters.feature
index 908507e43..288f93206 100644
--- a/tests/gql_behave/tests/memgraph_V1/features/parameters.feature
+++ b/tests/gql_behave/tests/memgraph_V1/features/parameters.feature
@@ -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}) |
diff --git a/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher b/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher
new file mode 100644
index 000000000..a7a72aced
--- /dev/null
+++ b/tests/gql_behave/tests/memgraph_V1/graphs/graph_keanu.cypher
@@ -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);
diff --git a/tests/manual/expression_pretty_printer.cpp b/tests/manual/expression_pretty_printer.cpp
index e2d58b350..20757b195 100644
--- a/tests/manual/expression_pretty_printer.cpp
+++ b/tests/manual/expression_pretty_printer.cpp
@@ -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();
diff --git a/tests/manual/interactive_planning.cpp b/tests/manual/interactive_planning.cpp
index d5da0ba2b..f550b9724 100644
--- a/tests/manual/interactive_planning.cpp
+++ b/tests/manual/interactive_planning.cpp
@@ -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});
   }
diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp
index 31fd95c6c..9cb33589c 100644
--- a/tests/unit/cypher_main_visitor.cpp
+++ b/tests/unit/cypher_main_visitor.cpp
@@ -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_);
   }
diff --git a/tests/unit/query_cost_estimator.cpp b/tests/unit/query_cost_estimator.cpp
index ff9525cdc..631d17414 100644
--- a/tests/unit/query_cost_estimator.cpp
+++ b/tests/unit/query_cost_estimator.cpp
@@ -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();
   }
diff --git a/tests/unit/query_expression_evaluator.cpp b/tests/unit/query_expression_evaluator.cpp
index c070aaa32..b2a7c1f7a 100644
--- a/tests/unit/query_expression_evaluator.cpp
+++ b/tests/unit/query_expression_evaluator.cpp
@@ -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) {
diff --git a/tests/unit/utils_string.cpp b/tests/unit/utils_string.cpp
index cefe57a6a..fdf64ae9f 100644
--- a/tests/unit/utils_string.cpp
+++ b/tests/unit/utils_string.cpp
@@ -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");
+}