diff --git a/tests/e2e/CMakeLists.txt b/tests/e2e/CMakeLists.txt index 8ebd66dda..6dd6995ed 100644 --- a/tests/e2e/CMakeLists.txt +++ b/tests/e2e/CMakeLists.txt @@ -66,6 +66,7 @@ add_subdirectory(concurrent_query_modules) add_subdirectory(show_index_info) add_subdirectory(set_properties) add_subdirectory(transaction_rollback) +add_subdirectory(constraints_as_indices) copy_e2e_python_files(pytest_runner pytest_runner.sh "") file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/memgraph-selfsigned.crt DESTINATION ${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tests/e2e/constraints_as_indices/CMakeLists.txt b/tests/e2e/constraints_as_indices/CMakeLists.txt new file mode 100644 index 000000000..ef57996ed --- /dev/null +++ b/tests/e2e/constraints_as_indices/CMakeLists.txt @@ -0,0 +1,6 @@ +function(copy_constraints_as_indices_e2e_python_files FILE_NAME) + copy_e2e_python_files(constraints_as_indices ${FILE_NAME}) +endfunction() + +copy_constraints_as_indices_e2e_python_files(common.py) +copy_constraints_as_indices_e2e_python_files(constraints_as_indices.py) diff --git a/tests/e2e/constraints_as_indices/common.py b/tests/e2e/constraints_as_indices/common.py new file mode 100644 index 000000000..67ce41522 --- /dev/null +++ b/tests/e2e/constraints_as_indices/common.py @@ -0,0 +1,30 @@ +# 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 pytest +from gqlalchemy import Memgraph + +QUERY_PLAN = "QUERY PLAN" + + +@pytest.fixture +def memgraph(**kwargs) -> Memgraph: + memgraph = Memgraph() + + yield memgraph + + memgraph.drop_database() + memgraph.drop_indexes() + memgraph.ensure_constraints([]) + + +def extract_query_plan(results): + return [x[QUERY_PLAN] for x in results] diff --git a/tests/e2e/constraints_as_indices/constraints_as_indices.py b/tests/e2e/constraints_as_indices/constraints_as_indices.py new file mode 100644 index 000000000..baed5fc59 --- /dev/null +++ b/tests/e2e/constraints_as_indices/constraints_as_indices.py @@ -0,0 +1,69 @@ +# 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 pytest +from common import extract_query_plan, memgraph + + +def test_given_constraint_when_querying_then_index_scanning(memgraph): + memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop IS UNIQUE;") + + expected_results = [" * Produce {n}", " * ScanAllByLabelPropertyValue (n :label {prop})", " * Once"] + + actual_results = extract_query_plan( + memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop = 1 RETURN n;") + ) + + assert expected_results == actual_results + + +def test_given_multiprop_constraint_when_querying_then_sequential_scanning(memgraph): + memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop1, n.prop2 IS UNIQUE;") + + expected_results = [" * Produce {n}", " * Filter", " * ScanAll (n)", " * Once"] + actual_results = extract_query_plan( + memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop1 = 1 AND n.prop2 = 2 RETURN n;") + ) + + assert expected_results == actual_results + + +def test_given_constraint_and_index_when_querying_then_index_scanning(memgraph): + memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop1 IS UNIQUE;") + memgraph.execute("CREATE INDEX ON :label(prop1);") + + expected_results = [" * Produce {n}", " * ScanAllByLabelPropertyValue (n :label {prop1})", " * Once"] + + actual_results = extract_query_plan( + memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop1 = 1 RETURN n;") + ) + + assert expected_results == actual_results + + +def test_given_constraint_and_index_with_different_distribution_when_querying_then_prefer_constraint(memgraph): + memgraph.execute("CREATE CONSTRAINT ON (n:label) ASSERT n.prop1 IS UNIQUE;") + memgraph.execute("CREATE INDEX ON :label(prop2);") + memgraph.execute("FOREACH (i IN range(1, 1000) | CREATE (:Node {prop1: i, prop2: i % 2}))") + + expected_results = [" * Produce {n}", " * Filter", " * ScanAllByLabelPropertyValue (n :label {prop1})", " * Once"] + + actual_results = extract_query_plan( + memgraph.execute_and_fetch("EXPLAIN MATCH (n:label) WHERE n.prop1 = 500 and n.prop2 = 0 RETURN n;") + ) + + assert expected_results == actual_results + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/constraints_as_indices/workloads.yaml b/tests/e2e/constraints_as_indices/workloads.yaml new file mode 100644 index 000000000..ea3786df8 --- /dev/null +++ b/tests/e2e/constraints_as_indices/workloads.yaml @@ -0,0 +1,14 @@ +constraints_as_indices_cluster: &constraints_as_indices_cluster + cluster: + main: + args: ["--bolt-port", "7687", "--log-level=TRACE"] + log_file: "analyze_graph.log" + setup_queries: [] + validation_queries: [] + + +workloads: + - name: "Constraints as indices" + binary: "tests/e2e/pytest_runner.sh" + args: ["analyze_graph/constraints_as_indices.py"] + <<: *constraints_as_indices_cluster diff --git a/tests/unit/query_plan.cpp b/tests/unit/query_plan.cpp index 910ebdc54..96a156de6 100644 --- a/tests/unit/query_plan.cpp +++ b/tests/unit/query_plan.cpp @@ -1144,6 +1144,20 @@ TYPED_TEST(TestPlanner, WhereIndexedLabelProperty) { CheckPlan(planner.plan(), symbol_table, ExpectScanAllByLabelPropertyValue(label, property, lit_42), ExpectProduce()); } +TYPED_TEST(TestPlanner, WhereIndexedUniqueConstraint) { + // Test MATCH (n :label) WHERE n.property = 42 RETURN n + FakeDbAccessor dba; + auto label = dba.Label("label"); + auto property = PROPERTY_PAIR(dba, "property"); + dba.SetConstraintCount(label, property.second, 0); + auto lit_42 = LITERAL(42); + auto *query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n", "label"))), + WHERE(EQ(PROPERTY_LOOKUP(dba, "n", property), lit_42)), RETURN("n"))); + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, this->storage, symbol_table, query); + CheckPlan(planner.plan(), symbol_table, ExpectScanAllByLabelPropertyValue(label, property, lit_42), ExpectProduce()); +} + TYPED_TEST(TestPlanner, BestPropertyIndexed) { // Test MATCH (n :label) WHERE n.property = 1 AND n.better = 42 RETURN n FakeDbAccessor dba; @@ -1165,6 +1179,27 @@ TYPED_TEST(TestPlanner, BestPropertyIndexed) { ExpectProduce()); } +TYPED_TEST(TestPlanner, BetterConstraintIndexedThanLabelPropertyIndex) { + // Test MATCH (n :label) WHERE n.property = 1 AND n.better = 42 RETURN n + FakeDbAccessor dba; + auto label = dba.Label("label"); + auto property = dba.Property("property"); + // Add a vertex with :label+property combination, so that the best + // :label+better remains empty and thus better choice. + dba.SetIndexCount(label, property, 1); + auto better = PROPERTY_PAIR(dba, "better"); + dba.SetConstraintCount(label, better.second, 0); + auto lit_42 = LITERAL(42); + auto *query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n", "label"))), + WHERE(AND(EQ(PROPERTY_LOOKUP(dba, "n", property), LITERAL(1)), EQ(PROPERTY_LOOKUP(dba, "n", better), lit_42))), + RETURN("n"))); + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, this->storage, symbol_table, query); + CheckPlan(planner.plan(), symbol_table, ExpectScanAllByLabelPropertyValue(label, better, lit_42), ExpectFilter(), + ExpectProduce()); +} + TYPED_TEST(TestPlanner, MultiPropertyIndexScan) { // Test MATCH (n :label1), (m :label2) WHERE n.prop1 = 1 AND m.prop2 = 2 // RETURN n, m diff --git a/tests/unit/query_plan_checker.hpp b/tests/unit/query_plan_checker.hpp index 13fbff199..07d732349 100644 --- a/tests/unit/query_plan_checker.hpp +++ b/tests/unit/query_plan_checker.hpp @@ -629,6 +629,28 @@ class FakeDbAccessor { label_property_index_.emplace_back(label, property, count); } + void SetConstraintCount(memgraph::storage::LabelId label, memgraph::storage::PropertyId property, int64_t count) { + for (auto &constraint : unique_constraints_) { + if (std::get<0>(constraint) != label) { + continue; + } + auto const &props = std::get<1>(constraint); + if (props.size() != 1) { + continue; + } + + if (props[0] != property) { + continue; + } + + std::get<2>(constraint) = count; + return; + } + + std::vector props{property}; + unique_constraints_.emplace_back(label, props, count); + } + memgraph::storage::LabelId NameToLabel(const std::string &name) { auto found = labels_.find(name); if (found != labels_.end()) return found->second;