From 156e2cd095e4bce496bf0be485999ced05de2e92 Mon Sep 17 00:00:00 2001 From: Andi Date: Wed, 18 Jan 2023 15:05:10 +0100 Subject: [PATCH 1/9] On delete triggers invalid edge reference (#717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added check if there is invalid reference to the underlying edge * Added fix and e2e tests * Isolation levels tracking based on from_vertex_ * Added explicit transaction test + edge accessor changes based on the vertex_edge * Autocommit on tests, initialize deleted by checking out_edges Co-authored-by: Marko Budiselić --- src/storage/v2/edge_accessor.cpp | 43 ++++- tests/e2e/triggers/CMakeLists.txt | 7 + tests/e2e/triggers/common.py | 34 ++++ .../e2e/triggers/triggers_properties_false.py | 170 ++++++++++++++++++ tests/e2e/triggers/workloads.yaml | 18 +- 5 files changed, 266 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/triggers/common.py create mode 100644 tests/e2e/triggers/triggers_properties_false.py diff --git a/src/storage/v2/edge_accessor.cpp b/src/storage/v2/edge_accessor.cpp index ef0444422..2a15292a2 100644 --- a/src/storage/v2/edge_accessor.cpp +++ b/src/storage/v2/edge_accessor.cpp @@ -12,6 +12,7 @@ #include "storage/v2/edge_accessor.hpp" #include +#include #include "storage/v2/mvcc.hpp" #include "storage/v2/property_value.hpp" @@ -21,8 +22,47 @@ namespace memgraph::storage { bool EdgeAccessor::IsVisible(const View view) const { - bool deleted = true; bool exists = true; + bool deleted = true; + // When edges don't have properties, their isolation level is still dictated by MVCC -> + // iterate over the deltas of the from_vertex_ and see which deltas can be applied on edges. + if (!config_.properties_on_edges) { + Delta *delta = nullptr; + { + std::lock_guard guard(from_vertex_->lock); + // Initialize deleted by checking if out edges contain edge_ + deleted = std::find_if(from_vertex_->out_edges.begin(), from_vertex_->out_edges.end(), [&](const auto &out_edge) { + return std::get<2>(out_edge) == edge_; + }) == from_vertex_->out_edges.end(); + delta = from_vertex_->delta; + } + ApplyDeltasForRead(transaction_, delta, view, [&](const Delta &delta) { + switch (delta.action) { + case Delta::Action::ADD_LABEL: + case Delta::Action::REMOVE_LABEL: + case Delta::Action::SET_PROPERTY: + case Delta::Action::REMOVE_IN_EDGE: + case Delta::Action::ADD_IN_EDGE: + case Delta::Action::RECREATE_OBJECT: + case Delta::Action::DELETE_OBJECT: + break; + case Delta::Action::ADD_OUT_EDGE: { // relevant for the from_vertex_ -> we just deleted the edge + if (delta.vertex_edge.edge == edge_) { + deleted = false; + } + break; + } + case Delta::Action::REMOVE_OUT_EDGE: { // also relevant for the from_vertex_ -> we just added the edge + if (delta.vertex_edge.edge == edge_) { + exists = false; + } + break; + } + } + }); + return exists && (for_deleted_ || !deleted); + } + Delta *delta = nullptr; { std::lock_guard guard(edge_.ptr->lock); @@ -49,7 +89,6 @@ bool EdgeAccessor::IsVisible(const View view) const { } } }); - return exists && (for_deleted_ || !deleted); } diff --git a/tests/e2e/triggers/CMakeLists.txt b/tests/e2e/triggers/CMakeLists.txt index 625d0d574..7b540d59f 100644 --- a/tests/e2e/triggers/CMakeLists.txt +++ b/tests/e2e/triggers/CMakeLists.txt @@ -20,3 +20,10 @@ add_subdirectory(procedures) add_dependencies(memgraph__e2e__triggers__on_create memgraph__e2e__triggers__write.py) add_dependencies(memgraph__e2e__triggers__on_update memgraph__e2e__triggers__write.py) add_dependencies(memgraph__e2e__triggers__on_delete memgraph__e2e__triggers__write.py) + +function(copy_triggers_e2e_python_files FILE_NAME) + copy_e2e_python_files(triggers ${FILE_NAME}) +endfunction() + +copy_triggers_e2e_python_files(common.py) +copy_triggers_e2e_python_files(triggers_properties_false.py) diff --git a/tests/e2e/triggers/common.py b/tests/e2e/triggers/common.py new file mode 100644 index 000000000..89e7e5a88 --- /dev/null +++ b/tests/e2e/triggers/common.py @@ -0,0 +1,34 @@ +# Copyright 2022 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 + + +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 + triggers_list = execute_and_fetch_all(connection.cursor(), "SHOW TRIGGERS;") + for trigger in triggers_list: + execute_and_fetch_all(connection.cursor(), f"DROP TRIGGER {trigger[0]}") + execute_and_fetch_all(connection.cursor(), "MATCH (n) DETACH DELETE n") + yield connection + for trigger in triggers_list: + execute_and_fetch_all(connection.cursor(), f"DROP TRIGGER {trigger[0]}") + execute_and_fetch_all(connection.cursor(), "MATCH (n) DETACH DELETE n") diff --git a/tests/e2e/triggers/triggers_properties_false.py b/tests/e2e/triggers/triggers_properties_false.py new file mode 100644 index 000000000..2f5b69feb --- /dev/null +++ b/tests/e2e/triggers/triggers_properties_false.py @@ -0,0 +1,170 @@ +# Copyright 2022 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 connect, execute_and_fetch_all + + +@pytest.mark.parametrize("ba_commit", ["BEFORE COMMIT", "AFTER COMMIT"]) +def test_create_on_create(ba_commit, connect): + """ + Args: + ba_commit (str): BEFORE OR AFTER commit + """ + cursor = connect.cursor() + QUERY_TRIGGER_CREATE = f""" + CREATE TRIGGER CreateTriggerEdgesCount + ON --> CREATE + {ba_commit} + EXECUTE + CREATE (n:CreatedEdge {{count: size(createdEdges)}}) + """ + execute_and_fetch_all(cursor, QUERY_TRIGGER_CREATE) + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 1})") + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 2})") + res = execute_and_fetch_all(cursor, "MATCH (n:Node) RETURN n") + assert len(res) == 2 + res2 = execute_and_fetch_all(cursor, "MATCH (n:CreatedEdge) RETURN n") + assert len(res2) == 0 + QUERY_CREATE_EDGE = """ + MATCH (n:Node {id: 1}), (m:Node {id: 2}) + CREATE (n)-[r:TYPE]->(m); + """ + execute_and_fetch_all(cursor, QUERY_CREATE_EDGE) + # See if trigger was triggered + nodes = execute_and_fetch_all(cursor, "MATCH (n:Node) RETURN n") + assert len(nodes) == 2 + created_edges = execute_and_fetch_all(cursor, "MATCH (n:CreatedEdge) RETURN n") + assert len(created_edges) == 1 + # execute_and_fetch_all(cursor, "DROP TRIGGER CreateTriggerEdgesCount") + # execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n;") + + +@pytest.mark.parametrize("ba_commit", ["AFTER COMMIT", "BEFORE COMMIT"]) +def test_create_on_delete(ba_commit, connect): + """ + Args: + ba_commit (str): BEFORE OR AFTER commit + """ + cursor = connect.cursor() + QUERY_TRIGGER_CREATE = f""" + CREATE TRIGGER DeleteTriggerEdgesCount + ON --> DELETE + {ba_commit} + EXECUTE + CREATE (n:DeletedEdge {{count: size(deletedEdges)}}) + """ + # Setup queries + execute_and_fetch_all(cursor, QUERY_TRIGGER_CREATE) + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 1})") + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 2})") + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 3})") + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 4})") + res = execute_and_fetch_all(cursor, "MATCH (n:Node) RETURN n") + assert len(res) == 4 + res2 = execute_and_fetch_all(cursor, "MATCH (n:DeletedEdge) RETURN n") + assert len(res2) == 0 + # create an edge that will be deleted + QUERY_CREATE_EDGE = """ + MATCH (n:Node {id: 1}), (m:Node {id: 2}) + CREATE (n)-[r:TYPE]->(m); + """ + execute_and_fetch_all(cursor, QUERY_CREATE_EDGE) + # create an edge that won't be deleted + QUERY_CREATE_EDGE_NO_DELETE = """ + MATCH (n:Node {id: 3}), (m:Node {id: 4}) + CREATE (n)-[r:NO_DELETE_EDGE]->(m); + """ + execute_and_fetch_all(cursor, QUERY_CREATE_EDGE_NO_DELETE) + # Delete only one type of the edger + QUERY_DELETE_EDGE = """ + MATCH ()-[r:TYPE]->() + DELETE r; + """ + execute_and_fetch_all(cursor, QUERY_DELETE_EDGE) + # See if trigger was triggered + nodes = execute_and_fetch_all(cursor, "MATCH (n:Node) RETURN n") + assert len(nodes) == 4 + # Check how many edges got deleted + deleted_edges = execute_and_fetch_all(cursor, "MATCH (n:DeletedEdge) RETURN n") + assert len(deleted_edges) == 1 + # execute_and_fetch_all(cursor, "DROP TRIGGER DeleteTriggerEdgesCount") + # execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n")`` + + +@pytest.mark.parametrize("ba_commit", ["BEFORE COMMIT", "AFTER COMMIT"]) +def test_create_on_delete_explicit_transaction(ba_commit): + """ + Args: + ba_commit (str): BEFORE OR AFTER commit + """ + connection_with_autocommit = mgclient.connect(host="localhost", port=7687) + connection_with_autocommit.autocommit = True + cursor_autocommit = connection_with_autocommit.cursor() + QUERY_TRIGGER_CREATE = f""" + CREATE TRIGGER DeleteTriggerEdgesCountExplicit + ON --> DELETE + {ba_commit} + EXECUTE + CREATE (n:DeletedEdge {{count: size(deletedEdges)}}) + """ + # Setup queries + execute_and_fetch_all(cursor_autocommit, QUERY_TRIGGER_CREATE) + # Start explicit transaction on the execution of the first command + connection_without_autocommit = mgclient.connect(host="localhost", port=7687) + connection_without_autocommit.autocommit = False + cursor = connection_without_autocommit.cursor() + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 1})") + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 2})") + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 3})") + execute_and_fetch_all(cursor, "CREATE (n:Node {id: 4})") + res = execute_and_fetch_all(cursor, "MATCH (n:Node) RETURN n") + assert len(res) == 4 + res2 = execute_and_fetch_all(cursor, "MATCH (n:DeletedEdge) RETURN n;") + assert len(res2) == 0 + QUERY_CREATE_EDGE = """ + MATCH (n:Node {id: 1}), (m:Node {id: 2}) + CREATE (n)-[r:TYPE]->(m); + """ + execute_and_fetch_all(cursor, QUERY_CREATE_EDGE) + # create an edge that won't be deleted + QUERY_CREATE_EDGE_NO_DELETE = """ + MATCH (n:Node {id: 3}), (m:Node {id: 4}) + CREATE (n)-[r:NO_DELETE_EDGE]->(m); + """ + execute_and_fetch_all(cursor, QUERY_CREATE_EDGE_NO_DELETE) + # Delete only one type of the edger + QUERY_DELETE_EDGE = """ + MATCH ()-[r:TYPE]->() + DELETE r; + """ + execute_and_fetch_all(cursor, QUERY_DELETE_EDGE) + connection_without_autocommit.commit() # finish explicit transaction + nodes = execute_and_fetch_all(cursor, "MATCH (n:Node) RETURN n") + assert len(nodes) == 4 + # Check how many edges got deleted + deleted_nodes_edges = execute_and_fetch_all(cursor, "MATCH (n:DeletedEdge) RETURN n") + assert len(deleted_nodes_edges) == 0 + # Delete with the original cursor because triggers aren't allowed in multi-transaction environment + execute_and_fetch_all(cursor_autocommit, "DROP TRIGGER DeleteTriggerEdgesCountExplicit") + execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n") + connection_without_autocommit.commit() # finish explicit transaction + nodes = execute_and_fetch_all(cursor_autocommit, "MATCH (n) RETURN n") + assert len(nodes) == 0 + connection_with_autocommit.close() + connection_without_autocommit.close() + + +if __name__ == "__main__": + sys.exit(pytest.main([__file__, "-rA"])) diff --git a/tests/e2e/triggers/workloads.yaml b/tests/e2e/triggers/workloads.yaml index dc2706f93..a80a8f2fa 100644 --- a/tests/e2e/triggers/workloads.yaml +++ b/tests/e2e/triggers/workloads.yaml @@ -2,7 +2,14 @@ bolt_port: &bolt_port "7687" template_cluster: &template_cluster cluster: main: - args: ["--bolt-port", *bolt_port, "--log-level=TRACE"] + args: ["--bolt-port", *bolt_port, "--log-level=TRACE", "--storage-properties-on-edges=True"] + log_file: "triggers-e2e.log" + setup_queries: [] + validation_queries: [] +storage_properties_edges_false: &storage_properties_edges_false + cluster: + main: + args: ["--bolt-port", *bolt_port, "--log-level=TRACE", "--also-log-to-stderr", "--storage-properties-on-edges=False"] log_file: "triggers-e2e.log" setup_queries: [] validation_queries: [] @@ -18,7 +25,7 @@ workloads: args: ["--bolt-port", *bolt_port] proc: "tests/e2e/triggers/procedures/" <<: *template_cluster - - name: "ON DELETE Triggers" + - name: "ON DELETE Triggers Storage Properties On Edges True" binary: "tests/e2e/triggers/memgraph__e2e__triggers__on_delete" args: ["--bolt-port", *bolt_port] proc: "tests/e2e/triggers/procedures/" @@ -27,5 +34,8 @@ workloads: binary: "tests/e2e/triggers/memgraph__e2e__triggers__privileges" args: ["--bolt-port", *bolt_port] <<: *template_cluster - - + - name: "ON DELETE Triggers Storage Properties On Edges False" # should be the same as the python file + binary: "tests/e2e/pytest_runner.sh" + proc: "tests/e2e/triggers/procedures/" + args: ["triggers/triggers_properties_false.py"] + <<: *storage_properties_edges_false From d9eeedb9ee167639b066fb857d846a761842ef8b Mon Sep 17 00:00:00 2001 From: niko4299 <51059248+niko4299@users.noreply.github.com> Date: Wed, 18 Jan 2023 16:33:03 +0100 Subject: [PATCH 2/9] Adding qid in bolt (#721) --- src/communication/bolt/client.cpp | 7 +- src/communication/bolt/v1/states/handlers.hpp | 102 +++++++++++------- tests/integration/transactions/tester.cpp | 27 +++++ 3 files changed, 98 insertions(+), 38 deletions(-) diff --git a/src/communication/bolt/client.cpp b/src/communication/bolt/client.cpp index 20e2699c4..5a8dbf9d2 100644 --- a/src/communication/bolt/client.cpp +++ b/src/communication/bolt/client.cpp @@ -146,9 +146,10 @@ QueryData Client::Execute(const std::string &query, const std::map ExceptionToErrorMessage(const std::ex namespace details { -template -State HandleRun(TSession &session, const State state, const Value &query, const Value ¶ms) { - if (state != State::Idle) { - // Client could potentially recover if we move to error state, but there is - // no legitimate situation in which well working client would end up in this - // situation. - spdlog::trace("Unexpected RUN command!"); - return State::Close; - } - - DMG_ASSERT(!session.encoder_buffer_.HasData(), "There should be no data to write in this state"); - - spdlog::debug("[Run] '{}'", query.ValueString()); - - try { - // Interpret can throw. - const auto [header, qid] = session.Interpret(query.ValueString(), params.ValueMap()); - // Convert std::string to Value - std::vector vec; - std::map data; - vec.reserve(header.size()); - for (auto &i : header) vec.emplace_back(std::move(i)); - data.emplace("fields", std::move(vec)); - // Send the header. - if (!session.encoder_.MessageSuccess(data)) { - spdlog::trace("Couldn't send query header!"); - return State::Close; - } - return State::Result; - } catch (const std::exception &e) { - return HandleFailure(session, e); - } -} - template State HandlePullDiscard(TSession &session, std::optional n, std::optional qid) { try { @@ -229,7 +195,36 @@ State HandleRunV1(TSession &session, const State state, const Marker marker) { return State::Close; } - return details::HandleRun(session, state, query, params); + if (state != State::Idle) { + // Client could potentially recover if we move to error state, but there is + // no legitimate situation in which well working client would end up in this + // situation. + spdlog::trace("Unexpected RUN command!"); + return State::Close; + } + + DMG_ASSERT(!session.encoder_buffer_.HasData(), "There should be no data to write in this state"); + + spdlog::debug("[Run] '{}'", query.ValueString()); + + try { + // Interpret can throw. + const auto [header, qid] = session.Interpret(query.ValueString(), params.ValueMap()); + // Convert std::string to Value + std::vector vec; + std::map data; + vec.reserve(header.size()); + for (auto &i : header) vec.emplace_back(std::move(i)); + data.emplace("fields", std::move(vec)); + // Send the header. + if (!session.encoder_.MessageSuccess(data)) { + spdlog::trace("Couldn't send query header!"); + return State::Close; + } + return State::Result; + } catch (const std::exception &e) { + return HandleFailure(session, e); + } } template @@ -257,7 +252,40 @@ State HandleRunV4(TSession &session, const State state, const Marker marker) { spdlog::trace("Couldn't read extra field!"); } - return details::HandleRun(session, state, query, params); + if (state != State::Idle) { + // Client could potentially recover if we move to error state, but there is + // no legitimate situation in which well working client would end up in this + // situation. + spdlog::trace("Unexpected RUN command!"); + return State::Close; + } + + DMG_ASSERT(!session.encoder_buffer_.HasData(), "There should be no data to write in this state"); + + spdlog::debug("[Run] '{}'", query.ValueString()); + + try { + // Interpret can throw. + const auto [header, qid] = session.Interpret(query.ValueString(), params.ValueMap()); + // Convert std::string to Value + std::vector vec; + std::map data; + vec.reserve(header.size()); + for (auto &i : header) vec.emplace_back(std::move(i)); + data.emplace("fields", std::move(vec)); + if (qid.has_value()) { + data.emplace("qid", Value{*qid}); + } + + // Send the header. + if (!session.encoder_.MessageSuccess(data)) { + spdlog::trace("Couldn't send query header!"); + return State::Close; + } + return State::Result; + } catch (const std::exception &e) { + return HandleFailure(session, e); + } } template diff --git a/tests/integration/transactions/tester.cpp b/tests/integration/transactions/tester.cpp index 06f89202c..407b11892 100644 --- a/tests/integration/transactions/tester.cpp +++ b/tests/integration/transactions/tester.cpp @@ -45,6 +45,21 @@ class BoltClient : public ::testing::Test { return true; } + bool ExecuteAndCheckQid(const std::string &query, int qid, const std::string &message = "") { + try { + auto ret = client_.Execute(query, {}); + if (ret.metadata["qid"].ValueInt() != qid) { + return false; + } + } catch (const ClientQueryException &e) { + if (message != "") { + EXPECT_EQ(e.what(), message); + } + throw; + } + return true; + } + int64_t GetCount() { auto ret = client_.Execute("match (n) return count(n)", {}); EXPECT_EQ(ret.records.size(), 1); @@ -461,6 +476,18 @@ TEST_F(BoltClient, MixedCaseAndWhitespace) { EXPECT_FALSE(TransactionActive()); } +TEST_F(BoltClient, TestQid) { + for (int i = 0; i < 3; ++i) { + EXPECT_TRUE(Execute("match (n) return count(n)")); + } + EXPECT_TRUE(Execute("begin")); + for (int i = 0; i < 3; ++i) { + EXPECT_TRUE(ExecuteAndCheckQid("match (n) return count(n)", i + 1)); + } + EXPECT_TRUE(Execute("commit")); + EXPECT_FALSE(TransactionActive()); +} + int main(int argc, char **argv) { ::testing::InitGoogleTest(&argc, argv); gflags::ParseCommandLineFlags(&argc, &argv, true); From 128a6cd522cf34af218f27752184c0e356dc226a Mon Sep 17 00:00:00 2001 From: Jure Bajic Date: Thu, 19 Jan 2023 12:31:59 +0100 Subject: [PATCH 3/9] Update license year (#739) --- licenses/BSL.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/licenses/BSL.txt b/licenses/BSL.txt index 5acc4447a..6565fd4c2 100644 --- a/licenses/BSL.txt +++ b/licenses/BSL.txt @@ -36,7 +36,7 @@ ADDITIONAL USE GRANT: You may use the Licensed Work in accordance with the 3. using the Licensed Work to create a work or solution which competes (or might reasonably be expected to compete) with the Licensed Work. -CHANGE DATE: 2026-13-12 +CHANGE DATE: 2027-19-01 CHANGE LICENSE: Apache License, Version 2.0 For information about alternative licensing arrangements, please visit: https://memgraph.com/legal. From 1cd1da84fd9b5f5d205fac5444376018670c05bb Mon Sep 17 00:00:00 2001 From: Antonio Filipovic <61245998+antoniofilipovic@users.noreply.github.com> Date: Mon, 23 Jan 2023 12:57:17 +0100 Subject: [PATCH 4/9] Fix bug on (vertex|edge) properties in C++ API (#732) --- include/_mgp.hpp | 10 ++- include/mgp.hpp | 137 ++++++++++++++++------------------------- tests/unit/cpp_api.cpp | 22 ++++++- 3 files changed, 81 insertions(+), 88 deletions(-) diff --git a/include/_mgp.hpp b/include/_mgp.hpp index 39cbedb4e..aae4a0824 100644 --- a/include/_mgp.hpp +++ b/include/_mgp.hpp @@ -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 @@ -378,6 +378,10 @@ inline mgp_value *vertex_get_property(mgp_vertex *v, const char *property_name, return MgInvoke(mgp_vertex_get_property, v, property_name, memory); } +inline void vertex_set_property(mgp_vertex *v, const char *property_name, mgp_value *property_value) { + MgInvokeVoid(mgp_vertex_set_property, v, property_name, property_value); +} + inline mgp_properties_iterator *vertex_iter_properties(mgp_vertex *v, mgp_memory *memory) { return MgInvoke(mgp_vertex_iter_properties, v, memory); } @@ -410,6 +414,10 @@ inline mgp_value *edge_get_property(mgp_edge *e, const char *property_name, mgp_ return MgInvoke(mgp_edge_get_property, e, property_name, memory); } +inline void edge_set_property(mgp_edge *e, const char *property_name, mgp_value *property_value) { + MgInvokeVoid(mgp_edge_set_property, e, property_name, property_value); +} + inline mgp_properties_iterator *edge_iter_properties(mgp_edge *e, mgp_memory *memory) { return MgInvoke(mgp_edge_iter_properties, e, memory); } diff --git a/include/mgp.hpp b/include/mgp.hpp index e18f18961..29d6fb25e 100644 --- a/include/mgp.hpp +++ b/include/mgp.hpp @@ -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 @@ -66,9 +66,12 @@ struct MapItem; class Duration; class Value; +struct StealType {}; +inline constexpr StealType steal{}; + inline mgp_memory *memory{nullptr}; -/* #region Graph (Id, Graph, Nodes, GraphRelationships, Relationships, Properties & Labels) */ +/* #region Graph (Id, Graph, Nodes, GraphRelationships, Relationships & Labels) */ /// Wrapper for int64_t IDs to prevent dangerous implicit conversions. class Id { @@ -281,40 +284,6 @@ class Relationships { mgp_edges_iterator *relationships_iterator_ = nullptr; }; -/// @brief View of node properties. -class Properties { - public: - explicit Properties(mgp_properties_iterator *properties_iterator); - - /// @brief Returns the size of the properties map. - size_t Size() const; - /// @brief Returns whether the properties map is empty. - bool Empty() const; - - /// @brief Returns the value associated with the given `key`. If there’s no such value, the behavior is undefined. - /// @note Each key-value pair needs to be checked, ensuing O(n) time complexity. - Value operator[](const std::string_view key) const; - - std::map::const_iterator begin() const; - std::map::const_iterator end() const; - - std::map::const_iterator cbegin() const; - std::map::const_iterator cend() const; - - /// @brief Returns the key-value iterator for the given `key`. If there’s no such pair, returns the end of the - /// iterator. - /// @note Each key-value pair needs to be checked, ensuing O(n) time complexity. - std::map::const_iterator find(const std::string_view key) const; - - /// @exception std::runtime_error Map contains value(s) of unknown type. - bool operator==(const Properties &other) const; - /// @exception std::runtime_error Map contains value(s) of unknown type. - bool operator!=(const Properties &other) const; - - private: - std::map property_map_; -}; - /// @brief View of node labels. class Labels { public: @@ -596,10 +565,11 @@ class Node { bool HasLabel(std::string_view label) const; /// @brief Returns an iterable & indexable structure of the node’s properties. - class Properties Properties() const; + std::map Properties() const; - /// @brief Returns the value of the node’s `property_name` property. - Value operator[](const std::string_view property_name) const; + void SetProperty(std::string property, Value value); + + Value GetProperty(const std::string &property) const; /// @brief Returns an iterable structure of the node’s inbound relationships. Relationships InRelationships() const; @@ -649,11 +619,12 @@ class Relationship { /// @brief Returns the relationship’s type. std::string_view Type() const; - /// @brief Returns an iterable & indexable structure of the relationship’s properties. - class Properties Properties() const; + /// @brief Returns an std::map of the relationship’s properties. + std::map Properties() const; - /// @brief Returns the value of the relationship’s `property_name` property. - Value operator[](const std::string_view property_name) const; + void SetProperty(std::string property, Value value); + + Value GetProperty(const std::string &property) const; /// @brief Returns the relationship’s source node. Node From() const; @@ -986,6 +957,8 @@ class Value { explicit Value(mgp_value *ptr); + explicit Value(StealType /*steal*/, mgp_value *ptr); + // Null constructor: explicit Value(); @@ -1963,35 +1936,6 @@ inline Relationships::Iterator Relationships::cbegin() const { return Iterator(r inline Relationships::Iterator Relationships::cend() const { return Iterator(nullptr); } -// Properties: - -inline Properties::Properties(mgp_properties_iterator *properties_iterator) { - for (auto property = mgp::properties_iterator_get(properties_iterator); property; - property = mgp::properties_iterator_next(properties_iterator)) { - auto value = Value(property->value); - property_map_.emplace(property->name, value); - } - mgp::properties_iterator_destroy(properties_iterator); -} - -inline size_t Properties::Size() const { return property_map_.size(); } - -inline bool Properties::Empty() const { return Size() == 0; } - -inline Value Properties::operator[](const std::string_view key) const { return property_map_.at(key); } - -inline std::map::const_iterator Properties::begin() const { return property_map_.begin(); } - -inline std::map::const_iterator Properties::end() const { return property_map_.end(); } - -inline std::map::const_iterator Properties::cbegin() const { return property_map_.cbegin(); } - -inline std::map::const_iterator Properties::cend() const { return property_map_.cend(); } - -inline bool Properties::operator==(const Properties &other) const { return property_map_ == other.property_map_; } - -inline bool Properties::operator!=(const Properties &other) const { return !(*this == other); } - // Labels: inline Labels::Labels(mgp_vertex *node_ptr) : node_ptr_(mgp::vertex_copy(node_ptr, memory)) {} @@ -2306,10 +2250,6 @@ inline void Map::Insert(std::string_view key, Value &&value) { value.ptr_ = nullptr; } -inline std::map::const_iterator Properties::find(const std::string_view key) const { - return property_map_.find(key); -} - inline bool Map::operator==(const Map &other) const { return util::MapsEqual(ptr_, other.ptr_); } inline bool Map::operator!=(const Map &other) const { return !(*this == other); } @@ -2366,10 +2306,6 @@ inline bool Node::HasLabel(std::string_view label) const { return false; } -inline class Properties Node::Properties() const { return mgp::Properties(mgp::vertex_iter_properties(ptr_, memory)); } - -inline Value Node::operator[](const std::string_view property_name) const { return Properties()[property_name]; } - inline Relationships Node::InRelationships() const { auto relationship_iterator = mgp::vertex_iter_in_edges(ptr_, memory); if (relationship_iterator == nullptr) { @@ -2390,6 +2326,26 @@ inline void Node::AddLabel(const std::string_view label) { mgp::vertex_add_label(this->ptr_, mgp_label{.name = label.data()}); } +inline std::map Node::Properties() const { + mgp_properties_iterator *properties_iterator = mgp::vertex_iter_properties(ptr_, memory); + std::map property_map; + for (auto *property = mgp::properties_iterator_get(properties_iterator); property; + property = mgp::properties_iterator_next(properties_iterator)) { + property_map.emplace(std::string(property->name), Value(property->value)); + } + mgp::properties_iterator_destroy(properties_iterator); + return property_map; +} + +inline void Node::SetProperty(std::string property, Value value) { + mgp::vertex_set_property(ptr_, property.data(), value.ptr()); +} + +inline Value Node::GetProperty(const std::string &property) const { + mgp_value *vertex_prop = mgp::vertex_get_property(ptr_, property.data(), memory); + return Value(steal, vertex_prop); +} + inline bool Node::operator<(const Node &other) const { return Id() < other.Id(); } inline bool Node::operator==(const Node &other) const { return util::NodesEqual(ptr_, other.ptr_); } @@ -2436,12 +2392,24 @@ inline mgp::Id Relationship::Id() const { return Id::FromInt(mgp::edge_get_id(pt inline std::string_view Relationship::Type() const { return mgp::edge_get_type(ptr_).name; } -inline class Properties Relationship::Properties() const { - return mgp::Properties(mgp::edge_iter_properties(ptr_, memory)); +inline std::map Relationship::Properties() const { + mgp_properties_iterator *properties_iterator = mgp::edge_iter_properties(ptr_, memory); + std::map property_map; + for (mgp_property *property = mgp::properties_iterator_get(properties_iterator); property; + property = mgp::properties_iterator_next(properties_iterator)) { + property_map.emplace(property->name, Value(property->value)); + } + mgp::properties_iterator_destroy(properties_iterator); + return property_map; } -inline Value Relationship::operator[](const std::string_view property_name) const { - return Properties()[property_name]; +inline void Relationship::SetProperty(std::string property, Value value) { + mgp::edge_set_property(ptr_, property.data(), value.ptr()); +} + +inline Value Relationship::GetProperty(const std::string &property) const { + mgp_value *edge_prop = mgp::edge_get_property(ptr_, property.data(), memory); + return Value(steal, edge_prop); } inline Node Relationship::From() const { return Node(mgp::edge_get_from(ptr_)); } @@ -2917,6 +2885,7 @@ inline bool Duration::operator<(const Duration &other) const { /* #region Value */ inline Value::Value(mgp_value *ptr) : ptr_(mgp::value_copy(ptr, memory)) {} +inline Value::Value(StealType /*steal*/, mgp_value *ptr) : ptr_{ptr} {} inline Value::Value() : ptr_(mgp::value_make_null(memory)) {} diff --git a/tests/unit/cpp_api.cpp b/tests/unit/cpp_api.cpp index f1344776b..09673b00c 100644 --- a/tests/unit/cpp_api.cpp +++ b/tests/unit/cpp_api.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 @@ -195,7 +195,7 @@ TEST_F(CppApiTestFixture, TestNode) { ASSERT_EQ(node_1.HasLabel("L1"), true); ASSERT_EQ(node_1.HasLabel("L2"), true); - ASSERT_EQ(node_1.Properties().Size(), 0); + ASSERT_EQ(node_1.Properties().size(), 0); auto node_2 = graph.GetNodeById(node_1.Id()); @@ -264,7 +264,7 @@ TEST_F(CppApiTestFixture, TestRelationship) { auto relationship = graph.CreateRelationship(node_1, node_2, "edge_type"); ASSERT_EQ(relationship.Type(), "edge_type"); - ASSERT_EQ(relationship.Properties().Size(), 0); + ASSERT_EQ(relationship.Properties().size(), 0); ASSERT_EQ(relationship.From().Id(), node_1.Id()); ASSERT_EQ(relationship.To().Id(), node_2.Id()); @@ -419,3 +419,19 @@ TEST_F(CppApiTestFixture, TestDuration) { // Use Value move constructor auto value_y = mgp::Value(mgp::Duration("PT2M2.33S")); } + +TEST_F(CppApiTestFixture, TestNodeProperties) { + mgp_graph raw_graph = CreateGraph(memgraph::storage::View::NEW); + auto graph = mgp::Graph(&raw_graph); + + auto node_1 = graph.CreateNode(); + + ASSERT_EQ(node_1.Properties().size(), 0); + + std::map node1_prop = node_1.Properties(); + node_1.SetProperty("b", mgp::Value("b")); + + ASSERT_EQ(node_1.Properties().size(), 1); + ASSERT_EQ(node_1.Properties()["b"].ValueString(), "b"); + ASSERT_EQ(node_1.GetProperty("b").ValueString(), "b"); +} From 8cf51d9f688ec31e7cad8f0781cafe6e91425c22 Mon Sep 17 00:00:00 2001 From: Josipmrden Date: Wed, 25 Jan 2023 12:53:33 +0100 Subject: [PATCH 5/9] Fix bug in query plan to use indexes on optional match and foreach (#736) * Add fix in query plan to use indexes on optional match and foreach --- src/query/plan/rewrite/index_lookup.hpp | 4 +- src/query/plan/rule_based_planner.hpp | 8 +++- tests/unit/query_plan.cpp | 51 ++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/src/query/plan/rewrite/index_lookup.hpp b/src/query/plan/rewrite/index_lookup.hpp index d832645e1..edc61bb09 100644 --- a/src/query/plan/rewrite/index_lookup.hpp +++ b/src/query/plan/rewrite/index_lookup.hpp @@ -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 @@ -444,6 +444,8 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor { bool PreVisit(Foreach &op) override { prev_ops_.push_back(&op); + op.input()->Accept(*this); + RewriteBranch(&op.update_clauses_); return false; } diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp index e95befe7a..fed534583 100644 --- a/src/query/plan/rule_based_planner.hpp +++ b/src/query/plan/rule_based_planner.hpp @@ -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 @@ -174,7 +174,11 @@ class RuleBasedPlanner { input_op = PlanMatching(match_ctx, std::move(input_op)); for (const auto &matching : query_part.optional_matching) { MatchContext opt_ctx{matching, *context.symbol_table, context.bound_symbols}; - auto match_op = PlanMatching(opt_ctx, nullptr); + + std::vector bound_symbols(context_->bound_symbols.begin(), context_->bound_symbols.end()); + auto once_with_symbols = std::make_unique(bound_symbols); + + auto match_op = PlanMatching(opt_ctx, std::move(once_with_symbols)); if (match_op) { input_op = std::make_unique(std::move(input_op), std::move(match_op), opt_ctx.new_symbols); } diff --git a/tests/unit/query_plan.cpp b/tests/unit/query_plan.cpp index 24812b26e..ccd38aa47 100644 --- a/tests/unit/query_plan.cpp +++ b/tests/unit/query_plan.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 @@ -662,6 +662,30 @@ TYPED_TEST(TestPlanner, MatchOptionalMatchWhereReturn) { DeleteListContent(&optional); } +TYPED_TEST(TestPlanner, MatchOptionalMatchNodePropertyWithIndex) { + // Test MATCH (n:Label) OPTIONAL MATCH (m:Label) WHERE n.prop = m.prop RETURN n + AstStorage storage; + FakeDbAccessor dba; + + const auto label_name = "label"; + const auto label = dba.Label(label_name); + const auto property = PROPERTY_PAIR("prop"); + dba.SetIndexCount(label, property.second, 0); + + auto *query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n", label_name))), OPTIONAL_MATCH(PATTERN(NODE("m", label_name))), + WHERE(EQ(PROPERTY_LOOKUP("n", property.second), PROPERTY_LOOKUP("m", property.second))), RETURN("n"))); + + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, storage, symbol_table, query); + + auto m_prop = PROPERTY_LOOKUP("m", property); + std::list optional{new ExpectScanAllByLabelPropertyValue(label, property, m_prop)}; + + CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), ExpectFilter(), ExpectOptional(optional), ExpectProduce()); + DeleteListContent(&optional); +} + TYPED_TEST(TestPlanner, MatchUnwindReturn) { // Test MATCH (n) UNWIND [1,2,3] AS x RETURN n, x AstStorage storage; @@ -1685,5 +1709,30 @@ TYPED_TEST(TestPlanner, Foreach) { QUERY(SINGLE_QUERY(FOREACH(i, {CREATE(PATTERN(NODE("n")))}), FOREACH(j, {CREATE(PATTERN(NODE("n")))}))); CheckPlan(query, storage, ExpectForeach(input, updates), ExpectEmptyResult()); } + + { + // FOREACH with index + // FOREACH (n in [...] | MERGE (v:Label)); + const auto label_name = "label"; + const auto label = dba.Label(label_name); + dba.SetIndexCount(label, 0); + + auto *n = NEXPR("n", IDENT("n")); + auto *query = QUERY(SINGLE_QUERY(FOREACH(n, {MERGE(PATTERN(NODE("v", label_name)))}))); + + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, storage, symbol_table, query); + + std::list on_match{new ExpectScanAllByLabel()}; + std::list on_create{new ExpectCreateNode()}; + + auto create = ExpectMerge(on_match, on_create); + std::list updates{&create}; + std::list input; + CheckPlan(planner.plan(), symbol_table, ExpectForeach(input, updates), ExpectEmptyResult()); + + DeleteListContent(&on_match); + DeleteListContent(&on_create); + } } } // namespace From 034b54cb7222877de1b0f77e4b532e36319a2afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Sa=C4=8Dari=C4=87?= Date: Wed, 25 Jan 2023 15:32:00 +0100 Subject: [PATCH 6/9] Fix bug on all shortest paths with an upper bound (#737) --- src/query/plan/operator.cpp | 43 ++++++++----------- .../features/memgraph_allshortest.feature | 20 +++++++++ 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index debbc36bc..6583a52a3 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.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 @@ -2020,7 +2020,7 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { next_edges_.clear(); traversal_stack_.clear(); - pq_.push({TypedValue(), 0, *start_vertex, std::nullopt}); + expand_from_vertex(*start_vertex, TypedValue(), 0); visited_cost_.emplace(*start_vertex, 0); frame[self_.common_.edge_symbol] = TypedValue::TVector(memory); } @@ -2029,33 +2029,28 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { while (!pq_.empty()) { if (MustAbort(context)) throw HintedAbortError(); - auto [current_weight, current_depth, current_vertex, maybe_directed_edge] = pq_.top(); + const auto [current_weight, current_depth, current_vertex, directed_edge] = pq_.top(); pq_.pop(); + const auto &[current_edge, direction, weight] = directed_edge; + if (expanded_.contains(current_edge)) continue; + expanded_.emplace(current_edge); + // Expand only if what we've just expanded is less than max depth. if (current_depth < upper_bound_) { - if (maybe_directed_edge) { - auto &[current_edge, direction, weight] = *maybe_directed_edge; - if (expanded_.find(current_edge) != expanded_.end()) continue; - expanded_.emplace(current_edge); - } expand_from_vertex(current_vertex, current_weight, current_depth); } - // if current vertex is not starting vertex, maybe_directed_edge will not be nullopt - if (maybe_directed_edge) { - auto &[current_edge, direction, weight] = *maybe_directed_edge; - // Searching for a previous vertex in the expansion - auto prev_vertex = direction == EdgeAtom::Direction::IN ? current_edge.To() : current_edge.From(); + // Searching for a previous vertex in the expansion + auto prev_vertex = direction == EdgeAtom::Direction::IN ? current_edge.To() : current_edge.From(); - // Update the parent - if (next_edges_.find({prev_vertex, current_depth - 1}) == next_edges_.end()) { - utils::pmr::list empty(memory); - next_edges_[{prev_vertex, current_depth - 1}] = std::move(empty); - } - - next_edges_.at({prev_vertex, current_depth - 1}).emplace_back(*maybe_directed_edge); + // Update the parent + if (next_edges_.find({prev_vertex, current_depth - 1}) == next_edges_.end()) { + utils::pmr::list empty(memory); + next_edges_[{prev_vertex, current_depth - 1}] = std::move(empty); } + + next_edges_.at({prev_vertex, current_depth - 1}).emplace_back(directed_edge); } if (start_vertex && next_edges_.find({*start_vertex, 0}) != next_edges_.end()) { @@ -2112,8 +2107,8 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { // Priority queue comparator. Keep lowest weight on top of the queue. class PriorityQueueComparator { public: - bool operator()(const std::tuple> &lhs, - const std::tuple> &rhs) { + bool operator()(const std::tuple &lhs, + const std::tuple &rhs) { const auto &lhs_weight = std::get<0>(lhs); const auto &rhs_weight = std::get<0>(rhs); // Null defines minimum value for all types @@ -2132,8 +2127,8 @@ class ExpandAllShortestPathsCursor : public query::plan::Cursor { // Priority queue - core element of the algorithm. // Stores: {weight, depth, next vertex, edge and direction} - std::priority_queue>, - utils::pmr::vector>>, + std::priority_queue, + utils::pmr::vector>, PriorityQueueComparator> pq_; diff --git a/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature b/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature index 8a8cd76cc..7f224a016 100644 --- a/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature +++ b/tests/gql_behave/tests/memgraph_V1/features/memgraph_allshortest.feature @@ -15,6 +15,26 @@ Feature: All Shortest Path | '1' | | '3' | + Scenario: Test match allShortest upper bound 2 + Given an empty graph + And having executed: + """ + CREATE (a {a:'0'})-[:r {w: 2}]->(b {a:'1'})-[:r {w: 3}]->(c {a:'2'}), + (a)-[:re {w: 2}]->(b), + (b)-[:re {w:3}]->(c), + ({a: '4'})<-[:r {w: 1}]-(a), + ({a: '5'})<-[:r {w: 1}]-(a), + (c)-[:r {w: 1}]->({a: '6'}), + (c)-[:r {w: 1}]->({a: '7'}) + """ + When executing query: + """ + MATCH path=(n {a:'0'})-[r *allShortest ..2 (e, n | 1 ) w]->(m {a:'2'}) RETURN COUNT(path) AS c + """ + Then the result should be: + | c | + | 4 | + Scenario: Test match allShortest filtered Given an empty graph And having executed: From aad4bcb7a0ac026d83b807b75e197608c3910839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Pu=C5=A1i=C4=87?= Date: Wed, 25 Jan 2023 17:23:46 +0100 Subject: [PATCH 7/9] Fix C++ API memory leak on Relationships() (#743) --- include/mgp.hpp | 79 +++++++++++++++++++++++------------------- tests/unit/cpp_api.cpp | 20 +++++++++-- 2 files changed, 61 insertions(+), 38 deletions(-) diff --git a/include/mgp.hpp b/include/mgp.hpp index 29d6fb25e..4d39a7c86 100644 --- a/include/mgp.hpp +++ b/include/mgp.hpp @@ -162,7 +162,7 @@ class Nodes { explicit Iterator(mgp_vertices_iterator *nodes_iterator); - Iterator(const Iterator &other); + Iterator(const Iterator &other) noexcept; Iterator &operator=(const Iterator &other) = delete; ~Iterator(); @@ -210,7 +210,7 @@ class GraphRelationships { explicit Iterator(mgp_vertices_iterator *nodes_iterator); - Iterator(const Iterator &other); + Iterator(const Iterator &other) noexcept; Iterator &operator=(const Iterator &other) = delete; ~Iterator(); @@ -256,7 +256,7 @@ class Relationships { explicit Iterator(mgp_edges_iterator *relationships_iterator); - Iterator(const Iterator &other); + Iterator(const Iterator &other) noexcept; Iterator &operator=(const Iterator &other) = delete; ~Iterator(); @@ -289,7 +289,7 @@ class Labels { public: explicit Labels(mgp_vertex *node_ptr); - Labels(const Labels &other); + Labels(const Labels &other) noexcept; Labels(Labels &&other) noexcept; Labels &operator=(const Labels &other) noexcept; @@ -332,6 +332,7 @@ class Labels { private: mgp_vertex *node_ptr_; }; + /* #endregion */ /* #region Types */ @@ -366,7 +367,7 @@ class List { /// @brief Creates a List from the given initializer_list. explicit List(const std::initializer_list list); - List(const List &other); + List(const List &other) noexcept; List(List &&other) noexcept; List &operator=(const List &other) noexcept; @@ -458,7 +459,7 @@ class Map { /// @brief Creates a Map from the given initializer_list (map items correspond to initializer list pairs). Map(const std::initializer_list> items); - Map(const Map &other); + Map(const Map &other) noexcept; Map(Map &&other) noexcept; Map &operator=(const Map &other) noexcept; @@ -488,7 +489,7 @@ class Map { explicit Iterator(mgp_map_items_iterator *map_items_iterator); - Iterator(const Iterator &other); + Iterator(const Iterator &other) noexcept; Iterator &operator=(const Iterator &other) = delete; ~Iterator(); @@ -547,7 +548,7 @@ class Node { /// @brief Creates a Node from the copy of the given @ref mgp_vertex. explicit Node(const mgp_vertex *const_ptr); - Node(const Node &other); + Node(const Node &other) noexcept; Node(Node &&other) noexcept; Node &operator=(const Node &other) noexcept; @@ -559,16 +560,18 @@ class Node { mgp::Id Id() const; /// @brief Returns an iterable & indexable structure of the node’s labels. - class Labels Labels() const; + mgp::Labels Labels() const; /// @brief Returns whether the node has the given `label`. bool HasLabel(std::string_view label) const; - /// @brief Returns an iterable & indexable structure of the node’s properties. + /// @brief Returns an std::map of the node’s properties. std::map Properties() const; + /// @brief Sets the chosen property to the given value. void SetProperty(std::string property, Value value); + /// @brief Retrieves the value of the chosen property. Value GetProperty(const std::string &property) const; /// @brief Returns an iterable structure of the node’s inbound relationships. @@ -605,7 +608,7 @@ class Relationship { /// @brief Creates a Relationship from the copy of the given @ref mgp_edge. explicit Relationship(const mgp_edge *const_ptr); - Relationship(const Relationship &other); + Relationship(const Relationship &other) noexcept; Relationship(Relationship &&other) noexcept; Relationship &operator=(const Relationship &other) noexcept; @@ -622,8 +625,10 @@ class Relationship { /// @brief Returns an std::map of the relationship’s properties. std::map Properties() const; + /// @brief Sets the chosen property to the given value. void SetProperty(std::string property, Value value); + /// @brief Retrieves the value of the chosen property. Value GetProperty(const std::string &property) const; /// @brief Returns the relationship’s source node. @@ -659,7 +664,7 @@ class Path { /// @brief Creates a Path starting with the given `start_node`. explicit Path(const Node &start_node); - Path(const Path &other); + Path(const Path &other) noexcept; Path(Path &&other) noexcept; Path &operator=(const Path &other) noexcept; @@ -715,7 +720,7 @@ class Date { /// @brief Creates a Date object with the given `year`, `month`, and `day` properties. Date(int year, int month, int day); - Date(const Date &other); + Date(const Date &other) noexcept; Date(Date &&other) noexcept; Date &operator=(const Date &other) noexcept; @@ -770,7 +775,7 @@ class LocalTime { /// properties. LocalTime(int hour, int minute, int second, int millisecond, int microsecond); - LocalTime(const LocalTime &other); + LocalTime(const LocalTime &other) noexcept; LocalTime(LocalTime &&other) noexcept; LocalTime &operator=(const LocalTime &other) noexcept; @@ -829,7 +834,7 @@ class LocalDateTime { /// `millisecond`, and `microsecond` properties. LocalDateTime(int year, int month, int day, int hour, int minute, int second, int millisecond, int microsecond); - LocalDateTime(const LocalDateTime &other); + LocalDateTime(const LocalDateTime &other) noexcept; LocalDateTime(LocalDateTime &&other) noexcept; LocalDateTime &operator=(const LocalDateTime &other) noexcept; @@ -900,7 +905,7 @@ class Duration { /// `microsecond` properties. Duration(double day, double hour, double minute, double second, double millisecond, double microsecond); - Duration(const Duration &other); + Duration(const Duration &other) noexcept; Duration(Duration &&other) noexcept; Duration &operator=(const Duration &other) noexcept; @@ -1029,7 +1034,7 @@ class Value { /// @note The behavior of accessing `duration` after performing this operation is undefined. explicit Value(Duration &&duration); - Value(const Value &other); + Value(const Value &other) noexcept; Value(Value &&other) noexcept; Value &operator=(const Value &other) noexcept; @@ -1677,7 +1682,7 @@ inline Nodes::Iterator::Iterator(mgp_vertices_iterator *nodes_iterator) : nodes_ } } -inline Nodes::Iterator::Iterator(const Iterator &other) : Iterator(other.nodes_iterator_) {} +inline Nodes::Iterator::Iterator(const Iterator &other) noexcept : Iterator(other.nodes_iterator_) {} inline Nodes::Iterator::~Iterator() { if (nodes_iterator_ != nullptr) { @@ -1768,7 +1773,7 @@ inline GraphRelationships::Iterator::Iterator(mgp_vertices_iterator *nodes_itera } } -inline GraphRelationships::Iterator::Iterator(const Iterator &other) : Iterator(other.nodes_iterator_) {} +inline GraphRelationships::Iterator::Iterator(const Iterator &other) noexcept : Iterator(other.nodes_iterator_) {} inline GraphRelationships::Iterator::~Iterator() { if (nodes_iterator_ != nullptr) { @@ -1787,10 +1792,12 @@ inline GraphRelationships::Iterator &GraphRelationships::Iterator::operator++() if (out_relationships_iterator_ != nullptr) { auto next = mgp::edges_iterator_next(out_relationships_iterator_); - if (next == nullptr) { - mgp::edges_iterator_destroy(out_relationships_iterator_); - out_relationships_iterator_ = nullptr; + if (next != nullptr) { + return *this; } + + mgp::edges_iterator_destroy(out_relationships_iterator_); + out_relationships_iterator_ = nullptr; } // 2. Move onto the next nodes @@ -1877,7 +1884,7 @@ inline Relationships::Iterator::Iterator(mgp_edges_iterator *relationships_itera } } -inline Relationships::Iterator::Iterator(const Iterator &other) : Iterator(other.relationships_iterator_) {} +inline Relationships::Iterator::Iterator(const Iterator &other) noexcept : Iterator(other.relationships_iterator_) {} inline Relationships::Iterator::~Iterator() { if (relationships_iterator_ != nullptr) { @@ -1940,7 +1947,7 @@ inline Relationships::Iterator Relationships::cend() const { return Iterator(nul inline Labels::Labels(mgp_vertex *node_ptr) : node_ptr_(mgp::vertex_copy(node_ptr, memory)) {} -inline Labels::Labels(const Labels &other) : Labels(other.node_ptr_) {} +inline Labels::Labels(const Labels &other) noexcept : Labels(other.node_ptr_) {} inline Labels::Labels(Labels &&other) noexcept : node_ptr_(other.node_ptr_) { other.node_ptr_ = nullptr; } @@ -2030,7 +2037,7 @@ inline List::List(const std::initializer_list values) : ptr_(mgp::list_ma } } -inline List::List(const List &other) : List(other.ptr_) {} +inline List::List(const List &other) noexcept : List(other.ptr_) {} inline List::List(List &&other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } @@ -2138,7 +2145,7 @@ inline Map::Map(const std::initializer_list> } } -inline Map::Map(const Map &other) : Map(other.ptr_) {} +inline Map::Map(const Map &other) noexcept : Map(other.ptr_) {} inline Map::Map(Map &&other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } @@ -2183,7 +2190,7 @@ inline Map::Iterator::Iterator(mgp_map_items_iterator *map_items_iterator) : map } } -inline Map::Iterator::Iterator(const Iterator &other) : Iterator(other.map_items_iterator_) {} +inline Map::Iterator::Iterator(const Iterator &other) noexcept : Iterator(other.map_items_iterator_) {} inline Map::Iterator::~Iterator() { if (map_items_iterator_ != nullptr) { @@ -2264,7 +2271,7 @@ inline Node::Node(mgp_vertex *ptr) : ptr_(mgp::vertex_copy(ptr, memory)) {} inline Node::Node(const mgp_vertex *const_ptr) : ptr_(mgp::vertex_copy(const_cast(const_ptr), memory)) {} -inline Node::Node(const Node &other) : Node(other.ptr_) {} +inline Node::Node(const Node &other) noexcept : Node(other.ptr_) {} inline Node::Node(Node &&other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } @@ -2295,7 +2302,7 @@ inline Node::~Node() { inline mgp::Id Node::Id() const { return Id::FromInt(mgp::vertex_get_id(ptr_).as_int); } -inline class Labels Node::Labels() const { return mgp::Labels(ptr_); } +inline mgp::Labels Node::Labels() const { return mgp::Labels(ptr_); } inline bool Node::HasLabel(std::string_view label) const { for (const auto node_label : Labels()) { @@ -2359,7 +2366,7 @@ inline Relationship::Relationship(mgp_edge *ptr) : ptr_(mgp::edge_copy(ptr, memo inline Relationship::Relationship(const mgp_edge *const_ptr) : ptr_(mgp::edge_copy(const_cast(const_ptr), memory)) {} -inline Relationship::Relationship(const Relationship &other) : Relationship(other.ptr_) {} +inline Relationship::Relationship(const Relationship &other) noexcept : Relationship(other.ptr_) {} inline Relationship::Relationship(Relationship &&other) noexcept : ptr_(other.ptr_) { other.ptr_ = nullptr; } @@ -2432,7 +2439,7 @@ inline Path::Path(const mgp_path *const_ptr) : ptr_(mgp::path_copy(const_cast Date: Wed, 25 Jan 2023 18:49:02 +0100 Subject: [PATCH 8/9] Exclude license dir from CI trigger (#740) --- .github/workflows/diff.yaml | 1 + .pre-commit-config.yaml | 2 +- init | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/diff.yaml b/.github/workflows/diff.yaml index 1e7d7e86d..605783495 100644 --- a/.github/workflows/diff.yaml +++ b/.github/workflows/diff.yaml @@ -14,6 +14,7 @@ on: - "**/*.md" - ".clang-format" - "CODEOWNERS" + - "licenses/*" jobs: community_build: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5abd746c9..79bf689df 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - repo: https://github.com/psf/black - rev: 22.10.0 + rev: 22.8.0 hooks: - id: black - repo: https://github.com/pycqa/isort diff --git a/init b/init index 595037ba4..adaafdeff 100755 --- a/init +++ b/init @@ -149,7 +149,7 @@ python3 -m pre_commit install # Install py format tools echo "Install black formatter" -python3 -m pip install black==22.10.* +python3 -m pip install black==22.8.* echo "Install isort" python3 -m pip install isort==5.10.* From 34dd47ef076444240b2323aa1bd84d2fe2c888db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Sa=C4=8Dari=C4=87?= Date: Wed, 25 Jan 2023 20:06:05 +0100 Subject: [PATCH 9/9] Fix nested FOREACH shadowing bug (#725) --- .../frontend/semantic/symbol_generator.cpp | 38 ++++++----------- .../frontend/semantic/symbol_generator.hpp | 4 +- .../memgraph_V1/features/foreach.feature | 42 ++++++------------- tests/unit/query_semantic.cpp | 2 +- 4 files changed, 28 insertions(+), 58 deletions(-) diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp index 0df1f3771..2468d9c73 100644 --- a/src/query/frontend/semantic/symbol_generator.cpp +++ b/src/query/frontend/semantic/symbol_generator.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 @@ -64,14 +64,6 @@ auto SymbolGenerator::CreateSymbol(const std::string &name, bool user_declared, return symbol; } -auto SymbolGenerator::GetOrCreateSymbolLocalScope(const std::string &name, bool user_declared, Symbol::Type type) { - auto &scope = scopes_.back(); - if (auto maybe_symbol = FindSymbolInScope(name, scope, type); maybe_symbol) { - return *maybe_symbol; - } - return CreateSymbol(name, user_declared, type); -} - auto SymbolGenerator::GetOrCreateSymbol(const std::string &name, bool user_declared, Symbol::Type type) { // NOLINTNEXTLINE for (auto scope = scopes_.rbegin(); scope != scopes_.rend(); ++scope) { @@ -206,7 +198,7 @@ bool SymbolGenerator::PreVisit(CallProcedure &call_proc) { bool SymbolGenerator::PostVisit(CallProcedure &call_proc) { for (auto *ident : call_proc.result_identifiers_) { - if (HasSymbolLocalScope(ident->name_)) { + if (HasSymbol(ident->name_)) { throw RedeclareVariableError(ident->name_); } ident->MapTo(CreateSymbol(ident->name_, true)); @@ -217,7 +209,7 @@ bool SymbolGenerator::PostVisit(CallProcedure &call_proc) { bool SymbolGenerator::PreVisit(LoadCsv &load_csv) { return false; } bool SymbolGenerator::PostVisit(LoadCsv &load_csv) { - if (HasSymbolLocalScope(load_csv.row_var_->name_)) { + if (HasSymbol(load_csv.row_var_->name_)) { throw RedeclareVariableError(load_csv.row_var_->name_); } load_csv.row_var_->MapTo(CreateSymbol(load_csv.row_var_->name_, true)); @@ -265,7 +257,7 @@ bool SymbolGenerator::PostVisit(Merge &) { bool SymbolGenerator::PostVisit(Unwind &unwind) { const auto &name = unwind.named_expression_->name_; - if (HasSymbolLocalScope(name)) { + if (HasSymbol(name)) { throw RedeclareVariableError(name); } unwind.named_expression_->MapTo(CreateSymbol(name, true)); @@ -282,7 +274,7 @@ bool SymbolGenerator::PostVisit(Match &) { // Check variables in property maps after visiting Match, so that they can // reference symbols out of bind order. for (auto &ident : scope.identifiers_in_match) { - if (!HasSymbolLocalScope(ident->name_) && !ConsumePredefinedIdentifier(ident->name_)) + if (!HasSymbol(ident->name_) && !ConsumePredefinedIdentifier(ident->name_)) throw UnboundVariableError(ident->name_); ident->MapTo(scope.symbols[ident->name_]); } @@ -314,7 +306,7 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) { if (scope.in_pattern && !(scope.in_node_atom || scope.visiting_edge)) { // If we are in the pattern, and outside of a node or an edge, the // identifier is the pattern name. - symbol = GetOrCreateSymbolLocalScope(ident.name_, ident.user_declared_, Symbol::Type::PATH); + symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_, Symbol::Type::PATH); } else if (scope.in_pattern && scope.in_pattern_atom_identifier) { // Patterns used to create nodes and edges cannot redeclare already // established bindings. Declaration only happens in single node @@ -322,19 +314,19 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) { // `MATCH (n) CREATE (n)` should throw an error that `n` is already // declared. While `MATCH (n) CREATE (n) -[:R]-> (n)` is allowed, // since `n` now references the bound node instead of declaring it. - if ((scope.in_create_node || scope.in_create_edge) && HasSymbolLocalScope(ident.name_)) { + if ((scope.in_create_node || scope.in_create_edge) && HasSymbol(ident.name_)) { throw RedeclareVariableError(ident.name_); } auto type = Symbol::Type::VERTEX; if (scope.visiting_edge) { // Edge referencing is not allowed (like in Neo4j): // `MATCH (n) - [r] -> (n) - [r] -> (n) RETURN r` is not allowed. - if (HasSymbolLocalScope(ident.name_)) { + if (HasSymbol(ident.name_)) { throw RedeclareVariableError(ident.name_); } type = scope.visiting_edge->IsVariable() ? Symbol::Type::EDGE_LIST : Symbol::Type::EDGE; } - symbol = GetOrCreateSymbolLocalScope(ident.name_, ident.user_declared_, type); + symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_, type); } else if (scope.in_pattern && !scope.in_pattern_atom_identifier && scope.in_match) { if (scope.in_edge_range && scope.visiting_edge->identifier_->name_ == ident.name_) { // Prevent variable path bounds to reference the identifier which is bound @@ -461,7 +453,7 @@ bool SymbolGenerator::PreVisit(NodeAtom &node_atom) { auto &scope = scopes_.back(); auto check_node_semantic = [&node_atom, &scope, this](const bool props_or_labels) { const auto &node_name = node_atom.identifier_->name_; - if ((scope.in_create || scope.in_merge) && props_or_labels && HasSymbolLocalScope(node_name)) { + if ((scope.in_create || scope.in_merge) && props_or_labels && HasSymbol(node_name)) { throw SemanticException("Cannot create node '" + node_name + "' with labels or properties, because it is already declared."); } @@ -555,11 +547,11 @@ bool SymbolGenerator::PreVisit(EdgeAtom &edge_atom) { edge_atom.identifier_->Accept(*this); scope.in_pattern_atom_identifier = false; if (edge_atom.total_weight_) { - if (HasSymbolLocalScope(edge_atom.total_weight_->name_)) { + if (HasSymbol(edge_atom.total_weight_->name_)) { throw RedeclareVariableError(edge_atom.total_weight_->name_); } - edge_atom.total_weight_->MapTo(GetOrCreateSymbolLocalScope( - edge_atom.total_weight_->name_, edge_atom.total_weight_->user_declared_, Symbol::Type::NUMBER)); + edge_atom.total_weight_->MapTo(GetOrCreateSymbol(edge_atom.total_weight_->name_, + edge_atom.total_weight_->user_declared_, Symbol::Type::NUMBER)); } return false; } @@ -602,10 +594,6 @@ bool SymbolGenerator::HasSymbol(const std::string &name) const { return std::ranges::any_of(scopes_, [&name](const auto &scope) { return scope.symbols.contains(name); }); } -bool SymbolGenerator::HasSymbolLocalScope(const std::string &name) const { - return scopes_.back().symbols.contains(name); -} - bool SymbolGenerator::ConsumePredefinedIdentifier(const std::string &name) { auto it = predefined_identifiers_.find(name); diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp index 8703fd4e6..80ea9d919 100644 --- a/src/query/frontend/semantic/symbol_generator.hpp +++ b/src/query/frontend/semantic/symbol_generator.hpp @@ -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 @@ -134,7 +134,6 @@ class SymbolGenerator : public HierarchicalTreeVisitor { static std::optional FindSymbolInScope(const std::string &name, const Scope &scope, Symbol::Type type); bool HasSymbol(const std::string &name) const; - bool HasSymbolLocalScope(const std::string &name) const; // @return true if it added a predefined identifier with that name bool ConsumePredefinedIdentifier(const std::string &name); @@ -147,7 +146,6 @@ class SymbolGenerator : public HierarchicalTreeVisitor { auto GetOrCreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY); // Returns the symbol by name. If the mapping already exists, checks if the // types match. Otherwise, returns a new symbol. - auto GetOrCreateSymbolLocalScope(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY); void VisitReturnBody(ReturnBody &body, Where *where = nullptr); diff --git a/tests/gql_behave/tests/memgraph_V1/features/foreach.feature b/tests/gql_behave/tests/memgraph_V1/features/foreach.feature index ce1d9c932..25c55ec78 100644 --- a/tests/gql_behave/tests/memgraph_V1/features/foreach.feature +++ b/tests/gql_behave/tests/memgraph_V1/features/foreach.feature @@ -66,29 +66,13 @@ Feature: Foreach | 4 | And no side effects - Scenario: Foreach shadowing in create - Given an empty graph - And having executed - """ - FOREACH (i IN [1] | FOREACH (j IN [3,4] | CREATE (i {prop: j}))); - """ - When executing query: - """ - MATCH (n) RETURN n.prop - """ - Then the result should be: - | n.prop | - | 3 | - | 4 | - And no side effects - Scenario: Foreach set Given an empty graph And having executed """ CREATE (n1 { marked: false })-[:RELATES]->(n2 { marked: false }) """ - And having executed + And having executed """ MATCH p=(n1)-[*]->(n2) FOREACH (n IN nodes(p) | SET n.marked = true) @@ -110,7 +94,7 @@ Feature: Foreach """ CREATE (n1 { marked: false })-[:RELATES]->(n2 { marked: false }) """ - And having executed + And having executed """ MATCH p=(n1)-[*]->(n2) FOREACH (n IN nodes(p) | REMOVE n.marked) @@ -132,7 +116,7 @@ Feature: Foreach """ CREATE (n1 { marked: false })-[:RELATES]->(n2 { marked: false }) """ - And having executed + And having executed """ MATCH p=(n1)-[*]->(n2) FOREACH (n IN nodes(p) | DETACH delete n) @@ -148,7 +132,7 @@ Feature: Foreach Scenario: Foreach merge Given an empty graph - And having executed + And having executed """ FOREACH (i IN [1, 2, 3] | MERGE (n { age : i })) """ @@ -166,7 +150,7 @@ Feature: Foreach Scenario: Foreach nested Given an empty graph - And having executed + And having executed """ FOREACH (i IN [1, 2, 3] | FOREACH( j IN [1] | CREATE (k { prop : j }))) """ @@ -183,11 +167,11 @@ Feature: Foreach Scenario: Foreach multiple update clauses Given an empty graph - And having executed + And having executed """ CREATE (n1 { marked1: false, marked2: false })-[:RELATES]->(n2 { marked1: false, marked2: false }) """ - And having executed + And having executed """ MATCH p=(n1)-[*]->(n2) FOREACH (n IN nodes(p) | SET n.marked1 = true SET n.marked2 = true) @@ -205,11 +189,11 @@ Feature: Foreach Scenario: Foreach multiple nested update clauses Given an empty graph - And having executed + And having executed """ CREATE (n1 { marked1: false, marked2: false })-[:RELATES]->(n2 { marked1: false, marked2: false }) """ - And having executed + And having executed """ MATCH p=(n1)-[*]->(n2) FOREACH (n IN nodes(p) | FOREACH (j IN [1] | SET n.marked1 = true SET n.marked2 = true)) @@ -227,7 +211,7 @@ Feature: Foreach Scenario: Foreach match foreach return Given an empty graph - And having executed + And having executed """ CREATE (n {prop: [[], [1,2]]}); """ @@ -242,7 +226,7 @@ Feature: Foreach Scenario: Foreach on null value Given an empty graph - And having executed + And having executed """ CREATE (n); """ @@ -254,9 +238,9 @@ Feature: Foreach | | And no side effects - Scenario: Foreach nested merge + Scenario: Foreach nested merge Given an empty graph - And having executed + And having executed """ FOREACH(i in [1, 2, 3] | foreach(j in [1] | MERGE (n { age : i }))); """ diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp index 89e3de946..e0bac0166 100644 --- a/tests/unit/query_semantic.cpp +++ b/tests/unit/query_semantic.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