From b1c3168308fa613824c1f424bd52443fe6dbf471 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ante=20Pu=C5=A1i=C4=87?= <ante.f.pusic@gmail.com>
Date: Sat, 28 Oct 2023 15:34:52 +0200
Subject: [PATCH] Fix PROFILE infinite loop (#1431)

---
 src/query/plan/operator.cpp              | 21 ++++++-----
 tests/e2e/CMakeLists.txt                 |  1 +
 tests/e2e/inspect_query/CMakeLists.txt   |  6 +++
 tests/e2e/inspect_query/common.py        | 45 +++++++++++++++++++++++
 tests/e2e/inspect_query/inspect_query.py | 47 ++++++++++++++++++++++++
 tests/e2e/inspect_query/workloads.yaml   | 13 +++++++
 6 files changed, 123 insertions(+), 10 deletions(-)
 create mode 100644 tests/e2e/inspect_query/CMakeLists.txt
 create mode 100644 tests/e2e/inspect_query/common.py
 create mode 100644 tests/e2e/inspect_query/inspect_query.py
 create mode 100644 tests/e2e/inspect_query/workloads.yaml

diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp
index 3e0310b91..5b8f0981e 100644
--- a/src/query/plan/operator.cpp
+++ b/src/query/plan/operator.cpp
@@ -2456,17 +2456,16 @@ Filter::FilterCursor::FilterCursor(const Filter &self, utils::MemoryResource *me
 bool Filter::FilterCursor::Pull(Frame &frame, ExecutionContext &context) {
   OOMExceptionEnabler oom_exception;
   SCOPED_PROFILE_OP_BY_REF(self_);
-  
+
   // Like all filters, newly set values should not affect filtering of old
   // nodes and edges.
   ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
                                 storage::View::OLD, context.frame_change_collector);
-  while (input_cursor_->Pull(frame, context) || UNLIKELY(context.is_profile_query)) {
+  while (input_cursor_->Pull(frame, context)) {
     for (const auto &pattern_filter_cursor : pattern_filter_cursors_) {
       pattern_filter_cursor->Pull(frame, context);
     }
     if (EvaluateFilter(evaluator, self_.expression_)) return true;
-    if (UNLIKELY(context.is_profile_query)) return false;
   }
   return false;
 }
@@ -2790,13 +2789,15 @@ SetProperties::SetPropertiesCursor::SetPropertiesCursor(const SetProperties &sel
 namespace {
 
 template <typename T>
-concept AccessorWithProperties = requires(T value, storage::PropertyId property_id,
-                                          storage::PropertyValue property_value,
-                                          std::map<storage::PropertyId, storage::PropertyValue> properties) {
-  { value.ClearProperties() } -> std::same_as<storage::Result<std::map<storage::PropertyId, storage::PropertyValue>>>;
-  {value.SetProperty(property_id, property_value)};
-  {value.UpdateProperties(properties)};
-};
+concept AccessorWithProperties =
+    requires(T value, storage::PropertyId property_id, storage::PropertyValue property_value,
+             std::map<storage::PropertyId, storage::PropertyValue> properties) {
+      {
+        value.ClearProperties()
+      } -> std::same_as<storage::Result<std::map<storage::PropertyId, storage::PropertyValue>>>;
+      { value.SetProperty(property_id, property_value) };
+      { value.UpdateProperties(properties) };
+    };
 
 /// Helper function that sets the given values on either a Vertex or an Edge.
 ///
diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt
index 9f67dcf77..7c7edb60e 100644
--- a/tests/e2e/CMakeLists.txt
+++ b/tests/e2e/CMakeLists.txt
@@ -69,6 +69,7 @@ add_subdirectory(transaction_rollback)
 add_subdirectory(index_hints)
 add_subdirectory(query_modules)
 add_subdirectory(constraints)
+add_subdirectory(inspect_query)
 
 copy_e2e_python_files(pytest_runner pytest_runner.sh "")
 copy_e2e_python_files(x x.sh "")
diff --git a/tests/e2e/inspect_query/CMakeLists.txt b/tests/e2e/inspect_query/CMakeLists.txt
new file mode 100644
index 000000000..f0dbdb7cc
--- /dev/null
+++ b/tests/e2e/inspect_query/CMakeLists.txt
@@ -0,0 +1,6 @@
+function(copy_inspect_query_e2e_python_files FILE_NAME)
+    copy_e2e_python_files(inspect_query ${FILE_NAME})
+endfunction()
+
+copy_inspect_query_e2e_python_files(common.py)
+copy_inspect_query_e2e_python_files(inspect_query.py)
diff --git a/tests/e2e/inspect_query/common.py b/tests/e2e/inspect_query/common.py
new file mode 100644
index 000000000..870c80d83
--- /dev/null
+++ b/tests/e2e/inspect_query/common.py
@@ -0,0 +1,45 @@
+# Copyright 2023 Memgraph Ltd.
+#
+# Use of this software is governed by the Business Source License
+# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+# License, and you may not use this file except in compliance with the Business Source License.
+#
+# As of the Change Date specified in that file, in accordance with
+# the Business Source License, use of this software will be governed
+# by the Apache License, Version 2.0, included in the file
+# licenses/APL.txt.
+
+import typing
+
+import mgclient
+import pytest
+from gqlalchemy import Memgraph
+
+
+def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, params: dict = {}) -> typing.List[tuple]:
+    cursor.execute(query, params)
+    return cursor.fetchall()
+
+
+@pytest.fixture
+def connect(**kwargs) -> mgclient.Connection:
+    connection = mgclient.connect(host="localhost", port=7687, **kwargs)
+    connection.autocommit = True
+    cursor = connection.cursor()
+    execute_and_fetch_all(cursor, "USE DATABASE memgraph")
+    try:
+        execute_and_fetch_all(cursor, "DROP DATABASE clean")
+    except:
+        pass
+    execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n")
+    yield connection
+
+
+@pytest.fixture
+def memgraph(**kwargs) -> Memgraph:
+    memgraph = Memgraph()
+
+    yield memgraph
+
+    memgraph.drop_database()
+    memgraph.drop_indexes()
diff --git a/tests/e2e/inspect_query/inspect_query.py b/tests/e2e/inspect_query/inspect_query.py
new file mode 100644
index 000000000..2e6aa491e
--- /dev/null
+++ b/tests/e2e/inspect_query/inspect_query.py
@@ -0,0 +1,47 @@
+# Copyright 2023 Memgraph Ltd.
+#
+# Use of this software is governed by the Business Source License
+# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
+# License, and you may not use this file except in compliance with the Business Source License.
+#
+# As of the Change Date specified in that file, in accordance with
+# the Business Source License, use of this software will be governed
+# by the Apache License, Version 2.0, included in the file
+# licenses/APL.txt.
+
+import sys
+
+import mgclient
+import pytest
+from common import memgraph
+
+
+def test_label_index_hint(memgraph):
+    explain_query = "EXPLAIN MATCH (n:Node) RETURN n.property1;"
+    profile_query = "PROFILE MATCH (n:Node) RETURN n.property1;"
+
+    expected_profile = ["* Produce {n.property1}", "* Filter (n :Node)", "* ScanAll (n)", "* Once"]
+    expected_explain = [" * Produce {n.property1}", " * Filter (n :Node)", " * ScanAll (n)", " * Once"]
+
+    explain_empty = [row["QUERY PLAN"] for row in memgraph.execute_and_fetch(explain_query)]
+    profile_empty = [row["OPERATOR"] for row in memgraph.execute_and_fetch(profile_query)]
+
+    assert explain_empty == expected_explain and profile_empty == expected_profile
+
+    memgraph.execute("CREATE (n:WrongLabel {property1: 2});")
+
+    explain_none_matched = [row["QUERY PLAN"] for row in memgraph.execute_and_fetch(explain_query)]
+    profile_none_matched = [row["OPERATOR"] for row in memgraph.execute_and_fetch(profile_query)]
+
+    assert explain_none_matched == expected_explain and profile_none_matched == expected_profile
+
+    memgraph.execute("CREATE (n:Node {property1: 2});")
+
+    explain_has_match = [row["QUERY PLAN"] for row in memgraph.execute_and_fetch(explain_query)]
+    profile_has_match = [row["OPERATOR"] for row in memgraph.execute_and_fetch(profile_query)]
+
+    assert explain_has_match == expected_explain and profile_has_match == expected_profile
+
+
+if __name__ == "__main__":
+    sys.exit(pytest.main([__file__, "-rA"]))
diff --git a/tests/e2e/inspect_query/workloads.yaml b/tests/e2e/inspect_query/workloads.yaml
new file mode 100644
index 000000000..c0e8d6688
--- /dev/null
+++ b/tests/e2e/inspect_query/workloads.yaml
@@ -0,0 +1,13 @@
+inspect_query_cluster: &inspect_query_cluster
+  cluster:
+    main:
+      args: ["--bolt-port", "7687", "--log-level=TRACE"]
+      log_file: "inspect_query.log"
+      setup_queries: []
+      validation_queries: []
+
+workloads:
+  - name: "Verify that query inspection works"
+    binary: "tests/e2e/pytest_runner.sh"
+    args: ["inspect_query/inspect_query.py"]
+    <<: *inspect_query_cluster