From d7443a55581d95122414f95ca97c2c2aa82bae2f Mon Sep 17 00:00:00 2001 From: Vlasta <95473291+vpavicic@users.noreply.github.com> Date: Thu, 23 Feb 2023 15:05:03 +0100 Subject: [PATCH 1/7] Update README.md --- README.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/README.md b/README.md index 012f9c62d..1477995db 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,6 @@ --- -

-Build modern, graph-based applications on top of your streaming data in minutes. -

-

license From d79dd696079241aee45cad4a252daa3391be6f4f Mon Sep 17 00:00:00 2001 From: Antonio Filipovic <61245998+antoniofilipovic@users.noreply.github.com> Date: Fri, 24 Feb 2023 15:40:35 +0100 Subject: [PATCH 2/7] Improve performance with props init on node|edge creation (#788) --- src/query/common.hpp | 34 +++++++++- src/query/db_accessor.hpp | 10 ++- src/query/plan/operator.cpp | 19 +++--- src/storage/v2/edge_accessor.cpp | 20 +++++- src/storage/v2/edge_accessor.hpp | 7 ++- src/storage/v2/property_store.cpp | 62 ++++++++++++++++++- src/storage/v2/property_store.hpp | 8 ++- src/storage/v2/vertex_accessor.cpp | 19 +++++- src/storage/v2/vertex_accessor.hpp | 7 ++- .../memgraph_V1/features/functions.feature | 4 +- tests/unit/storage_v2_property_store.cpp | 25 +++++++- 11 files changed, 195 insertions(+), 20 deletions(-) diff --git a/src/query/common.hpp b/src/query/common.hpp index c51c34dee..6154900b5 100644 --- a/src/query/common.hpp +++ b/src/query/common.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 @@ -107,5 +107,37 @@ storage::PropertyValue PropsSetChecked(T *record, const storage::PropertyId &key } } +template +concept AccessorWithInitProperties = requires(T accessor, + const std::map &properties) { + { accessor.InitProperties(properties) } -> std::same_as>; +}; + +/// Set property `values` mapped with given `key` on a `record`. +/// +/// @throw QueryRuntimeException if value cannot be set as a property value +template +bool MultiPropsInitChecked(T *record, std::map &properties) { + try { + auto maybe_values = record->InitProperties(properties); + if (maybe_values.HasError()) { + switch (maybe_values.GetError()) { + case storage::Error::SERIALIZATION_ERROR: + throw TransactionSerializationException(); + case storage::Error::DELETED_OBJECT: + throw QueryRuntimeException("Trying to set properties on a deleted object."); + case storage::Error::PROPERTIES_DISABLED: + throw QueryRuntimeException("Can't set property because properties on edges are disabled."); + case storage::Error::VERTEX_HAS_EDGES: + case storage::Error::NONEXISTENT_OBJECT: + throw QueryRuntimeException("Unexpected error when setting a property."); + } + } + return std::move(*maybe_values); + } catch (const TypedValueException &) { + throw QueryRuntimeException("Cannot set properties."); + } +} + int64_t QueryTimestamp(); } // namespace memgraph::query diff --git a/src/query/db_accessor.hpp b/src/query/db_accessor.hpp index 2c269a951..91c2ec721 100644 --- a/src/query/db_accessor.hpp +++ b/src/query/db_accessor.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 @@ -71,6 +71,10 @@ class EdgeAccessor final { return impl_.SetProperty(key, value); } + storage::Result InitProperties(const std::map &properties) { + return impl_.InitProperties(properties); + } + storage::Result RemoveProperty(storage::PropertyId key) { return SetProperty(key, storage::PropertyValue()); } @@ -125,6 +129,10 @@ class VertexAccessor final { return impl_.SetProperty(key, value); } + storage::Result InitProperties(const std::map &properties) { + return impl_.InitProperties(properties); + } + storage::Result RemoveProperty(storage::PropertyId key) { return SetProperty(key, storage::PropertyValue()); } diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 645b0a463..846545721 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -25,6 +25,7 @@ #include #include +#include "query/common.hpp" #include "spdlog/spdlog.h" #include "license/license.hpp" @@ -208,17 +209,18 @@ VertexAccessor &CreateLocalVertex(const NodeCreationInfo &node_info, Frame *fram storage::View::NEW); // TODO: PropsSetChecked allocates a PropertyValue, make it use context.memory // when we update PropertyValue with custom allocator. + std::map properties; if (const auto *node_info_properties = std::get_if(&node_info.properties)) { for (const auto &[key, value_expression] : *node_info_properties) { - PropsSetChecked(&new_node, key, value_expression->Accept(evaluator)); + properties.emplace(key, value_expression->Accept(evaluator)); } } else { auto property_map = evaluator.Visit(*std::get(node_info.properties)); for (const auto &[key, value] : property_map.ValueMap()) { - auto property_id = dba.NameToProperty(key); - PropsSetChecked(&new_node, property_id, value); + properties.emplace(dba.NameToProperty(key), value); } } + MultiPropsInitChecked(&new_node, properties); (*frame)[node_info.symbol] = new_node; return (*frame)[node_info.symbol].ValueVertex(); @@ -299,17 +301,18 @@ EdgeAccessor CreateEdge(const EdgeCreationInfo &edge_info, DbAccessor *dba, Vert auto maybe_edge = dba->InsertEdge(from, to, edge_info.edge_type); if (maybe_edge.HasValue()) { auto &edge = *maybe_edge; - if (const auto *properties = std::get_if(&edge_info.properties)) { - for (const auto &[key, value_expression] : *properties) { - PropsSetChecked(&edge, key, value_expression->Accept(*evaluator)); + std::map properties; + if (const auto *edge_info_properties = std::get_if(&edge_info.properties)) { + for (const auto &[key, value_expression] : *edge_info_properties) { + properties.emplace(key, value_expression->Accept(*evaluator)); } } else { auto property_map = evaluator->Visit(*std::get(edge_info.properties)); for (const auto &[key, value] : property_map.ValueMap()) { - auto property_id = dba->NameToProperty(key); - PropsSetChecked(&edge, property_id, value); + properties.emplace(dba->NameToProperty(key), value); } } + if (!properties.empty()) MultiPropsInitChecked(&edge, properties); (*frame)[edge_info.symbol] = edge; } else { diff --git a/src/storage/v2/edge_accessor.cpp b/src/storage/v2/edge_accessor.cpp index 2a15292a2..abfb67ff7 100644 --- a/src/storage/v2/edge_accessor.cpp +++ b/src/storage/v2/edge_accessor.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 @@ -123,6 +123,24 @@ Result EdgeAccessor::SetProperty(PropertyId property, co return std::move(current_value); } +Result EdgeAccessor::InitProperties(const std::map &properties) { + utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception; + if (!config_.properties_on_edges) return Error::PROPERTIES_DISABLED; + + std::lock_guard guard(edge_.ptr->lock); + + if (!PrepareForWrite(transaction_, edge_.ptr)) return Error::SERIALIZATION_ERROR; + + if (edge_.ptr->deleted) return Error::DELETED_OBJECT; + + if (!edge_.ptr->properties.InitProperties(properties)) return false; + for (const auto &[property, _] : properties) { + CreateAndLinkDelta(transaction_, edge_.ptr, Delta::SetPropertyTag(), property, PropertyValue()); + } + + return true; +} + Result> EdgeAccessor::ClearProperties() { if (!config_.properties_on_edges) return Error::PROPERTIES_DISABLED; diff --git a/src/storage/v2/edge_accessor.hpp b/src/storage/v2/edge_accessor.hpp index b0a1e1151..ee8176365 100644 --- a/src/storage/v2/edge_accessor.hpp +++ b/src/storage/v2/edge_accessor.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 @@ -58,6 +58,11 @@ class EdgeAccessor final { /// @throw std::bad_alloc Result SetProperty(PropertyId property, const PropertyValue &value); + /// Set property values only if property store is empty. Returns `true` if successully set all values, + /// `false` otherwise. + /// @throw std::bad_alloc + Result InitProperties(const std::map &properties); + /// Remove all properties and return old values for each removed property. /// @throw std::bad_alloc Result> ClearProperties(); diff --git a/src/storage/v2/property_store.cpp b/src/storage/v2/property_store.cpp index db8982b6f..f689aa844 100644 --- a/src/storage/v2/property_store.cpp +++ b/src/storage/v2/property_store.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 @@ -1053,7 +1053,7 @@ bool PropertyStore::SetProperty(PropertyId property, const PropertyValue &value) in_local_buffer = true; } else { // Allocate a new external buffer. - auto alloc_data = new uint8_t[property_size_to_power_of_8]; + auto *alloc_data = new uint8_t[property_size_to_power_of_8]; auto alloc_size = property_size_to_power_of_8; SetSizeData(buffer_, alloc_size, alloc_data); @@ -1144,6 +1144,64 @@ bool PropertyStore::SetProperty(PropertyId property, const PropertyValue &value) return !existed; } +bool PropertyStore::InitProperties(const std::map &properties) { + uint64_t size = 0; + uint8_t *data = nullptr; + std::tie(size, data) = GetSizeData(buffer_); + if (size != 0) { + return false; + } + + uint64_t property_size = 0; + { + Writer writer; + for (const auto &[property, value] : properties) { + if (value.IsNull()) { + continue; + } + EncodeProperty(&writer, property, value); + property_size = writer.Written(); + } + } + + auto property_size_to_power_of_8 = ToPowerOf8(property_size); + if (property_size <= sizeof(buffer_) - 1) { + // Use the local buffer. + buffer_[0] = kUseLocalBuffer; + size = sizeof(buffer_) - 1; + data = &buffer_[1]; + } else { + // Allocate a new external buffer. + auto *alloc_data = new uint8_t[property_size_to_power_of_8]; + auto alloc_size = property_size_to_power_of_8; + + SetSizeData(buffer_, alloc_size, alloc_data); + + size = alloc_size; + data = alloc_data; + } + + // Encode the property into the data buffer. + Writer writer(data, size); + + for (const auto &[property, value] : properties) { + if (value.IsNull()) { + continue; + } + MG_ASSERT(EncodeProperty(&writer, property, value), "Invalid database state!"); + writer.Written(); + } + + auto metadata = writer.WriteMetadata(); + if (metadata) { + // If there is any space left in the buffer we add a tombstone to + // indicate that there are no more properties to be decoded. + metadata->Set({Type::EMPTY}); + } + + return true; +} + bool PropertyStore::ClearProperties() { bool in_local_buffer = false; uint64_t size; diff --git a/src/storage/v2/property_store.hpp b/src/storage/v2/property_store.hpp index bd397285f..f0be30df7 100644 --- a/src/storage/v2/property_store.hpp +++ b/src/storage/v2/property_store.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 @@ -59,6 +59,12 @@ class PropertyStore { /// @throw std::bad_alloc bool SetProperty(PropertyId property, const PropertyValue &value); + /// Init property values and return `true` if insertion took place. `false` is + /// returned if there exists property in property store and insertion couldn't take place. The time complexity of this + /// function is O(n). + /// @throw std::bad_alloc + bool InitProperties(const std::map &properties); + /// Remove all properties and return `true` if any removal took place. /// `false` is returned if there were no properties to remove. The time /// complexity of this function is O(1). diff --git a/src/storage/v2/vertex_accessor.cpp b/src/storage/v2/vertex_accessor.cpp index 05ba1ebcc..3b76080cb 100644 --- a/src/storage/v2/vertex_accessor.cpp +++ b/src/storage/v2/vertex_accessor.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 @@ -230,6 +230,23 @@ Result VertexAccessor::SetProperty(PropertyId property, const Pro return std::move(current_value); } +Result VertexAccessor::InitProperties(const std::map &properties) { + utils::MemoryTracker::OutOfMemoryExceptionEnabler oom_exception; + std::lock_guard guard(vertex_->lock); + + if (!PrepareForWrite(transaction_, vertex_)) return Error::SERIALIZATION_ERROR; + + if (vertex_->deleted) return Error::DELETED_OBJECT; + + if (!vertex_->properties.InitProperties(properties)) return false; + for (const auto &[property, value] : properties) { + CreateAndLinkDelta(transaction_, vertex_, Delta::SetPropertyTag(), property, PropertyValue()); + UpdateOnSetProperty(indices_, property, value, vertex_, *transaction_); + } + + return true; +} + Result> VertexAccessor::ClearProperties() { std::lock_guard guard(vertex_->lock); diff --git a/src/storage/v2/vertex_accessor.hpp b/src/storage/v2/vertex_accessor.hpp index 840eec910..79a391708 100644 --- a/src/storage/v2/vertex_accessor.hpp +++ b/src/storage/v2/vertex_accessor.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 @@ -68,6 +68,11 @@ class VertexAccessor final { /// @throw std::bad_alloc Result SetProperty(PropertyId property, const PropertyValue &value); + /// Set property values only if property store is empty. Returns `true` if successully set all values, + /// `false` otherwise. + /// @throw std::bad_alloc + Result InitProperties(const std::map &properties); + /// Remove all properties and return the values of the removed properties. /// @throw std::bad_alloc Result> ClearProperties(); diff --git a/tests/gql_behave/tests/memgraph_V1/features/functions.feature b/tests/gql_behave/tests/memgraph_V1/features/functions.feature index 6df7e2e0c..169722f88 100644 --- a/tests/gql_behave/tests/memgraph_V1/features/functions.feature +++ b/tests/gql_behave/tests/memgraph_V1/features/functions.feature @@ -585,12 +585,12 @@ Feature: Functions """ When executing query: """ - MATCH ()-[r]->() RETURN PROPERTIES(r) AS p + MATCH ()-[r]->() RETURN PROPERTIES(r) AS p ORDER BY p.prop; """ Then the result should be: | p | - | {b: true} | | {} | + | {b: true} | | {c: 123} | Scenario: Properties test2: diff --git a/tests/unit/storage_v2_property_store.cpp b/tests/unit/storage_v2_property_store.cpp index e247806d8..bf2a8f0a9 100644 --- a/tests/unit/storage_v2_property_store.cpp +++ b/tests/unit/storage_v2_property_store.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 @@ -649,3 +649,26 @@ TEST(PropertyStore, IsPropertyEqualTemporalData) { ASSERT_FALSE(props.IsPropertyEqual(prop, memgraph::storage::PropertyValue(memgraph::storage::TemporalData{ memgraph::storage::TemporalType::Date, 30}))); } + +TEST(PropertyStore, SetMultipleProperties) { + memgraph::storage::PropertyStore store; + std::vector vec{memgraph::storage::PropertyValue(true), + memgraph::storage::PropertyValue(123), + memgraph::storage::PropertyValue()}; + std::map map{{"nandare", memgraph::storage::PropertyValue(false)}}; + const memgraph::storage::TemporalData temporal{memgraph::storage::TemporalType::LocalDateTime, 23}; + std::map data{ + {memgraph::storage::PropertyId::FromInt(1), memgraph::storage::PropertyValue(true)}, + {memgraph::storage::PropertyId::FromInt(2), memgraph::storage::PropertyValue(123)}, + {memgraph::storage::PropertyId::FromInt(3), memgraph::storage::PropertyValue(123.5)}, + {memgraph::storage::PropertyId::FromInt(4), memgraph::storage::PropertyValue("nandare")}, + {memgraph::storage::PropertyId::FromInt(5), memgraph::storage::PropertyValue(vec)}, + {memgraph::storage::PropertyId::FromInt(6), memgraph::storage::PropertyValue(map)}, + {memgraph::storage::PropertyId::FromInt(7), memgraph::storage::PropertyValue(temporal)}}; + + store.InitProperties(data); + + for (auto &[key, value] : data) { + ASSERT_TRUE(store.IsPropertyEqual(key, value)); + } +} From eead24a56238692970420cea2ca3763dd9a7f056 Mon Sep 17 00:00:00 2001 From: Kruno Golubic <46486712+kgolubic@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:08:07 +0100 Subject: [PATCH 3/7] Add Lima badge to README (#802) --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1477995db..9b2c5452a 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,7 @@ your browser. ### macOS [![macOS](https://img.shields.io/badge/macOS-Docker-000000?style=for-the-badge&logo=macos&logoColor=F0F0F0)](https://memgraph.com/docs/memgraph/install-memgraph-on-macos-docker) +[![macOS](https://img.shields.io/badge/lima-AACF41?style=for-the-badge&logo=macos&logoColor=F0F0F0)](https://memgraph.com/docs/memgraph/install-memgraph-on-ubuntu) ### Linux From 362dc95e27d792f23ad5b09789f50c1e1e5f0df2 Mon Sep 17 00:00:00 2001 From: Jure Bajic Date: Wed, 1 Mar 2023 18:44:56 +0100 Subject: [PATCH 4/7] Add support for Ubuntu 22.04 ARM (#810) --- .github/workflows/package_all.yaml | 27 ++++-- environment/os/ubuntu-22.04-arm.sh | 96 +++++++++++++++++++++ environment/toolchain/v4.sh | 20 +++-- environment/util.sh | 5 +- release/package/arm-builders.yml | 11 +++ release/package/run.sh | 4 +- release/package/ubuntu-22.04-arm/Dockerfile | 17 ++++ 7 files changed, 166 insertions(+), 14 deletions(-) create mode 100755 environment/os/ubuntu-22.04-arm.sh create mode 100644 release/package/arm-builders.yml create mode 100644 release/package/ubuntu-22.04-arm/Dockerfile diff --git a/.github/workflows/package_all.yaml b/.github/workflows/package_all.yaml index 6e1196e32..b0ef18fde 100644 --- a/.github/workflows/package_all.yaml +++ b/.github/workflows/package_all.yaml @@ -160,6 +160,23 @@ jobs: name: debian-11-platform path: build/output/debian-11/memgraph*.deb + fedora-36: + runs-on: [self-hosted, DockerMgBuild, X64] + timeout-minutes: 60 + steps: + - name: "Set up repository" + uses: actions/checkout@v3 + with: + fetch-depth: 0 # Required because of release/get_version.py + - name: "Build package" + run: | + ./release/package/run.sh package fedora-36 + - name: "Upload package" + uses: actions/upload-artifact@v3 + with: + name: fedora-36 + path: build/output/fedora-36/memgraph*.rpm + debian-11-arm: runs-on: [self-hosted, DockerMgBuild, ARM64, strange] timeout-minutes: 60 @@ -177,8 +194,8 @@ jobs: name: debian-11-arm path: build/output/debian-11-arm/memgraph*.deb - fedora-36: - runs-on: [self-hosted, DockerMgBuild, X64] + ubuntu-2204-arm: + runs-on: [self-hosted, DockerMgBuild, ARM64, strange] timeout-minutes: 60 steps: - name: "Set up repository" @@ -187,9 +204,9 @@ jobs: fetch-depth: 0 # Required because of release/get_version.py - name: "Build package" run: | - ./release/package/run.sh package fedora-36 + ./release/package/run.sh package ubuntu-22.04-arm - name: "Upload package" uses: actions/upload-artifact@v3 with: - name: fedora-36 - path: build/output/fedora-36/memgraph*.rpm + name: ubuntu-22.04-arm + path: build/output/ubuntu-22.04-arm/memgraph*.deb diff --git a/environment/os/ubuntu-22.04-arm.sh b/environment/os/ubuntu-22.04-arm.sh new file mode 100755 index 000000000..d3bf8f040 --- /dev/null +++ b/environment/os/ubuntu-22.04-arm.sh @@ -0,0 +1,96 @@ +#!/bin/bash + +set -Eeuo pipefail + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +source "$DIR/../util.sh" + +check_operating_system "ubuntu-22.04" +check_architecture "arm64" "aarch64" + +TOOLCHAIN_BUILD_DEPS=( + coreutils gcc g++ build-essential make # generic build tools + wget # used for archive download + gnupg # used for archive signature verification + tar gzip bzip2 xz-utils unzip # used for archive unpacking + zlib1g-dev # zlib library used for all builds + libexpat1-dev libbabeltrace-dev liblzma-dev python3-dev texinfo # for gdb + libcurl4-openssl-dev # for cmake + libreadline-dev # for cmake and llvm + libffi-dev libxml2-dev # for llvm + curl # snappy + file + git # for thrift + libgmp-dev # for gdb + gperf # for proxygen + libssl-dev + libedit-dev libpcre3-dev automake bison # for swig +) + +TOOLCHAIN_RUN_DEPS=( + make # generic build tools + tar gzip bzip2 xz-utils # used for archive unpacking + zlib1g # zlib library used for all builds + libexpat1 libbabeltrace1 liblzma5 python3 # for gdb + libcurl4 # for cmake + libreadline8 # for cmake and llvm + libffi7 libxml2 # for llvm + libssl-dev # for libevent +) + +MEMGRAPH_BUILD_DEPS=( + git # source code control + make pkg-config # build system + curl wget # for downloading libs + uuid-dev default-jre-headless # required by antlr + libreadline-dev # for memgraph console + libpython3-dev python3-dev # for query modules + libssl-dev + libseccomp-dev + netcat # tests are using nc to wait for memgraph + python3 python3-virtualenv python3-pip # for qa, macro_benchmark and stress tests + python3-yaml # for the configuration generator + libcurl4-openssl-dev # mg-requests + sbcl # for custom Lisp C++ preprocessing + doxygen graphviz # source documentation generators + mono-runtime mono-mcs zip unzip default-jdk-headless # for driver tests + dotnet-sdk-6.0 golang nodejs npm + autoconf # for jemalloc code generation + libtool # for protobuf code generation +) + +list() { + echo "$1" +} + +check() { + check_all_dpkg "$1" +} + +install() { + cd "$DIR" + apt update + # If GitHub Actions runner is installed, append LANG to the environment. + # Python related tests doesn't work the LANG export. + if [ -d "/home/gh/actions-runner" ]; then + echo "LANG=en_US.utf8" >> /home/gh/actions-runner/.env + else + echo "NOTE: export LANG=en_US.utf8" + fi + apt install -y wget + for pkg in $1; do + if [ "$pkg" == dotnet-sdk-6.0 ]; then + if ! dpkg -s dotnet-sdk-6.0 2>/dev/null >/dev/null; then + wget -nv https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb + dpkg -i packages-microsoft-prod.deb + apt-get update + apt-get install -y apt-transport-https dotnet-sdk-6.0 + fi + continue + fi + apt install -y "$pkg" + done +} + +deps=$2"[*]" +"$1" "${!deps}" diff --git a/environment/toolchain/v4.sh b/environment/toolchain/v4.sh index 9dd997ad4..e3fddaa1f 100755 --- a/environment/toolchain/v4.sh +++ b/environment/toolchain/v4.sh @@ -51,11 +51,19 @@ CPPCHECK_VERSION=2.6 LLVM_VERSION=13.0.0 SWIG_VERSION=4.0.2 # used only for LLVM compilation -# Check for the dependencies. -echo "ALL BUILD PACKAGES: $($DIR/../os/$DISTRO.sh list TOOLCHAIN_BUILD_DEPS)" -$DIR/../os/$DISTRO.sh check TOOLCHAIN_BUILD_DEPS -echo "ALL RUN PACKAGES: $($DIR/../os/$DISTRO.sh list TOOLCHAIN_RUN_DEPS)" -$DIR/../os/$DISTRO.sh check TOOLCHAIN_RUN_DEPS +# Set the right env script +ENV_SCRIPT="$DIR/../os/$DISTRO.sh" +if [[ "$for_arm" = true ]]; then + ENV_SCRIPT="$DIR/../os/$DISTRO-arm.sh" +fi + +# Check for the toolchain build dependencies. +echo "ALL BUILD PACKAGES: $(${ENV_SCRIPT} list TOOLCHAIN_BUILD_DEPS)" +${ENV_SCRIPT} check TOOLCHAIN_BUILD_DEPS + +# Check for the toolchain run dependencies. +echo "ALL RUN PACKAGES: $(${ENV_SCRIPT} list TOOLCHAIN_RUN_DEPS)" +${ENV_SCRIPT} check TOOLCHAIN_RUN_DEPS # check installation directory NAME=toolchain-v$TOOLCHAIN_VERSION @@ -622,7 +630,7 @@ In order to be able to run all of these tools you should install the following packages: \`\`\` -$($DIR/../os/$DISTRO.sh list TOOLCHAIN_RUN_DEPS) +$($DIR/../os/$ENV_SCRIPT.sh list TOOLCHAIN_RUN_DEPS) \`\`\` ## Usage diff --git a/environment/util.sh b/environment/util.sh index f1f215939..e2ecf67cf 100644 --- a/environment/util.sh +++ b/environment/util.sh @@ -19,13 +19,16 @@ function architecture() { } check_architecture() { + local ARCH=$(architecture) for arch in "$@"; do - if [ "$(architecture)" = "$arch" ]; then + if [ "${ARCH}" = "$arch" ]; then echo "The right architecture!" return 0 fi done echo "Not the right architecture!" + echo "Expected: $@" + echo "Actual: ${ARCH}" exit 1 } diff --git a/release/package/arm-builders.yml b/release/package/arm-builders.yml new file mode 100644 index 000000000..d52f3bb26 --- /dev/null +++ b/release/package/arm-builders.yml @@ -0,0 +1,11 @@ +version: "3" + +services: + debian-11-arm: + build: + context: debian-11-arm + container_name: "mgbuild_debian-11-arm" + ubuntu-2204-arm: + build: + context: ubuntu-22.04-arm + container_name: "mgbuild_ubuntu-22.04-arm" diff --git a/release/package/run.sh b/release/package/run.sh index 680e7a4db..7d068eaa9 100755 --- a/release/package/run.sh +++ b/release/package/run.sh @@ -3,7 +3,7 @@ set -Eeuo pipefail SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -SUPPORTED_OS=(centos-7 centos-9 debian-10 debian-11 ubuntu-18.04 ubuntu-20.04 ubuntu-22.04 debian-11-arm fedora-36) +SUPPORTED_OS=(centos-7 centos-9 debian-10 debian-11 ubuntu-18.04 ubuntu-20.04 ubuntu-22.04 debian-11-arm fedora-36 ubuntu-22.04-arm) PROJECT_ROOT="$SCRIPT_DIR/../.." TOOLCHAIN_VERSION="toolchain-v4" ACTIVATE_TOOLCHAIN="source /opt/${TOOLCHAIN_VERSION}/activate" @@ -76,7 +76,7 @@ make_package () { docker exec "$build_container" bash -c "cd /memgraph && git config --global --add safe.directory '*'" docker exec "$build_container" bash -c "cd /memgraph && $ACTIVATE_TOOLCHAIN && ./init" docker exec "$build_container" bash -c "cd $container_build_dir && rm -rf ./*" - if [[ "$os" == "debian-11-arm" ]]; then + if [[ "$os" =~ "-arm" ]]; then docker exec "$build_container" bash -c "cd $container_build_dir && $ACTIVATE_TOOLCHAIN && cmake -DCMAKE_BUILD_TYPE=release -DMG_ARCH="ARM64" $telemetry_id_override_flag .." else docker exec "$build_container" bash -c "cd $container_build_dir && $ACTIVATE_TOOLCHAIN && cmake -DCMAKE_BUILD_TYPE=release $telemetry_id_override_flag .." diff --git a/release/package/ubuntu-22.04-arm/Dockerfile b/release/package/ubuntu-22.04-arm/Dockerfile new file mode 100644 index 000000000..5ddb94883 --- /dev/null +++ b/release/package/ubuntu-22.04-arm/Dockerfile @@ -0,0 +1,17 @@ +FROM ubuntu:22.04 + +ARG TOOLCHAIN_VERSION + +# Stops tzdata interactive configuration. +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt update && apt install -y \ + ca-certificates wget git +# Do NOT be smart here and clean the cache because the container is used in the +# stateful context. + +RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-arm64.tar.gz \ + -O ${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-arm64.tar.gz \ + && tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-arm64.tar.gz -C /opt + +ENTRYPOINT ["sleep", "infinity"] From 173f5430aaf1862d641167777f24f101193e5728 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ante=20Pu=C5=A1i=C4=87?= Date: Mon, 6 Mar 2023 17:34:34 +0100 Subject: [PATCH 5/7] Remove noexcept from functions that may throw (#819) --- src/query/procedure/mg_procedure_impl.hpp | 15 +++++++-------- tests/unit/cpp_api.cpp | 4 ++++ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/query/procedure/mg_procedure_impl.hpp b/src/query/procedure/mg_procedure_impl.hpp index 4d2acb77d..81765c4af 100644 --- a/src/query/procedure/mg_procedure_impl.hpp +++ b/src/query/procedure/mg_procedure_impl.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 @@ -146,10 +146,10 @@ struct mgp_date { mgp_date(const memgraph::utils::Date &date, memgraph::utils::MemoryResource *memory) noexcept : memory(memory), date(date) {} - mgp_date(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept + mgp_date(const std::string_view string, memgraph::utils::MemoryResource *memory) : memory(memory), date(memgraph::utils::ParseDateParameters(string).first) {} - mgp_date(const mgp_date_parameters *parameters, memgraph::utils::MemoryResource *memory) noexcept + mgp_date(const mgp_date_parameters *parameters, memgraph::utils::MemoryResource *memory) : memory(memory), date(MapDateParameters(parameters)) {} mgp_date(const int64_t microseconds, memgraph::utils::MemoryResource *memory) noexcept @@ -194,10 +194,10 @@ struct mgp_local_time { // have everything noexcept here. static_assert(std::is_nothrow_copy_constructible_v); - mgp_local_time(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept + mgp_local_time(const std::string_view string, memgraph::utils::MemoryResource *memory) : memory(memory), local_time(memgraph::utils::ParseLocalTimeParameters(string).first) {} - mgp_local_time(const mgp_local_time_parameters *parameters, memgraph::utils::MemoryResource *memory) noexcept + mgp_local_time(const mgp_local_time_parameters *parameters, memgraph::utils::MemoryResource *memory) : memory(memory), local_time(MapLocalTimeParameters(parameters)) {} mgp_local_time(const memgraph::utils::LocalTime &local_time, memgraph::utils::MemoryResource *memory) noexcept @@ -250,8 +250,7 @@ struct mgp_local_date_time { mgp_local_date_time(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept : memory(memory), local_date_time(CreateLocalDateTimeFromString(string)) {} - mgp_local_date_time(const mgp_local_date_time_parameters *parameters, - memgraph::utils::MemoryResource *memory) noexcept + mgp_local_date_time(const mgp_local_date_time_parameters *parameters, memgraph::utils::MemoryResource *memory) : memory(memory), local_date_time(MapDateParameters(parameters->date_parameters), MapLocalTimeParameters(parameters->local_time_parameters)) {} @@ -301,7 +300,7 @@ struct mgp_duration { // have everything noexcept here. static_assert(std::is_nothrow_copy_constructible_v); - mgp_duration(const std::string_view string, memgraph::utils::MemoryResource *memory) noexcept + mgp_duration(const std::string_view string, memgraph::utils::MemoryResource *memory) : memory(memory), duration(memgraph::utils::ParseDurationParameters(string)) {} mgp_duration(const mgp_duration_parameters *parameters, memgraph::utils::MemoryResource *memory) noexcept diff --git a/tests/unit/cpp_api.cpp b/tests/unit/cpp_api.cpp index 53bd8f03e..f2e47f03e 100644 --- a/tests/unit/cpp_api.cpp +++ b/tests/unit/cpp_api.cpp @@ -389,6 +389,10 @@ TEST_F(CppApiTestFixture, TestLocalDateTime) { auto ldt_1 = mgp::LocalDateTime("2021-10-05T14:15:00"); auto ldt_2 = mgp::LocalDateTime(2021, 10, 5, 14, 15, 0, 0, 0); + ASSERT_ANY_THROW(mgp::LocalDateTime( + 2021, 10, 0, 14, 15, 0, 0, + 0)); // ...10, 0, 14... <- 0 is an illegal value for the `day` parameter; must throw an exception + ASSERT_EQ(ldt_1.Year(), 2021); ASSERT_EQ(ldt_1.Month(), 10); ASSERT_EQ(ldt_1.Day(), 5); From 99a6c72bba6df1ddc77b49bb92ad594117fd968e Mon Sep 17 00:00:00 2001 From: Vlasta <95473291+vpavicic@users.noreply.github.com> Date: Mon, 6 Mar 2023 20:01:02 +0100 Subject: [PATCH 6/7] Change message on incompatible epoch_id error (#786) --- src/storage/v2/replication/replication_client.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/storage/v2/replication/replication_client.cpp b/src/storage/v2/replication/replication_client.cpp index c55d68b75..28afb9e12 100644 --- a/src/storage/v2/replication/replication_client.cpp +++ b/src/storage/v2/replication/replication_client.cpp @@ -109,9 +109,11 @@ void Storage::ReplicationClient::InitializeClient() { } if (branching_point) { spdlog::error( - "Replica {} cannot be used with this instance. Please start a clean " - "instance of Memgraph server on the specified endpoint.", - name_); + "You cannot register Replica {} to this Main because at one point " + "Replica {} acted as the Main instance. Both the Main and Replica {} " + "now hold unique data. Please resolve data conflicts and start the " + "replication on a clean instance.", + name_, name_, name_); return; } From 6abd356d0193d02ce70de8bc4f8dabe9a40cd861 Mon Sep 17 00:00:00 2001 From: Josipmrden Date: Tue, 7 Mar 2023 00:28:41 +0100 Subject: [PATCH 7/7] [master < E214] WHERE Exists feature (#818) Add WHERE exists() to filter based on neighbouring pattern expressions --- src/query/frontend/ast/ast.lcp | 36 ++ src/query/frontend/ast/ast_visitor.hpp | 7 +- .../frontend/ast/cypher_main_visitor.cpp | 16 +- .../frontend/ast/cypher_main_visitor.hpp | 7 +- src/query/frontend/ast/pretty_print.cpp | 5 +- .../frontend/opencypher/grammar/Cypher.g4 | 3 + .../frontend/semantic/symbol_generator.cpp | 57 +- .../frontend/semantic/symbol_generator.hpp | 9 + src/query/frontend/semantic/symbol_table.hpp | 3 +- src/query/interpret/eval.hpp | 3 + src/query/plan/operator.cpp | 71 ++- src/query/plan/operator.lcp | 47 +- src/query/plan/preprocess.cpp | 126 +++-- src/query/plan/preprocess.hpp | 124 +++- src/query/plan/pretty_print.cpp | 34 +- src/query/plan/pretty_print.hpp | 4 +- src/query/plan/rewrite/index_lookup.hpp | 10 + src/query/plan/rule_based_planner.cpp | 30 +- src/query/plan/rule_based_planner.hpp | 313 +++++++---- src/query/plan/variable_start_planner.cpp | 107 +++- src/query/plan/variable_start_planner.hpp | 15 +- src/utils/event_counter.cpp | 3 +- .../features/memgraph_exists.feature | 529 ++++++++++++++++++ tests/unit/cypher_main_visitor.cpp | 74 ++- tests/unit/plan_pretty_print.cpp | 57 +- tests/unit/query_common.hpp | 12 +- tests/unit/query_cost_estimator.cpp | 5 +- tests/unit/query_plan.cpp | 115 ++++ tests/unit/query_plan_checker.hpp | 28 +- .../query_plan_create_set_remove_delete.cpp | 7 +- tests/unit/query_plan_match_filter_return.cpp | 249 ++++++++- .../unit/query_plan_read_write_typecheck.cpp | 8 +- tests/unit/query_pretty_print.cpp | 3 +- tests/unit/query_semantic.cpp | 35 ++ 34 files changed, 1891 insertions(+), 261 deletions(-) create mode 100644 tests/gql_behave/tests/memgraph_V1/features/memgraph_exists.feature diff --git a/src/query/frontend/ast/ast.lcp b/src/query/frontend/ast/ast.lcp index a1f3c8ef5..24b460c4f 100644 --- a/src/query/frontend/ast/ast.lcp +++ b/src/query/frontend/ast/ast.lcp @@ -2711,5 +2711,41 @@ cpp<# (:serialize (:slk)) (:clone)) +(lcp:define-class exists (expression) + ((pattern "Pattern *" :initval "nullptr" :scope :public + :slk-save #'slk-save-ast-pointer + :slk-load (slk-load-ast-pointer "Pattern")) + (symbol-pos :int32_t :initval -1 :scope :public + :documentation "Symbol table position of the symbol this Aggregation is mapped to.")) + + (:public + #>cpp + Exists() = default; + + DEFVISITABLE(ExpressionVisitor); + DEFVISITABLE(ExpressionVisitor); + DEFVISITABLE(ExpressionVisitor); + bool Accept(HierarchicalTreeVisitor &visitor) override { + if (visitor.PreVisit(*this)) { + pattern_->Accept(visitor); + } + return visitor.PostVisit(*this); + } + Exists *MapTo(const Symbol &symbol) { + symbol_pos_ = symbol.position(); + return this; + } + cpp<#) + (:protected + #>cpp + Exists(Pattern * pattern) : pattern_(pattern) {} + cpp<#) + (:private + #>cpp + friend class AstStorage; + cpp<#) + (:serialize (:slk)) + (:clone)) + (lcp:pop-namespace) ;; namespace query (lcp:pop-namespace) ;; namespace memgraph diff --git a/src/query/frontend/ast/ast_visitor.hpp b/src/query/frontend/ast/ast_visitor.hpp index f9a574f17..e860cad1d 100644 --- a/src/query/frontend/ast/ast_visitor.hpp +++ b/src/query/frontend/ast/ast_visitor.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 @@ -95,6 +95,7 @@ class SettingQuery; class VersionQuery; class Foreach; class ShowConfigQuery; +class Exists; using TreeCompositeVisitor = utils::CompositeVisitor< SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator, @@ -103,7 +104,7 @@ using TreeCompositeVisitor = utils::CompositeVisitor< ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral, PropertyLookup, 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>; + RemoveProperty, RemoveLabels, Merge, Unwind, RegexMatch, LoadCsv, Foreach, Exists>; using TreeLeafVisitor = utils::LeafVisitor; @@ -123,7 +124,7 @@ class ExpressionVisitor LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, SubscriptOperator, ListSlicingOperator, IfOperator, UnaryPlusOperator, UnaryMinusOperator, IsNullOperator, ListLiteral, MapLiteral, PropertyLookup, LabelsTest, Aggregation, Function, Reduce, Coalesce, Extract, All, Single, Any, - None, ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch> {}; + None, ParameterLookup, Identifier, PrimitiveLiteral, RegexMatch, Exists> {}; template class QueryVisitor : public utils::Visitor(ctx->extractExpression()->idInColl()->expression()->accept(this)); auto *expr = std::any_cast(ctx->extractExpression()->expression()->accept(this)); return static_cast(storage_->Create(ident, list, expr)); + } else if (ctx->existsExpression()) { + return std::any_cast(ctx->existsExpression()->accept(this)); } + // TODO: Implement this. We don't support comprehensions, filtering... at // the moment. throw utils::NotYetImplemented("atom expression '{}'", ctx->getText()); @@ -2204,6 +2207,17 @@ antlrcpp::Any CypherMainVisitor::visitLiteral(MemgraphCypher::LiteralContext *ct return visitChildren(ctx); } +antlrcpp::Any CypherMainVisitor::visitExistsExpression(MemgraphCypher::ExistsExpressionContext *ctx) { + auto *exists = storage_->Create(); + exists->pattern_ = std::any_cast(ctx->patternPart()->accept(this)); + + if (exists->pattern_->identifier_) { + throw SyntaxException("Identifiers are not supported in exists(...)."); + } + + return static_cast(exists); +} + antlrcpp::Any CypherMainVisitor::visitParenthesizedExpression(MemgraphCypher::ParenthesizedExpressionContext *ctx) { return std::any_cast(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 c6cb125da..e5c5f55f4 100644 --- a/src/query/frontend/ast/cypher_main_visitor.hpp +++ b/src/query/frontend/ast/cypher_main_visitor.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 @@ -763,6 +763,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor { */ antlrcpp::Any visitParameter(MemgraphCypher::ParameterContext *ctx) override; + /** + * @return Exists* (Expression) + */ + antlrcpp::Any visitExistsExpression(MemgraphCypher::ExistsExpressionContext *ctx) override; + /** * @return Expression* */ diff --git a/src/query/frontend/ast/pretty_print.cpp b/src/query/frontend/ast/pretty_print.cpp index 5f94d6def..c9fb4826c 100644 --- a/src/query/frontend/ast/pretty_print.cpp +++ b/src/query/frontend/ast/pretty_print.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 @@ -60,6 +60,7 @@ class ExpressionPrettyPrinter : public ExpressionVisitor { void Visit(Reduce &op) override; void Visit(Coalesce &op) override; void Visit(Extract &op) override; + void Visit(Exists &op) override; void Visit(All &op) override; void Visit(Single &op) override; void Visit(Any &op) override; @@ -264,6 +265,8 @@ void ExpressionPrettyPrinter::Visit(Extract &op) { PrintOperator(out_, "Extract", op.identifier_, op.list_, op.expression_); } +void ExpressionPrettyPrinter::Visit(Exists & /*op*/) { PrintOperator(out_, "Exists", "expression"); } + void ExpressionPrettyPrinter::Visit(All &op) { PrintOperator(out_, "All", op.identifier_, op.list_expression_, op.where_->expression_); } diff --git a/src/query/frontend/opencypher/grammar/Cypher.g4 b/src/query/frontend/opencypher/grammar/Cypher.g4 index 68f7b0908..65653d3b0 100644 --- a/src/query/frontend/opencypher/grammar/Cypher.g4 +++ b/src/query/frontend/opencypher/grammar/Cypher.g4 @@ -236,6 +236,7 @@ atom : literal | ( ANY '(' filterExpression ')' ) | ( NONE '(' filterExpression ')' ) | ( SINGLE '(' filterExpression ')' ) + | ( EXISTS '(' existsExpression ')' ) | relationshipsPattern | parenthesizedExpression | functionInvocation @@ -275,6 +276,8 @@ reduceExpression : accumulator=variable '=' initial=expression ',' idInColl '|' extractExpression : idInColl '|' expression ; +existsExpression : patternPart ; + idInColl : variable IN expression ; functionInvocation : functionName '(' ( DISTINCT )? ( expression ( ',' expression )* )? ')' ; diff --git a/src/query/frontend/semantic/symbol_generator.cpp b/src/query/frontend/semantic/symbol_generator.cpp index 2468d9c73..d5450392b 100644 --- a/src/query/frontend/semantic/symbol_generator.cpp +++ b/src/query/frontend/semantic/symbol_generator.cpp @@ -64,6 +64,11 @@ auto SymbolGenerator::CreateSymbol(const std::string &name, bool user_declared, return symbol; } +auto SymbolGenerator::CreateAnonymousSymbol(Symbol::Type /*type*/) { + auto symbol = symbol_table_->CreateAnonymousSymbol(); + return symbol; +} + auto SymbolGenerator::GetOrCreateSymbol(const std::string &name, bool user_declared, Symbol::Type type) { // NOLINTNEXTLINE for (auto scope = scopes_.rbegin(); scope != scopes_.rend(); ++scope) { @@ -302,6 +307,14 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) { if (scope.in_skip || scope.in_limit) { throw SemanticException("Variables are not allowed in {}.", scope.in_skip ? "SKIP" : "LIMIT"); } + + if (scope.in_exists && (scope.visiting_edge || scope.in_node_atom)) { + auto has_symbol = HasSymbol(ident.name_); + if (!has_symbol && !ConsumePredefinedIdentifier(ident.name_) && ident.user_declared_) { + throw SemanticException("Unbounded variables are not allowed in exists!"); + } + } + Symbol symbol; 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 @@ -328,7 +341,8 @@ SymbolGenerator::ReturnType SymbolGenerator::Visit(Identifier &ident) { } 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_) { + if (scope.in_edge_range && scope.visiting_edge && scope.visiting_edge->identifier_ && + scope.visiting_edge->identifier_->name_ == ident.name_) { // Prevent variable path bounds to reference the identifier which is bound // by the variable path itself. throw UnboundVariableError(ident.name_); @@ -430,6 +444,46 @@ bool SymbolGenerator::PreVisit(Extract &extract) { return false; } +bool SymbolGenerator::PreVisit(Exists &exists) { + auto &scope = scopes_.back(); + + if (scope.in_set_property) { + throw utils::NotYetImplemented("Set property can not be used with exists, but only during matching!"); + } + + if (scope.in_with) { + throw utils::NotYetImplemented("WITH can not be used with exists, but only during matching!"); + } + + scope.in_exists = true; + + const auto &symbol = CreateAnonymousSymbol(); + exists.MapTo(symbol); + + return true; +} + +bool SymbolGenerator::PostVisit(Exists & /*exists*/) { + auto &scope = scopes_.back(); + scope.in_exists = false; + + return true; +} + +bool SymbolGenerator::PreVisit(SetProperty & /*set_property*/) { + auto &scope = scopes_.back(); + scope.in_set_property = true; + + return true; +} + +bool SymbolGenerator::PostVisit(SetProperty & /*set_property*/) { + auto &scope = scopes_.back(); + scope.in_set_property = false; + + return true; +} + // Pattern and its subparts. bool SymbolGenerator::PreVisit(Pattern &pattern) { @@ -439,6 +493,7 @@ bool SymbolGenerator::PreVisit(Pattern &pattern) { MG_ASSERT(utils::IsSubtype(*pattern.atoms_[0], NodeAtom::kType), "Expected a single NodeAtom in Pattern"); scope.in_create_node = true; } + return true; } diff --git a/src/query/frontend/semantic/symbol_generator.hpp b/src/query/frontend/semantic/symbol_generator.hpp index 80ea9d919..556c6dab4 100644 --- a/src/query/frontend/semantic/symbol_generator.hpp +++ b/src/query/frontend/semantic/symbol_generator.hpp @@ -64,6 +64,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor { bool PostVisit(Match &) override; bool PreVisit(Foreach &) override; bool PostVisit(Foreach &) override; + bool PreVisit(SetProperty & /*set_property*/) override; + bool PostVisit(SetProperty & /*set_property*/) override; // Expressions ReturnType Visit(Identifier &) override; @@ -79,6 +81,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor { bool PreVisit(None &) override; bool PreVisit(Reduce &) override; bool PreVisit(Extract &) override; + bool PreVisit(Exists & /*exists*/) override; + bool PostVisit(Exists & /*exists*/) override; // Pattern and its subparts. bool PreVisit(Pattern &) override; @@ -113,6 +117,8 @@ class SymbolGenerator : public HierarchicalTreeVisitor { bool in_where{false}; bool in_match{false}; bool in_foreach{false}; + bool in_exists{false}; + bool in_set_property{false}; // True when visiting a pattern atom (node or edge) identifier, which can be // reused or created in the pattern itself. bool in_pattern_atom_identifier{false}; @@ -143,6 +149,9 @@ class SymbolGenerator : public HierarchicalTreeVisitor { auto CreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY, int token_position = -1); + // Returns a freshly generated anonymous symbol. + auto CreateAnonymousSymbol(Symbol::Type type = Symbol::Type::ANY); + 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. diff --git a/src/query/frontend/semantic/symbol_table.hpp b/src/query/frontend/semantic/symbol_table.hpp index 7d17edc2f..23b4965c5 100644 --- a/src/query/frontend/semantic/symbol_table.hpp +++ b/src/query/frontend/semantic/symbol_table.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 @@ -51,6 +51,7 @@ class SymbolTable final { const Symbol &at(const Identifier &ident) const { return table_.at(ident.symbol_pos_); } 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_); } // TODO: Remove these since members are public int32_t max_position() const { return static_cast(table_.size()); } diff --git a/src/query/interpret/eval.hpp b/src/query/interpret/eval.hpp index 74df223a5..a094fcea0 100644 --- a/src/query/interpret/eval.hpp +++ b/src/query/interpret/eval.hpp @@ -89,6 +89,7 @@ class ReferenceExpressionEvaluator : public ExpressionVisitor { UNSUCCESSFUL_VISIT(None); UNSUCCESSFUL_VISIT(ParameterLookup); UNSUCCESSFUL_VISIT(RegexMatch); + UNSUCCESSFUL_VISIT(Exists); private: Frame *frame_; @@ -619,6 +620,8 @@ class ExpressionEvaluator : public ExpressionVisitor { return TypedValue(result, ctx_->memory); } + TypedValue Visit(Exists &exists) override { return TypedValue{frame_->at(symbol_table_->at(exists)), ctx_->memory}; } + TypedValue Visit(All &all) override { auto list_value = all.list_expression_->Accept(*this); if (list_value.IsNull()) { diff --git a/src/query/plan/operator.cpp b/src/query/plan/operator.cpp index 846545721..322a08155 100644 --- a/src/query/plan/operator.cpp +++ b/src/query/plan/operator.cpp @@ -116,6 +116,7 @@ extern const Event CartesianOperator; extern const Event CallProcedureOperator; extern const Event ForeachOperator; extern const Event EmptyResultOperator; +extern const Event EvaluatePatternFilterOperator; } // namespace EventCounter namespace memgraph::query::plan { @@ -2257,10 +2258,19 @@ std::vector ConstructNamedPath::ModifiedSymbols(const SymbolTable &table return symbols; } -Filter::Filter(const std::shared_ptr &input, Expression *expression) - : input_(input ? input : std::make_shared()), expression_(expression) {} +Filter::Filter(const std::shared_ptr &input, + const std::vector> &pattern_filters, Expression *expression) + : input_(input ? input : std::make_shared()), pattern_filters_(pattern_filters), expression_(expression) {} -ACCEPT_WITH_INPUT(Filter) +bool Filter::Accept(HierarchicalLogicalOperatorVisitor &visitor) { + if (visitor.PreVisit(*this)) { + input_->Accept(visitor); + for (const auto &pattern_filter : pattern_filters_) { + pattern_filter->Accept(visitor); + } + } + return visitor.PostVisit(*this); +} UniqueCursorPtr Filter::MakeCursor(utils::MemoryResource *mem) const { EventCounter::IncrementCounter(EventCounter::FilterOperator); @@ -2270,8 +2280,24 @@ UniqueCursorPtr Filter::MakeCursor(utils::MemoryResource *mem) const { std::vector Filter::ModifiedSymbols(const SymbolTable &table) const { return input_->ModifiedSymbols(table); } +static std::vector MakeCursorVector(const std::vector> &ops, + utils::MemoryResource *mem) { + std::vector cursors; + cursors.reserve(ops.size()); + + if (!ops.empty()) { + for (const auto &op : ops) { + cursors.push_back(op->MakeCursor(mem)); + } + } + + return cursors; +} + Filter::FilterCursor::FilterCursor(const Filter &self, utils::MemoryResource *mem) - : self_(self), input_cursor_(self_.input_->MakeCursor(mem)) {} + : self_(self), + input_cursor_(self_.input_->MakeCursor(mem)), + pattern_filter_cursors_(MakeCursorVector(self_.pattern_filters_, mem)) {} bool Filter::FilterCursor::Pull(Frame &frame, ExecutionContext &context) { SCOPED_PROFILE_OP("Filter"); @@ -2281,6 +2307,10 @@ bool Filter::FilterCursor::Pull(Frame &frame, ExecutionContext &context) { ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor, storage::View::OLD); 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; } return false; @@ -2290,6 +2320,39 @@ void Filter::FilterCursor::Shutdown() { input_cursor_->Shutdown(); } void Filter::FilterCursor::Reset() { input_cursor_->Reset(); } +EvaluatePatternFilter::EvaluatePatternFilter(const std::shared_ptr &input, Symbol output_symbol) + : input_(input), output_symbol_(output_symbol) {} + +ACCEPT_WITH_INPUT(EvaluatePatternFilter); + +UniqueCursorPtr EvaluatePatternFilter::MakeCursor(utils::MemoryResource *mem) const { + EventCounter::IncrementCounter(EventCounter::EvaluatePatternFilterOperator); + + return MakeUniqueCursorPtr(mem, *this, mem); +} + +EvaluatePatternFilter::EvaluatePatternFilterCursor::EvaluatePatternFilterCursor(const EvaluatePatternFilter &self, + utils::MemoryResource *mem) + : self_(self), input_cursor_(self_.input_->MakeCursor(mem)) {} + +std::vector EvaluatePatternFilter::ModifiedSymbols(const SymbolTable &table) const { + return input_->ModifiedSymbols(table); +} + +bool EvaluatePatternFilter::EvaluatePatternFilterCursor::Pull(Frame &frame, ExecutionContext &context) { + SCOPED_PROFILE_OP("EvaluatePatternFilter"); + + input_cursor_->Reset(); + + frame[self_.output_symbol_] = TypedValue(input_cursor_->Pull(frame, context), context.evaluation_context.memory); + + return true; +} + +void EvaluatePatternFilter::EvaluatePatternFilterCursor::Shutdown() { input_cursor_->Shutdown(); } + +void EvaluatePatternFilter::EvaluatePatternFilterCursor::Reset() { input_cursor_->Reset(); } + Produce::Produce(const std::shared_ptr &input, const std::vector &named_expressions) : input_(input ? input : std::make_shared()), named_expressions_(named_expressions) {} diff --git a/src/query/plan/operator.lcp b/src/query/plan/operator.lcp index 4b1e10ce1..24e89b82e 100644 --- a/src/query/plan/operator.lcp +++ b/src/query/plan/operator.lcp @@ -133,6 +133,7 @@ class CallProcedure; class LoadCsv; class Foreach; class EmptyResult; +class EvaluatePatternFilter; using LogicalOperatorCompositeVisitor = utils::CompositeVisitor< Once, CreateNode, CreateExpand, ScanAll, ScanAllByLabel, @@ -141,7 +142,7 @@ using LogicalOperatorCompositeVisitor = utils::CompositeVisitor< Expand, ExpandVariable, ConstructNamedPath, Filter, Produce, Delete, SetProperty, SetProperties, SetLabels, RemoveProperty, RemoveLabels, EdgeUniquenessFilter, Accumulate, Aggregate, Skip, Limit, OrderBy, Merge, - Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv, Foreach, EmptyResult>; + Optional, Unwind, Distinct, Union, Cartesian, CallProcedure, LoadCsv, Foreach, EmptyResult, EvaluatePatternFilter>; using LogicalOperatorLeafVisitor = utils::LeafVisitor; @@ -1122,6 +1123,9 @@ pulled.") ((input "std::shared_ptr" :scope :public :slk-save #'slk-save-operator-pointer :slk-load #'slk-load-operator-pointer) + (pattern_filters "std::vector>" :scope :public + :slk-save #'slk-save-ast-vector + :slk-load (slk-load-ast-vector "std::shared_ptr")) (expression "Expression *" :scope :public :slk-save #'slk-save-ast-pointer :slk-load (slk-load-ast-pointer "Expression"))) @@ -1136,6 +1140,7 @@ a boolean value.") Filter() {} Filter(const std::shared_ptr &input_, + const std::vector> &pattern_filters_, Expression *expression_); bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override; UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override; @@ -1159,6 +1164,7 @@ a boolean value.") private: const Filter &self_; const UniqueCursorPtr input_cursor_; + const std::vector pattern_filter_cursors_; }; cpp<#) (:serialize (:slk)) @@ -1777,6 +1783,45 @@ operator's implementation does not expect this.") (:serialize (:slk)) (:clone)) +(lcp:define-class evaluate-pattern-filter (logical-operator) + ((input "std::shared_ptr" :scope :public + :slk-save #'slk-save-operator-pointer + :slk-load #'slk-load-operator-pointer) + (output-symbol "Symbol" :scope :public)) + (:documentation "Applies the pattern filter by putting the value of the input cursor to the frame.") + + (:public + #>cpp + EvaluatePatternFilter() {} + + EvaluatePatternFilter(const std::shared_ptr &input, Symbol output_symbol); + bool Accept(HierarchicalLogicalOperatorVisitor &visitor) override; + UniqueCursorPtr MakeCursor(utils::MemoryResource *) const override; + std::vector ModifiedSymbols(const SymbolTable &) const override; + + bool HasSingleInput() const override { return true; } + std::shared_ptr input() const override { return input_; } + void set_input(std::shared_ptr input) override { + input_ = input; + } + cpp<#) + (:private + #>cpp + class EvaluatePatternFilterCursor : public Cursor { + public: + EvaluatePatternFilterCursor(const EvaluatePatternFilter &, utils::MemoryResource *); + bool Pull(Frame &, ExecutionContext &) override; + void Shutdown() override; + void Reset() override; + + private: + const EvaluatePatternFilter &self_; + UniqueCursorPtr input_cursor_; + }; + cpp<#) + (:serialize (:slk)) + (:clone)) + (lcp:define-class limit (logical-operator) ((input "std::shared_ptr" :scope :public :slk-save #'slk-save-operator-pointer diff --git a/src/query/plan/preprocess.cpp b/src/query/plan/preprocess.cpp index 5f90a7b85..e2394d7c9 100644 --- a/src/query/plan/preprocess.cpp +++ b/src/query/plan/preprocess.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 @@ -84,56 +84,6 @@ std::vector NormalizePatterns(const SymbolTable &symbol_table, const return expansions; } -// Fills the given Matching, by converting the Match patterns to normalized -// representation as Expansions. Filters used in the Match are also collected, -// as well as edge symbols which determine Cyphermorphism. Collecting filters -// will lift them out of a pattern and generate new expressions (just like they -// were in a Where clause). -void AddMatching(const std::vector &patterns, Where *where, SymbolTable &symbol_table, AstStorage &storage, - Matching &matching) { - auto expansions = NormalizePatterns(symbol_table, patterns); - std::unordered_set edge_symbols; - for (const auto &expansion : expansions) { - // Matching may already have some expansions, so offset our index. - const size_t expansion_ix = matching.expansions.size(); - // Map node1 symbol to expansion - const auto &node1_sym = symbol_table.at(*expansion.node1->identifier_); - matching.node_symbol_to_expansions[node1_sym].insert(expansion_ix); - // Add node1 to all symbols. - matching.expansion_symbols.insert(node1_sym); - if (expansion.edge) { - const auto &edge_sym = symbol_table.at(*expansion.edge->identifier_); - // Fill edge symbols for Cyphermorphism. - edge_symbols.insert(edge_sym); - // Map node2 symbol to expansion - const auto &node2_sym = symbol_table.at(*expansion.node2->identifier_); - matching.node_symbol_to_expansions[node2_sym].insert(expansion_ix); - // Add edge and node2 to all symbols - matching.expansion_symbols.insert(edge_sym); - matching.expansion_symbols.insert(node2_sym); - } - matching.expansions.push_back(expansion); - } - if (!edge_symbols.empty()) { - matching.edge_symbols.emplace_back(edge_symbols); - } - for (auto *pattern : patterns) { - matching.filters.CollectPatternFilters(*pattern, symbol_table, storage); - if (pattern->identifier_->user_declared_) { - std::vector path_elements; - for (auto *pattern_atom : pattern->atoms_) - path_elements.emplace_back(symbol_table.at(*pattern_atom->identifier_)); - matching.named_paths.emplace(symbol_table.at(*pattern->identifier_), std::move(path_elements)); - } - } - if (where) { - matching.filters.CollectWhereFilter(*where, symbol_table); - } -} -void AddMatching(const Match &match, SymbolTable &symbol_table, AstStorage &storage, Matching &matching) { - return AddMatching(match.patterns_, match.where_, symbol_table, storage, matching); -} - auto SplitExpressionOnAnd(Expression *expression) { // TODO: Think about converting all filtering expression into CNF to improve // the granularity of filters which can be stand alone. @@ -519,6 +469,8 @@ void Filters::AnalyzeAndStoreFilter(Expression *expr, const SymbolTable &symbol_ if (!add_prop_is_not_null_check(is_not_null)) { all_filters_.emplace_back(make_filter(FilterInfo::Type::Generic)); } + } else if (auto *exists = utils::Downcast(expr)) { + all_filters_.emplace_back(make_filter(FilterInfo::Type::Pattern)); } else { all_filters_.emplace_back(make_filter(FilterInfo::Type::Generic)); } @@ -528,6 +480,78 @@ void Filters::AnalyzeAndStoreFilter(Expression *expr, const SymbolTable &symbol_ // as `expr1 < n.prop AND n.prop < expr2`. } +// Fills the given Matching, by converting the Match patterns to normalized +// representation as Expansions. Filters used in the Match are also collected, +// as well as edge symbols which determine Cyphermorphism. Collecting filters +// will lift them out of a pattern and generate new expressions (just like they +// were in a Where clause). +void AddMatching(const std::vector &patterns, Where *where, SymbolTable &symbol_table, AstStorage &storage, + Matching &matching) { + auto expansions = NormalizePatterns(symbol_table, patterns); + std::unordered_set edge_symbols; + for (const auto &expansion : expansions) { + // Matching may already have some expansions, so offset our index. + const size_t expansion_ix = matching.expansions.size(); + // Map node1 symbol to expansion + const auto &node1_sym = symbol_table.at(*expansion.node1->identifier_); + matching.node_symbol_to_expansions[node1_sym].insert(expansion_ix); + // Add node1 to all symbols. + matching.expansion_symbols.insert(node1_sym); + if (expansion.edge) { + const auto &edge_sym = symbol_table.at(*expansion.edge->identifier_); + // Fill edge symbols for Cyphermorphism. + edge_symbols.insert(edge_sym); + // Map node2 symbol to expansion + const auto &node2_sym = symbol_table.at(*expansion.node2->identifier_); + matching.node_symbol_to_expansions[node2_sym].insert(expansion_ix); + // Add edge and node2 to all symbols + matching.expansion_symbols.insert(edge_sym); + matching.expansion_symbols.insert(node2_sym); + } + matching.expansions.push_back(expansion); + } + if (!edge_symbols.empty()) { + matching.edge_symbols.emplace_back(edge_symbols); + } + for (auto *const pattern : patterns) { + matching.filters.CollectPatternFilters(*pattern, symbol_table, storage); + if (pattern->identifier_->user_declared_) { + std::vector path_elements; + for (auto *const pattern_atom : pattern->atoms_) + path_elements.push_back(symbol_table.at(*pattern_atom->identifier_)); + matching.named_paths.emplace(symbol_table.at(*pattern->identifier_), std::move(path_elements)); + } + } + if (where) { + matching.filters.CollectWhereFilter(*where, symbol_table); + } +} + +void AddMatching(const Match &match, SymbolTable &symbol_table, AstStorage &storage, Matching &matching) { + AddMatching(match.patterns_, match.where_, symbol_table, storage, matching); + + // If there are any pattern filters, we add those as well + for (auto &filter : matching.filters) { + PatternFilterVisitor visitor(symbol_table, storage); + + filter.expression->Accept(visitor); + filter.matchings = visitor.getMatchings(); + } +} + +void PatternFilterVisitor::Visit(Exists &op) { + std::vector patterns; + patterns.push_back(op.pattern_); + + FilterMatching filter_matching; + AddMatching(patterns, nullptr, symbol_table_, storage_, filter_matching); + + filter_matching.type = PatternFilterType::EXISTS; + filter_matching.symbol = std::make_optional(symbol_table_.at(op)); + + matchings_.push_back(std::move(filter_matching)); +} + static void ParseForeach(query::Foreach &foreach, SingleQueryPart &query_part, AstStorage &storage, SymbolTable &symbol_table) { for (auto *clause : foreach.clauses_) { diff --git a/src/query/plan/preprocess.hpp b/src/query/plan/preprocess.hpp index 35a3002ec..a8c02b13c 100644 --- a/src/query/plan/preprocess.hpp +++ b/src/query/plan/preprocess.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 @@ -70,7 +70,26 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor { } bool Visit(Identifier &ident) override { - symbols_.insert(symbol_table_.at(ident)); + if (!in_exists || ident.user_declared_) { + symbols_.insert(symbol_table_.at(ident)); + } + + return true; + } + + bool PreVisit(Exists &exists) override { + in_exists = true; + + // We do not visit pattern identifier since we're in exists filter pattern + for (auto &atom : exists.pattern_->atoms_) { + atom->Accept(*this); + } + + return false; + } + + bool PostVisit(Exists & /*exists*/) override { + in_exists = false; return true; } @@ -79,6 +98,9 @@ class UsedSymbolsCollector : public HierarchicalTreeVisitor { std::unordered_set symbols_; const SymbolTable &symbol_table_; + + private: + bool in_exists{false}; }; /// Normalized representation of a pattern that needs to be matched. @@ -99,6 +121,93 @@ struct Expansion { NodeAtom *node2 = nullptr; }; +struct FilterMatching; + +enum class PatternFilterType { EXISTS }; + +/// Collects matchings from filters that include patterns +class PatternFilterVisitor : public ExpressionVisitor { + public: + explicit PatternFilterVisitor(SymbolTable &symbol_table, AstStorage &storage) + : symbol_table_(symbol_table), storage_(storage) {} + + using ExpressionVisitor::Visit; + + // Unary operators + void Visit(NotOperator &op) override { op.expression_->Accept(*this); } + void Visit(IsNullOperator &op) override { op.expression_->Accept(*this); }; + void Visit(UnaryPlusOperator &op) override{}; + void Visit(UnaryMinusOperator &op) override{}; + + // Binary operators + void Visit(OrOperator &op) override { + op.expression1_->Accept(*this); + op.expression2_->Accept(*this); + } + void Visit(XorOperator &op) override { + op.expression1_->Accept(*this); + op.expression2_->Accept(*this); + } + void Visit(AndOperator &op) override { + op.expression1_->Accept(*this); + op.expression2_->Accept(*this); + } + void Visit(NotEqualOperator &op) override { + op.expression1_->Accept(*this); + op.expression2_->Accept(*this); + }; + void Visit(EqualOperator &op) override { + op.expression1_->Accept(*this); + op.expression2_->Accept(*this); + }; + void Visit(InListOperator &op) override { + op.expression1_->Accept(*this); + op.expression2_->Accept(*this); + }; + void Visit(AdditionOperator &op) override{}; + void Visit(SubtractionOperator &op) override{}; + void Visit(MultiplicationOperator &op) override{}; + void Visit(DivisionOperator &op) override{}; + void Visit(ModOperator &op) override{}; + void Visit(LessOperator &op) override{}; + void Visit(GreaterOperator &op) override{}; + void Visit(LessEqualOperator &op) override{}; + void Visit(GreaterEqualOperator &op) override{}; + void Visit(SubscriptOperator &op) override{}; + + // Other + void Visit(ListSlicingOperator &op) override{}; + void Visit(IfOperator &op) override{}; + void Visit(ListLiteral &op) override{}; + void Visit(MapLiteral &op) override{}; + void Visit(LabelsTest &op) override{}; + void Visit(Aggregation &op) override{}; + void Visit(Function &op) override{}; + void Visit(Reduce &op) override{}; + void Visit(Coalesce &op) override{}; + void Visit(Extract &op) override{}; + void Visit(Exists &op) override; + void Visit(All &op) override{}; + void Visit(Single &op) override{}; + void Visit(Any &op) override{}; + void Visit(None &op) override{}; + void Visit(Identifier &op) override{}; + void Visit(PrimitiveLiteral &op) override{}; + void Visit(PropertyLookup &op) override{}; + void Visit(ParameterLookup &op) override{}; + void Visit(NamedExpression &op) override{}; + void Visit(RegexMatch &op) override{}; + + std::vector getMatchings() { return matchings_; } + + SymbolTable &symbol_table_; + AstStorage &storage_; + + private: + /// Collection of matchings in the filter expression being analyzed. + std::vector matchings_; +}; + /// Stores the symbols and expression used to filter a property. class PropertyFilter { public: @@ -153,7 +262,7 @@ struct FilterInfo { /// applied for labels or a property. Non generic types contain extra /// information which can be used to produce indexed scans of graph /// elements. - enum class Type { Generic, Label, Property, Id }; + enum class Type { Generic, Label, Property, Id, Pattern }; Type type; /// The original filter expression which must be satisfied. @@ -166,6 +275,8 @@ struct FilterInfo { std::optional property_filter; /// Information for Type::Id filtering. std::optional id_filter; + /// Matchings for filters that include patterns + std::vector matchings; }; /// Stores information on filters used inside the @c Matching of a @c QueryPart. @@ -287,6 +398,13 @@ struct Matching { std::unordered_set expansion_symbols{}; }; +struct FilterMatching : Matching { + /// Type of pattern filter + PatternFilterType type; + /// Symbol for the filter expression + std::optional symbol; +}; + /// @brief Represents a read (+ write) part of a query. Parts are split on /// `WITH` clauses. /// diff --git a/src/query/plan/pretty_print.cpp b/src/query/plan/pretty_print.cpp index e800fdb7b..536bd3867 100644 --- a/src/query/plan/pretty_print.cpp +++ b/src/query/plan/pretty_print.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 @@ -14,6 +14,7 @@ #include "query/db_accessor.hpp" #include "query/frontend/ast/pretty_print.hpp" +#include "query/plan/operator.hpp" #include "utils/string.hpp" namespace memgraph::query::plan { @@ -148,7 +149,6 @@ bool PlanPrinter::PreVisit(query::plan::Produce &op) { } PRE_VISIT(ConstructNamedPath); -PRE_VISIT(Filter); PRE_VISIT(SetProperty); PRE_VISIT(SetProperties); PRE_VISIT(SetLabels); @@ -157,6 +157,7 @@ PRE_VISIT(RemoveLabels); PRE_VISIT(EdgeUniquenessFilter); PRE_VISIT(Accumulate); PRE_VISIT(EmptyResult); +PRE_VISIT(EvaluatePatternFilter); bool PlanPrinter::PreVisit(query::plan::Aggregate &op) { WithPrintLn([&](auto &out) { @@ -251,6 +252,15 @@ bool PlanPrinter::PreVisit(query::plan::Foreach &op) { op.input_->Accept(*this); return false; } + +bool PlanPrinter::PreVisit(query::plan::Filter &op) { + WithPrintLn([](auto &out) { out << "* Filter"; }); + for (const auto &pattern_filter : op.pattern_filters_) { + Branch(*pattern_filter); + } + op.input_->Accept(*this); + return false; +} #undef PRE_VISIT bool PlanPrinter::DefaultPreVisit() { @@ -589,6 +599,13 @@ bool PlanToJsonVisitor::PreVisit(Filter &op) { op.input_->Accept(*this); self["input"] = PopOutput(); + for (auto pattern_idx = 0; pattern_idx < op.pattern_filters_.size(); pattern_idx++) { + auto pattern_filter_key = "pattern_filter" + std::to_string(pattern_idx + 1); + + op.pattern_filters_[pattern_idx]->Accept(*this); + self[pattern_filter_key] = PopOutput(); + } + output_ = std::move(self); return false; } @@ -908,6 +925,7 @@ bool PlanToJsonVisitor::PreVisit(Cartesian &op) { output_ = std::move(self); return false; } + bool PlanToJsonVisitor::PreVisit(Foreach &op) { json self; self["name"] = "Foreach"; @@ -924,6 +942,18 @@ bool PlanToJsonVisitor::PreVisit(Foreach &op) { return false; } +bool PlanToJsonVisitor::PreVisit(EvaluatePatternFilter &op) { + json self; + self["name"] = "EvaluatePatternFilter"; + self["output_symbol"] = ToJson(op.output_symbol_); + + op.input_->Accept(*this); + self["input"] = PopOutput(); + + output_ = std::move(self); + return false; +} + } // namespace impl } // namespace memgraph::query::plan diff --git a/src/query/plan/pretty_print.hpp b/src/query/plan/pretty_print.hpp index dcd9c8095..716c60077 100644 --- a/src/query/plan/pretty_print.hpp +++ b/src/query/plan/pretty_print.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 @@ -74,6 +74,7 @@ class PlanPrinter : public virtual HierarchicalLogicalOperatorVisitor { bool PreVisit(ConstructNamedPath &) override; bool PreVisit(Filter &) override; + bool PreVisit(EvaluatePatternFilter & /*unused*/) override; bool PreVisit(EdgeUniquenessFilter &) override; bool PreVisit(Merge &) override; @@ -186,6 +187,7 @@ class PlanToJsonVisitor : public virtual HierarchicalLogicalOperatorVisitor { bool PreVisit(Optional &) override; bool PreVisit(Filter &) override; + bool PreVisit(EvaluatePatternFilter & /*op*/) override; bool PreVisit(EdgeUniquenessFilter &) override; bool PreVisit(Cartesian &) override; diff --git a/src/query/plan/rewrite/index_lookup.hpp b/src/query/plan/rewrite/index_lookup.hpp index edc61bb09..a6b3d5a9f 100644 --- a/src/query/plan/rewrite/index_lookup.hpp +++ b/src/query/plan/rewrite/index_lookup.hpp @@ -454,6 +454,16 @@ class IndexLookupRewriter final : public HierarchicalLogicalOperatorVisitor { return true; } + bool PreVisit(EvaluatePatternFilter &op) override { + prev_ops_.push_back(&op); + return true; + } + + bool PostVisit(EvaluatePatternFilter & /*op*/) override { + prev_ops_.pop_back(); + return true; + } + std::shared_ptr new_root_; private: diff --git a/src/query/plan/rule_based_planner.cpp b/src/query/plan/rule_based_planner.cpp index ab4a0752c..616b13129 100644 --- a/src/query/plan/rule_based_planner.cpp +++ b/src/query/plan/rule_based_planner.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 @@ -25,15 +25,6 @@ namespace memgraph::query::plan { namespace { -bool HasBoundFilterSymbols(const std::unordered_set &bound_symbols, const FilterInfo &filter) { - for (const auto &symbol : filter.used_symbols) { - if (bound_symbols.find(symbol) == bound_symbols.end()) { - return false; - } - } - return true; -} - // Ast tree visitor which collects the context for a return body. // The return body of WITH and RETURN clauses consists of: // @@ -495,7 +486,8 @@ std::unique_ptr GenReturnBody(std::unique_ptr // Where may see new symbols so it comes after we generate Produce and in // general, comes after any OrderBy, Skip or Limit. if (body.where()) { - last_op = std::make_unique(std::move(last_op), body.where()->expression_); + last_op = std::make_unique(std::move(last_op), std::vector>{}, + body.where()->expression_); } return last_op; } @@ -504,6 +496,12 @@ std::unique_ptr GenReturnBody(std::unique_ptr namespace impl { +bool HasBoundFilterSymbols(const std::unordered_set &bound_symbols, const FilterInfo &filter) { + return std::ranges::all_of( + filter.used_symbols.begin(), filter.used_symbols.end(), + [&bound_symbols](const auto &symbol) { return bound_symbols.find(symbol) != bound_symbols.end(); }); +} + Expression *ExtractFilters(const std::unordered_set &bound_symbols, Filters &filters, AstStorage &storage) { Expression *filter_expr = nullptr; for (auto filters_it = filters.begin(); filters_it != filters.end();) { @@ -517,16 +515,6 @@ Expression *ExtractFilters(const std::unordered_set &bound_symbols, Filt return filter_expr; } -std::unique_ptr GenFilters(std::unique_ptr last_op, - const std::unordered_set &bound_symbols, Filters &filters, - AstStorage &storage) { - auto *filter_expr = ExtractFilters(bound_symbols, filters, storage); - if (filter_expr) { - last_op = std::make_unique(std::move(last_op), filter_expr); - } - return last_op; -} - std::unique_ptr GenNamedPaths(std::unique_ptr last_op, std::unordered_set &bound_symbols, std::unordered_map> &named_paths) { diff --git a/src/query/plan/rule_based_planner.hpp b/src/query/plan/rule_based_planner.hpp index fed534583..5295c13b1 100644 --- a/src/query/plan/rule_based_planner.hpp +++ b/src/query/plan/rule_based_planner.hpp @@ -81,8 +81,8 @@ namespace impl { // removed from `Filters`. Expression *ExtractFilters(const std::unordered_set &, Filters &, AstStorage &); -std::unique_ptr GenFilters(std::unique_ptr, const std::unordered_set &, - Filters &, AstStorage &); +/// Checks if the filters has all the bound symbols to be included in the current part of the query +bool HasBoundFilterSymbols(const std::unordered_set &bound_symbols, const FilterInfo &filter); /// Utility function for iterating pattern atoms and accumulating a result. /// @@ -398,119 +398,11 @@ class RuleBasedPlanner { // Try to generate any filters even before the 1st match operator. This // optimizes the optional match which filters only on symbols bound in // regular match. - auto last_op = impl::GenFilters(std::move(input_op), bound_symbols, filters, storage); - for (const auto &expansion : matching.expansions) { - const auto &node1_symbol = symbol_table.at(*expansion.node1->identifier_); - if (bound_symbols.insert(node1_symbol).second) { - // We have just bound this symbol, so generate ScanAll which fills it. - last_op = std::make_unique(std::move(last_op), node1_symbol, match_context.view); - match_context.new_symbols.emplace_back(node1_symbol); - last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage); - last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths); - last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage); - } - // We have an edge, so generate Expand. - if (expansion.edge) { - auto *edge = expansion.edge; - // If the expand symbols were already bound, then we need to indicate - // that they exist. The Expand will then check whether the pattern holds - // instead of writing the expansion to symbols. - const auto &node_symbol = symbol_table.at(*expansion.node2->identifier_); - auto existing_node = utils::Contains(bound_symbols, node_symbol); - const auto &edge_symbol = symbol_table.at(*edge->identifier_); - MG_ASSERT(!utils::Contains(bound_symbols, edge_symbol), "Existing edges are not supported"); - std::vector edge_types; - edge_types.reserve(edge->edge_types_.size()); - for (const auto &type : edge->edge_types_) { - edge_types.push_back(GetEdgeType(type)); - } - if (edge->IsVariable()) { - std::optional weight_lambda; - std::optional total_weight; + auto last_op = GenFilters(std::move(input_op), bound_symbols, filters, storage, symbol_table); - if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || - edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { - weight_lambda.emplace(ExpansionLambda{symbol_table.at(*edge->weight_lambda_.inner_edge), - symbol_table.at(*edge->weight_lambda_.inner_node), - edge->weight_lambda_.expression}); + last_op = HandleExpansion(std::move(last_op), matching, symbol_table, storage, bound_symbols, + match_context.new_symbols, named_paths, filters, match_context.view); - total_weight.emplace(symbol_table.at(*edge->total_weight_)); - } - - ExpansionLambda filter_lambda; - filter_lambda.inner_edge_symbol = symbol_table.at(*edge->filter_lambda_.inner_edge); - filter_lambda.inner_node_symbol = symbol_table.at(*edge->filter_lambda_.inner_node); - { - // Bind the inner edge and node symbols so they're available for - // inline filtering in ExpandVariable. - bool inner_edge_bound = bound_symbols.insert(filter_lambda.inner_edge_symbol).second; - bool inner_node_bound = bound_symbols.insert(filter_lambda.inner_node_symbol).second; - MG_ASSERT(inner_edge_bound && inner_node_bound, "An inner edge and node can't be bound from before"); - } - // Join regular filters with lambda filter expression, so that they - // are done inline together. Semantic analysis should guarantee that - // lambda filtering uses bound symbols. - filter_lambda.expression = impl::BoolJoin( - storage, impl::ExtractFilters(bound_symbols, filters, storage), edge->filter_lambda_.expression); - // At this point it's possible we have leftover filters for inline - // filtering (they use the inner symbols. If they were not collected, - // we have to remove them manually because no other filter-extraction - // will ever bind them again. - filters.erase(std::remove_if( - filters.begin(), filters.end(), - [e = filter_lambda.inner_edge_symbol, n = filter_lambda.inner_node_symbol](FilterInfo &fi) { - return utils::Contains(fi.used_symbols, e) || utils::Contains(fi.used_symbols, n); - }), - filters.end()); - // Unbind the temporarily bound inner symbols for filtering. - bound_symbols.erase(filter_lambda.inner_edge_symbol); - bound_symbols.erase(filter_lambda.inner_node_symbol); - - if (total_weight) { - bound_symbols.insert(*total_weight); - } - - // TODO: Pass weight lambda. - MG_ASSERT(match_context.view == storage::View::OLD, - "ExpandVariable should only be planned with storage::View::OLD"); - last_op = std::make_unique(std::move(last_op), node1_symbol, node_symbol, edge_symbol, - edge->type_, expansion.direction, edge_types, expansion.is_flipped, - edge->lower_bound_, edge->upper_bound_, existing_node, - filter_lambda, weight_lambda, total_weight); - } else { - last_op = std::make_unique(std::move(last_op), node1_symbol, node_symbol, edge_symbol, - expansion.direction, edge_types, existing_node, match_context.view); - } - - // Bind the expanded edge and node. - bound_symbols.insert(edge_symbol); - match_context.new_symbols.emplace_back(edge_symbol); - if (bound_symbols.insert(node_symbol).second) { - match_context.new_symbols.emplace_back(node_symbol); - } - - // Ensure Cyphermorphism (different edge symbols always map to - // different edges). - for (const auto &edge_symbols : matching.edge_symbols) { - if (edge_symbols.find(edge_symbol) == edge_symbols.end()) { - continue; - } - std::vector other_symbols; - for (const auto &symbol : edge_symbols) { - if (symbol == edge_symbol || bound_symbols.find(symbol) == bound_symbols.end()) { - continue; - } - other_symbols.push_back(symbol); - } - if (!other_symbols.empty()) { - last_op = std::make_unique(std::move(last_op), edge_symbol, other_symbols); - } - } - last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage); - last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths); - last_op = impl::GenFilters(std::move(last_op), bound_symbols, filters, storage); - } - } MG_ASSERT(named_paths.empty(), "Expected to generate all named paths"); // We bound all named path symbols, so just add them to new_symbols. for (const auto &named_path : matching.named_paths) { @@ -547,6 +439,143 @@ class RuleBasedPlanner { return std::make_unique(std::move(input_op), std::move(on_match), std::move(on_create)); } + std::unique_ptr HandleExpansion(std::unique_ptr last_op, const Matching &matching, + const SymbolTable &symbol_table, AstStorage &storage, + std::unordered_set &bound_symbols, + std::vector &new_symbols, + std::unordered_map> &named_paths, + Filters &filters, storage::View view) { + for (const auto &expansion : matching.expansions) { + const auto &node1_symbol = symbol_table.at(*expansion.node1->identifier_); + if (bound_symbols.insert(node1_symbol).second) { + // We have just bound this symbol, so generate ScanAll which fills it. + last_op = std::make_unique(std::move(last_op), node1_symbol, view); + new_symbols.emplace_back(node1_symbol); + + last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table); + last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths); + last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table); + } + + if (expansion.edge) { + last_op = GenExpand(std::move(last_op), expansion, symbol_table, bound_symbols, matching, storage, filters, + named_paths, new_symbols, view); + } + } + + return last_op; + } + + std::unique_ptr GenExpand(std::unique_ptr last_op, const Expansion &expansion, + const SymbolTable &symbol_table, std::unordered_set &bound_symbols, + const Matching &matching, AstStorage &storage, Filters &filters, + std::unordered_map> &named_paths, + std::vector &new_symbols, storage::View view) { + // If the expand symbols were already bound, then we need to indicate + // that they exist. The Expand will then check whether the pattern holds + // instead of writing the expansion to symbols. + const auto &node1_symbol = symbol_table.at(*expansion.node1->identifier_); + bound_symbols.insert(node1_symbol); + + const auto &node_symbol = symbol_table.at(*expansion.node2->identifier_); + auto *edge = expansion.edge; + + auto existing_node = utils::Contains(bound_symbols, node_symbol); + const auto &edge_symbol = symbol_table.at(*edge->identifier_); + MG_ASSERT(!utils::Contains(bound_symbols, edge_symbol), "Existing edges are not supported"); + std::vector edge_types; + edge_types.reserve(edge->edge_types_.size()); + for (const auto &type : edge->edge_types_) { + edge_types.push_back(GetEdgeType(type)); + } + if (edge->IsVariable()) { + std::optional weight_lambda; + std::optional total_weight; + + if (edge->type_ == EdgeAtom::Type::WEIGHTED_SHORTEST_PATH || edge->type_ == EdgeAtom::Type::ALL_SHORTEST_PATHS) { + weight_lambda.emplace(ExpansionLambda{symbol_table.at(*edge->weight_lambda_.inner_edge), + symbol_table.at(*edge->weight_lambda_.inner_node), + edge->weight_lambda_.expression}); + + total_weight.emplace(symbol_table.at(*edge->total_weight_)); + } + + ExpansionLambda filter_lambda; + filter_lambda.inner_edge_symbol = symbol_table.at(*edge->filter_lambda_.inner_edge); + filter_lambda.inner_node_symbol = symbol_table.at(*edge->filter_lambda_.inner_node); + { + // Bind the inner edge and node symbols so they're available for + // inline filtering in ExpandVariable. + bool inner_edge_bound = bound_symbols.insert(filter_lambda.inner_edge_symbol).second; + bool inner_node_bound = bound_symbols.insert(filter_lambda.inner_node_symbol).second; + MG_ASSERT(inner_edge_bound && inner_node_bound, "An inner edge and node can't be bound from before"); + } + // Join regular filters with lambda filter expression, so that they + // are done inline together. Semantic analysis should guarantee that + // lambda filtering uses bound symbols. + filter_lambda.expression = impl::BoolJoin( + storage, impl::ExtractFilters(bound_symbols, filters, storage), edge->filter_lambda_.expression); + // At this point it's possible we have leftover filters for inline + // filtering (they use the inner symbols. If they were not collected, + // we have to remove them manually because no other filter-extraction + // will ever bind them again. + filters.erase( + std::remove_if(filters.begin(), filters.end(), + [e = filter_lambda.inner_edge_symbol, n = filter_lambda.inner_node_symbol](FilterInfo &fi) { + return utils::Contains(fi.used_symbols, e) || utils::Contains(fi.used_symbols, n); + }), + filters.end()); + // Unbind the temporarily bound inner symbols for filtering. + bound_symbols.erase(filter_lambda.inner_edge_symbol); + bound_symbols.erase(filter_lambda.inner_node_symbol); + + if (total_weight) { + bound_symbols.insert(*total_weight); + } + + // TODO: Pass weight lambda. + MG_ASSERT(view == storage::View::OLD, "ExpandVariable should only be planned with storage::View::OLD"); + last_op = std::make_unique(std::move(last_op), node1_symbol, node_symbol, edge_symbol, + edge->type_, expansion.direction, edge_types, expansion.is_flipped, + edge->lower_bound_, edge->upper_bound_, existing_node, filter_lambda, + weight_lambda, total_weight); + } else { + last_op = std::make_unique(std::move(last_op), node1_symbol, node_symbol, edge_symbol, + expansion.direction, edge_types, existing_node, view); + } + + // Bind the expanded edge and node. + bound_symbols.insert(edge_symbol); + new_symbols.emplace_back(edge_symbol); + if (bound_symbols.insert(node_symbol).second) { + new_symbols.emplace_back(node_symbol); + } + + // Ensure Cyphermorphism (different edge symbols always map to + // different edges). + for (const auto &edge_symbols : matching.edge_symbols) { + if (edge_symbols.find(edge_symbol) == edge_symbols.end()) { + continue; + } + std::vector other_symbols; + for (const auto &symbol : edge_symbols) { + if (symbol == edge_symbol || bound_symbols.find(symbol) == bound_symbols.end()) { + continue; + } + other_symbols.push_back(symbol); + } + if (!other_symbols.empty()) { + last_op = std::make_unique(std::move(last_op), edge_symbol, other_symbols); + } + } + + last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table); + last_op = impl::GenNamedPaths(std::move(last_op), bound_symbols, named_paths); + last_op = GenFilters(std::move(last_op), bound_symbols, filters, storage, symbol_table); + + return last_op; + } + std::unique_ptr HandleForeachClause(query::Foreach *foreach, std::unique_ptr input_op, const SymbolTable &symbol_table, @@ -567,6 +596,64 @@ class RuleBasedPlanner { return std::make_unique(std::move(input_op), std::move(op), foreach->named_expression_->expression_, symbol); } + + std::unique_ptr GenFilters(std::unique_ptr last_op, + const std::unordered_set &bound_symbols, Filters &filters, + AstStorage &storage, const SymbolTable &symbol_table) { + auto pattern_filters = ExtractPatternFilters(filters, symbol_table, storage, bound_symbols); + auto *filter_expr = impl::ExtractFilters(bound_symbols, filters, storage); + + if (filter_expr) { + last_op = std::make_unique(std::move(last_op), std::move(pattern_filters), filter_expr); + } + return last_op; + } + + std::unique_ptr MakeExistsFilter(const FilterMatching &matching, const SymbolTable &symbol_table, + AstStorage &storage, + const std::unordered_set &bound_symbols) { + std::vector once_symbols(bound_symbols.begin(), bound_symbols.end()); + std::unique_ptr last_op = std::make_unique(once_symbols); + + std::vector new_symbols; + std::unordered_set expand_symbols(bound_symbols.begin(), bound_symbols.end()); + + auto filters = matching.filters; + + std::unordered_map> named_paths; + + last_op = HandleExpansion(std::move(last_op), matching, symbol_table, storage, expand_symbols, new_symbols, + named_paths, filters, storage::View::OLD); + + last_op = std::make_unique(std::move(last_op), storage.Create(1)); + + last_op = std::make_unique(std::move(last_op), matching.symbol.value()); + + return last_op; + } + + std::vector> ExtractPatternFilters(Filters &filters, const SymbolTable &symbol_table, + AstStorage &storage, + const std::unordered_set &bound_symbols) { + std::vector> operators; + + for (const auto &filter : filters) { + for (const auto &matching : filter.matchings) { + if (!impl::HasBoundFilterSymbols(bound_symbols, filter)) { + continue; + } + + switch (matching.type) { + case PatternFilterType::EXISTS: { + operators.push_back(MakeExistsFilter(matching, symbol_table, storage, bound_symbols)); + break; + } + } + } + } + + return operators; + } }; } // namespace memgraph::query::plan diff --git a/src/query/plan/variable_start_planner.cpp b/src/query/plan/variable_start_planner.cpp index 9c8e0de4c..eb30226d8 100644 --- a/src/query/plan/variable_start_planner.cpp +++ b/src/query/plan/variable_start_planner.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 @@ -155,6 +155,18 @@ auto ExpansionNodes(const std::vector &expansions, const SymbolTable return nodes; } +FilterMatching ToFilterMatching(Matching &matching) { + FilterMatching filter_matching; + filter_matching.expansions = matching.expansions; + filter_matching.edge_symbols = matching.edge_symbols; + filter_matching.filters = matching.filters; + filter_matching.node_symbol_to_expansions = matching.node_symbol_to_expansions; + filter_matching.named_paths = matching.named_paths; + filter_matching.expansion_symbols = matching.expansion_symbols; + + return filter_matching; +} + } // namespace VaryMatchingStart::VaryMatchingStart(Matching matching, const SymbolTable &symbol_table) @@ -209,11 +221,31 @@ CartesianProduct VaryMultiMatchingStarts(const std::vector VaryFilterMatchingStarts(const Matching &matching, + const SymbolTable &symbol_table) { + auto filter_matchings_cnt = 0; + for (const auto &filter : matching.filters) { + filter_matchings_cnt += static_cast(filter.matchings.size()); + } + + std::vector variants; + variants.reserve(filter_matchings_cnt); + + for (const auto &filter : matching.filters) { + for (const auto &filter_matching : filter.matchings) { + variants.emplace_back(filter_matching, symbol_table); + } + } + + return MakeCartesianProduct(std::move(variants)); +} + VaryQueryPartMatching::VaryQueryPartMatching(SingleQueryPart query_part, const SymbolTable &symbol_table) : query_part_(std::move(query_part)), matchings_(VaryMatchingStart(query_part_.matching, symbol_table)), optional_matchings_(VaryMultiMatchingStarts(query_part_.optional_matching, symbol_table)), - merge_matchings_(VaryMultiMatchingStarts(query_part_.merge_matching, symbol_table)) {} + merge_matchings_(VaryMultiMatchingStarts(query_part_.merge_matching, symbol_table)), + filter_matchings_(VaryFilterMatchingStarts(query_part_.matching, symbol_table)) {} VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part, VaryMatchingStart::iterator matchings_begin, @@ -221,7 +253,9 @@ VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part, CartesianProduct::iterator optional_begin, CartesianProduct::iterator optional_end, CartesianProduct::iterator merge_begin, - CartesianProduct::iterator merge_end) + CartesianProduct::iterator merge_end, + CartesianProduct::iterator filter_begin, + CartesianProduct::iterator filter_end) : current_query_part_(query_part), matchings_it_(matchings_begin), matchings_end_(matchings_end), @@ -230,7 +264,10 @@ VaryQueryPartMatching::iterator::iterator(const SingleQueryPart &query_part, optional_end_(optional_end), merge_it_(merge_begin), merge_begin_(merge_begin), - merge_end_(merge_end) { + merge_end_(merge_end), + filter_it_(filter_begin), + filter_begin_(filter_begin), + filter_end_(filter_end) { if (matchings_it_ != matchings_end_) { // Fill the query part with the first variation of matchings SetCurrentQueryPart(); @@ -242,26 +279,37 @@ VaryQueryPartMatching::iterator &VaryQueryPartMatching::iterator::operator++() { // * matchings (m1) and (m2) // * optional matchings (o1) and (o2) // * merge matching (g1) + // * filter matching (f1) and (f2) // We want to produce parts for: - // * (m1), (o1), (g1) - // * (m1), (o2), (g1) - // * (m2), (o1), (g1) - // * (m2), (o2), (g1) - // Create variations by changing the merge part first. - if (merge_it_ != merge_end_) ++merge_it_; - // If all merge variations are done, start them from beginning and move to the - // next optional matching variation. - if (merge_it_ == merge_end_) { + // * (m1), (o1), (g1), (f1) + // * (m1), (o1), (g1), (f2) + // * (m1), (o2), (g1), (f1) + // * (m1), (o2), (g1), (f2) + // * (m2), (o1), (g1), (f1) + // * (m2), (o1), (g1), (f2) + // * (m2), (o2), (g1), (f1) + // * (m2), (o2), (g1), (f2) + + // Create variations by changing the filter part first. + if (filter_it_ != filter_end_) ++filter_it_; + + // Create variations by changing the merge part. + if (filter_it_ == filter_end_) { + filter_it_ = filter_begin_; + if (merge_it_ != merge_end_) ++merge_it_; + } + + // Create variations by changing the optional part. + if (merge_it_ == merge_end_ && filter_it_ == filter_begin_) { merge_it_ = merge_begin_; if (optional_it_ != optional_end_) ++optional_it_; } - // If all optional matching variations are done (after exhausting merge - // variations), start them from beginning and move to the next regular - // matching variation. - if (optional_it_ == optional_end_ && merge_it_ == merge_begin_) { + + if (optional_it_ == optional_end_ && merge_it_ == merge_begin_ && filter_it_ == filter_begin_) { optional_it_ = optional_begin_; if (matchings_it_ != matchings_end_) ++matchings_it_; } + // We have reached the end, so return; if (matchings_it_ == matchings_end_) return *this; // Fill the query part with the new variation of matchings. @@ -283,6 +331,28 @@ void VaryQueryPartMatching::iterator::SetCurrentQueryPart() { if (merge_it_ != merge_end_) { current_query_part_.merge_matching = *merge_it_; } + DMG_ASSERT(filter_it_ != filter_end_ || filter_begin_ == filter_end_, + "Either there are no filter matchings or we can always generate" + "a variation"); + + auto all_filter_matchings = *filter_it_; + auto all_filter_matchings_idx = 0; + for (auto &filter : current_query_part_.matching.filters) { + auto matchings_size = filter.matchings.size(); + + std::vector new_matchings; + new_matchings.reserve(matchings_size); + + for (auto i = 0; i < matchings_size; i++) { + new_matchings.push_back(ToFilterMatching(all_filter_matchings[all_filter_matchings_idx])); + new_matchings[i].symbol = filter.matchings[i].symbol; + new_matchings[i].type = filter.matchings[i].type; + + all_filter_matchings_idx++; + } + + filter.matchings = std::move(new_matchings); + } } bool VaryQueryPartMatching::iterator::operator==(const iterator &other) const { @@ -291,7 +361,8 @@ bool VaryQueryPartMatching::iterator::operator==(const iterator &other) const { // iterators can be at any position. return true; } - return matchings_it_ == other.matchings_it_ && optional_it_ == other.optional_it_ && merge_it_ == other.merge_it_; + return matchings_it_ == other.matchings_it_ && optional_it_ == other.optional_it_ && merge_it_ == other.merge_it_ && + filter_it_ == other.filter_it_; } } // namespace memgraph::query::plan::impl diff --git a/src/query/plan/variable_start_planner.hpp b/src/query/plan/variable_start_planner.hpp index f6c9dbeac..171b9b80c 100644 --- a/src/query/plan/variable_start_planner.hpp +++ b/src/query/plan/variable_start_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 @@ -230,6 +230,8 @@ class VaryMatchingStart { // Cartesian product of all of them is returned. CartesianProduct VaryMultiMatchingStarts(const std::vector &, const SymbolTable &); +CartesianProduct VaryFilterMatchingStarts(const Matching &matching, const SymbolTable &symbol_table); + // Produces alternative query parts out of a single part by varying how each // graph matching is done. class VaryQueryPartMatching { @@ -245,6 +247,7 @@ class VaryQueryPartMatching { typedef const SingleQueryPart *pointer; iterator(const SingleQueryPart &, VaryMatchingStart::iterator, VaryMatchingStart::iterator, + CartesianProduct::iterator, CartesianProduct::iterator, CartesianProduct::iterator, CartesianProduct::iterator, CartesianProduct::iterator, CartesianProduct::iterator); @@ -266,15 +269,20 @@ class VaryQueryPartMatching { CartesianProduct::iterator merge_it_; CartesianProduct::iterator merge_begin_; CartesianProduct::iterator merge_end_; + CartesianProduct::iterator filter_it_; + CartesianProduct::iterator filter_begin_; + CartesianProduct::iterator filter_end_; }; auto begin() { return iterator(query_part_, matchings_.begin(), matchings_.end(), optional_matchings_.begin(), - optional_matchings_.end(), merge_matchings_.begin(), merge_matchings_.end()); + optional_matchings_.end(), merge_matchings_.begin(), merge_matchings_.end(), + filter_matchings_.begin(), filter_matchings_.end()); } auto end() { return iterator(query_part_, matchings_.end(), matchings_.end(), optional_matchings_.end(), - optional_matchings_.end(), merge_matchings_.end(), merge_matchings_.end()); + optional_matchings_.end(), merge_matchings_.end(), merge_matchings_.end(), filter_matchings_.end(), + filter_matchings_.end()); } private: @@ -286,6 +294,7 @@ class VaryQueryPartMatching { CartesianProduct optional_matchings_; // Like optional matching, but for merge matchings. CartesianProduct merge_matchings_; + CartesianProduct filter_matchings_; }; } // namespace impl diff --git a/src/utils/event_counter.cpp b/src/utils/event_counter.cpp index c011a4503..7dc9ff020 100644 --- a/src/utils/event_counter.cpp +++ b/src/utils/event_counter.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 @@ -51,6 +51,7 @@ M(CartesianOperator, "Number of times Cartesian operator was used.") \ M(CallProcedureOperator, "Number of times CallProcedure operator was used.") \ M(ForeachOperator, "Number of times Foreach operator was used.") \ + M(EvaluatePatternFilterOperator, "Number of times EvaluatePatternFilter operator was used.") \ \ M(FailedQuery, "Number of times executing a query failed.") \ M(LabelIndexCreated, "Number of times a label index was created.") \ diff --git a/tests/gql_behave/tests/memgraph_V1/features/memgraph_exists.feature b/tests/gql_behave/tests/memgraph_V1/features/memgraph_exists.feature new file mode 100644 index 000000000..fb24f6270 --- /dev/null +++ b/tests/gql_behave/tests/memgraph_V1/features/memgraph_exists.feature @@ -0,0 +1,529 @@ +Feature: WHERE exists + + Scenario: Test exists with empty edge and node specifiers + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists with empty edge and node specifiers return 2 entries + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two), (:One {prop: 3})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) RETURN n.prop ORDER BY n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + | 3 | + + Scenario: Test exists with edge specifier + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE]-()) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists with wrong edge specifier + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE2]-()) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with correct edge direction + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE]->()) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists with wrong edge direction + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)<-[:TYPE]-()) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with destination node label + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]->(:Two)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists with wrong destination node label + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]->(:Three)) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with destination node property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]->({prop: 2})) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists with wrong destination node property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]->({prop: 3})) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with edge property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 1}]->()) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists with wrong edge property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 2}]->()) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with both edge property and node label property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 1}]->(:Two {prop: 2})) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists with correct edge property and wrong node label property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 1}]->(:Two {prop: 3})) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with wrong edge property and correct node label property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 2}]->(:Two {prop:2})) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with wrong edge property and wrong node label property + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE {prop: 2}]->(:Two {prop:3})) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists AND exists + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE]->()) AND exists((n)-[]->(:Two)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists OR exists first condition + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE]->()) OR exists((n)-[]->(:Three)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists OR exists second condition + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE2]->()) OR exists((n)-[]->(:Two)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists OR exists fail + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE2]->()) OR exists((n)-[]->(:Three)) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test NOT exists + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2}) + """ + When executing query: + """ + MATCH (n:One) WHERE NOT exists((n)-[:TYPE2]->()) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test multi-hop first in sequence + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n) WHERE exists((n)-[]->()-[]->()) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test multi-hop in middle sequence + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n) WHERE exists(()-[]->(n)-[]->()) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 2 | + + Scenario: Test multi-hop at the end of the sequence + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n) WHERE exists(()-[]->()-[]->(n)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 3 | + + Scenario: Test multi-hop not exists + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n) WHERE exists(()-[]->(n)<-[]-()) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test multi-hop with filters + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n) WHERE exists(({prop: 1})-[:TYPE]->(n)-[{prop:2}]->(:Three)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 2 | + + Scenario: Test multi-hop with wrong filters + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n) WHERE exists(({prop: 1})-[:TYPE]->(n)-[:TYPE2]->(:Three)) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test node-only hop + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n) WHERE exists((n)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + | 2 | + | 3 | + + Scenario: Test exists with different edge type + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[:TYPE2]->()) RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists with correct edge type multiple edges + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two {prop: 10}), (:One {prop: 2})-[:TYPE]->(:Two {prop: 11}); + """ + When executing query: + """ + MATCH (n:Two) WHERE exists((n)<-[:TYPE]-()) RETURN n.prop ORDER BY n.prop; + """ + Then the result should be: + | n.prop | + | 10 | + | 11 | + + Scenario: Test exists does not work in WITH clauses + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:Two) WITH n WHERE exists((n)<-[:TYPE]-()) RETURN n.prop; + """ + Then an error should be raised + + Scenario: Test exists is not null + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) is not null RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists is null + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) is null RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists equal to true + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) = true RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists equal to true + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) = false RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists in list + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) in [true] RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test BFS hop + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE {prop: 1}]->(:Two {prop: 2})-[:TYPE {prop:2}]->(:Three {prop: 3}) + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[*bfs]->(:Three)) RETURN n.prop; + """ + Then the result should be: + | n.prop | + | 1 | + + Scenario: Test exists not in list + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:One) WHERE exists((n)-[]-()) in [false] RETURN n.prop; + """ + Then the result should be empty + + Scenario: Test exists on multihop patterns without results + Given an empty graph + And having executed: + """ + MATCH (n) DETACH DELETE n; + """ + When executing query: + """ + MATCH ()-[]-(m)-[]->(a) WHERE m.prop=1 and a.prop=3 and exists(()-[]->(m)) RETURN m, a; + """ + Then the result should be empty + + Scenario: Test exists does not work in SetProperty clauses + Given an empty graph + And having executed: + """ + CREATE (:One {prop:1})-[:TYPE]->(:Two); + """ + When executing query: + """ + MATCH (n:Two) SET n.prop = exists((n)<-[:TYPE]-()) RETURN n.prop; + """ + Then an error should be raised diff --git a/tests/unit/cypher_main_visitor.cpp b/tests/unit/cypher_main_visitor.cpp index 6ead1a7ad..aa3232a89 100644 --- a/tests/unit/cypher_main_visitor.cpp +++ b/tests/unit/cypher_main_visitor.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 @@ -4307,3 +4307,75 @@ TEST_P(CypherMainVisitorTest, Foreach) { ASSERT_TRUE(dynamic_cast(*++clauses.begin())); } } + +TEST_P(CypherMainVisitorTest, ExistsThrow) { + auto &ast_generator = *GetParam(); + + TestInvalidQueryWithMessage("MATCH (n) WHERE exists(p=(n)-[]->()) RETURN n;", ast_generator, + "Identifiers are not supported in exists(...)."); +} + +TEST_P(CypherMainVisitorTest, Exists) { + auto &ast_generator = *GetParam(); + { + const auto *query = + dynamic_cast(ast_generator.ParseQuery("MATCH (n) WHERE exists((n)-[]->()) RETURN n;")); + const auto *match = dynamic_cast(query->single_query_->clauses_[0]); + + const auto *exists = dynamic_cast(match->where_->expression_); + + ASSERT_TRUE(exists); + + const auto pattern = exists->pattern_; + ASSERT_TRUE(pattern->atoms_.size() == 3); + + const auto *node1 = dynamic_cast(pattern->atoms_[0]); + const auto *edge = dynamic_cast(pattern->atoms_[1]); + const auto *node2 = dynamic_cast(pattern->atoms_[2]); + + ASSERT_TRUE(node1); + ASSERT_TRUE(edge); + ASSERT_TRUE(node2); + } + + { + const auto *query = + dynamic_cast(ast_generator.ParseQuery("MATCH (n) WHERE exists((n)-[]->()-[]->()) RETURN n;")); + const auto *match = dynamic_cast(query->single_query_->clauses_[0]); + + const auto *exists = dynamic_cast(match->where_->expression_); + + ASSERT_TRUE(exists); + + const auto pattern = exists->pattern_; + ASSERT_TRUE(pattern->atoms_.size() == 5); + + const auto *node1 = dynamic_cast(pattern->atoms_[0]); + const auto *edge = dynamic_cast(pattern->atoms_[1]); + const auto *node2 = dynamic_cast(pattern->atoms_[2]); + const auto *edge2 = dynamic_cast(pattern->atoms_[3]); + const auto *node3 = dynamic_cast(pattern->atoms_[4]); + + ASSERT_TRUE(node1); + ASSERT_TRUE(edge); + ASSERT_TRUE(node2); + ASSERT_TRUE(edge2); + ASSERT_TRUE(node3); + } + + { + const auto *query = dynamic_cast(ast_generator.ParseQuery("MATCH (n) WHERE exists((n)) RETURN n;")); + const auto *match = dynamic_cast(query->single_query_->clauses_[0]); + + const auto *exists = dynamic_cast(match->where_->expression_); + + ASSERT_TRUE(exists); + + const auto pattern = exists->pattern_; + ASSERT_TRUE(pattern->atoms_.size() == 1); + + const auto *node = dynamic_cast(pattern->atoms_[0]); + + ASSERT_TRUE(node); + } +} diff --git a/tests/unit/plan_pretty_print.cpp b/tests/unit/plan_pretty_print.cpp index 5ad8b8e02..da24db9a2 100644 --- a/tests/unit/plan_pretty_print.cpp +++ b/tests/unit/plan_pretty_print.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 @@ -378,7 +378,8 @@ TEST_F(PrintToJsonTest, ConstructNamedPath) { TEST_F(PrintToJsonTest, Filter) { std::shared_ptr last_op = std::make_shared(nullptr, GetSymbol("node1")); - last_op = std::make_shared(last_op, EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(5))); + last_op = std::make_shared(last_op, std::vector>{}, + EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(5))); Check(last_op.get(), R"sep( { @@ -995,3 +996,55 @@ TEST_F(PrintToJsonTest, Foreach) { } })sep"); } + +TEST_F(PrintToJsonTest, Exists) { + Symbol x = GetSymbol("x"); + Symbol e = GetSymbol("edge"); + Symbol n = GetSymbol("node"); + Symbol output = GetSymbol("output_symbol"); + std::shared_ptr last_op = std::make_shared(nullptr, x); + std::shared_ptr expand = std::make_shared( + nullptr, x, n, e, memgraph::query::EdgeAtom::Direction::BOTH, + std::vector{dba.NameToEdgeType("EdgeType1")}, false, memgraph::storage::View::OLD); + std::shared_ptr limit = std::make_shared(expand, LITERAL(1)); + std::shared_ptr evaluate_pattern_filter = std::make_shared(limit, output); + last_op = std::make_shared( + last_op, std::vector>{evaluate_pattern_filter}, + EXISTS(PATTERN(NODE("x"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {}, false), + NODE("node", std::nullopt, false)))); + + Check(last_op.get(), R"sep( + { + "expression": "(Exists expression)", + "input": { + "input": { + "name": "Once" + }, + "name": "ScanAll", + "output_symbol": "x" + }, + "name": "Filter", + "pattern_filter1": { + "input": { + "expression": "1", + "input": { + "direction": "both", + "edge_symbol": "edge", + "edge_types": [ + "EdgeType1" + ], + "existing_node": false, + "input": { + "name": "Once" + }, + "input_symbol": "x", + "name": "Expand", + "node_symbol": "node" + }, + "name": "Limit" + }, + "name": "EvaluatePatternFilter", + "output_symbol": "output_symbol" + } + })sep"); +} diff --git a/tests/unit/query_common.hpp b/tests/unit/query_common.hpp index 4017a1656..e21ef291f 100644 --- a/tests/unit/query_common.hpp +++ b/tests/unit/query_common.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 @@ -157,13 +157,13 @@ auto GetPropertyLookup(AstStorage &storage, TDbAccessor &, Expression *expr, /// /// Name is used to create the Identifier which is assigned to the edge. auto GetEdge(AstStorage &storage, const std::string &name, EdgeAtom::Direction dir = EdgeAtom::Direction::BOTH, - const std::vector &edge_types = {}) { + const std::vector &edge_types = {}, const bool user_declared = true) { std::vector types; types.reserve(edge_types.size()); for (const auto &type : edge_types) { types.push_back(storage.GetEdgeTypeIx(type)); } - return storage.Create(storage.Create(name), EdgeAtom::Type::SINGLE, dir, types); + return storage.Create(storage.Create(name, user_declared), EdgeAtom::Type::SINGLE, dir, types); } /// Create a variable length expansion EdgeAtom with given name, direction and @@ -205,8 +205,9 @@ auto GetEdgeVariable(AstStorage &storage, const std::string &name, EdgeAtom::Typ /// Create a NodeAtom with given name and label. /// /// Name is used to create the Identifier which is assigned to the node. -auto GetNode(AstStorage &storage, const std::string &name, std::optional label = std::nullopt) { - auto node = storage.Create(storage.Create(name)); +auto GetNode(AstStorage &storage, const std::string &name, std::optional label = std::nullopt, + const bool user_declared = true) { + auto node = storage.Create(storage.Create(name, user_declared)); if (label) node->labels_.emplace_back(storage.GetLabelIx(*label)); return node; } @@ -586,6 +587,7 @@ auto GetForeach(AstStorage &storage, NamedExpression *named_expr, const std::vec #define COALESCE(...) storage.Create(std::vector{__VA_ARGS__}) #define EXTRACT(variable, list, expr) \ storage.Create(storage.Create(variable), list, expr) +#define EXISTS(pattern) storage.Create(pattern) #define AUTH_QUERY(action, user, role, user_or_role, password, privileges, labels, edgeTypes) \ storage.Create((action), (user), (role), (user_or_role), password, (privileges), \ (labels), (edgeTypes)) diff --git a/tests/unit/query_cost_estimator.cpp b/tests/unit/query_cost_estimator.cpp index 86ba6cd1d..a574f97d3 100644 --- a/tests/unit/query_cost_estimator.cpp +++ b/tests/unit/query_cost_estimator.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 @@ -204,7 +204,8 @@ TEST_F(QueryCostEstimator, Foreach) { EXPECT_COST(OP_COST_PARAM + OP_CARD_PARAM * OP_COST_PARAM); TEST_F(QueryCostEstimator, Filter) { - TEST_OP(MakeOp(last_op_, Literal(true)), CostParam::kFilter, CardParam::kFilter); + TEST_OP(MakeOp(last_op_, std::vector>{}, Literal(true)), CostParam::kFilter, + CardParam::kFilter); } TEST_F(QueryCostEstimator, EdgeUniquenessFilter) { diff --git a/tests/unit/query_plan.cpp b/tests/unit/query_plan.cpp index ccd38aa47..1cc3cd59e 100644 --- a/tests/unit/query_plan.cpp +++ b/tests/unit/query_plan.cpp @@ -1735,4 +1735,119 @@ TYPED_TEST(TestPlanner, Foreach) { DeleteListContent(&on_create); } } + +TYPED_TEST(TestPlanner, Exists) { + AstStorage storage; + FakeDbAccessor dba; + + // MATCH (n) WHERE exists((n)-[]-()) + { + auto *query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n"))), + WHERE(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {}, false), + NODE("node", std::nullopt, false)))), + RETURN("n"))); + + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, storage, symbol_table, query); + std::list pattern_filter{new ExpectExpand(), new ExpectLimit(), new ExpectEvaluatePatternFilter()}; + + CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), + ExpectFilter(std::vector>{pattern_filter}), ExpectProduce()); + + DeleteListContent(&pattern_filter); + } + + // MATCH (n) WHERE exists((n)-[:TYPE]-(:Two)) + { + auto *query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n"))), + WHERE(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false), + NODE("node", "Two", false)))), + RETURN("n"))); + + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, storage, symbol_table, query); + std::list pattern_filter{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(), + new ExpectEvaluatePatternFilter()}; + + CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), + ExpectFilter(std::vector>{pattern_filter}), ExpectProduce()); + + DeleteListContent(&pattern_filter); + } + + // MATCH (n) WHERE exists((n)-[:TYPE]-(:Two)) AND exists((n)-[]-()) + { + auto *query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n"))), + WHERE(AND(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false), + NODE("node", "Two", false))), + EXISTS(PATTERN(NODE("n"), EDGE("edge2", memgraph::query::EdgeAtom::Direction::BOTH, {}, false), + NODE("node2", std::nullopt, false))))), + RETURN("n"))); + + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, storage, symbol_table, query); + std::list pattern_filter_with_types{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(), + new ExpectEvaluatePatternFilter()}; + std::list pattern_filter_without_types{new ExpectExpand(), new ExpectLimit(), + new ExpectEvaluatePatternFilter()}; + + CheckPlan( + planner.plan(), symbol_table, ExpectScanAll(), + ExpectFilter(std::vector>{pattern_filter_without_types, pattern_filter_with_types}), + ExpectProduce()); + + DeleteListContent(&pattern_filter_with_types); + DeleteListContent(&pattern_filter_without_types); + } + + // MATCH (n) WHERE n.prop = 1 AND exists((n)-[:TYPE]-(:Two)) + { + auto property = dba.Property("prop"); + auto *query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n"))), + WHERE(AND(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false), + NODE("node", "Two", false))), + PROPERTY_LOOKUP("n", property))), + RETURN("n"))); + + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, storage, symbol_table, query); + std::list pattern_filter{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(), + new ExpectEvaluatePatternFilter()}; + + CheckPlan(planner.plan(), symbol_table, ExpectScanAll(), + ExpectFilter(std::vector>{pattern_filter}), ExpectProduce()); + + DeleteListContent(&pattern_filter); + } + + // MATCH (n) WHERE exists((n)-[:TYPE]-(:Two)) OR exists((n)-[]-()) + { + auto *query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n"))), + WHERE(OR(EXISTS(PATTERN(NODE("n"), EDGE("edge", memgraph::query::EdgeAtom::Direction::BOTH, {"TYPE"}, false), + NODE("node", "Two", false))), + EXISTS(PATTERN(NODE("n"), EDGE("edge2", memgraph::query::EdgeAtom::Direction::BOTH, {}, false), + NODE("node2", std::nullopt, false))))), + RETURN("n"))); + + auto symbol_table = memgraph::query::MakeSymbolTable(query); + auto planner = MakePlanner(&dba, storage, symbol_table, query); + std::list pattern_filter_with_types{new ExpectExpand(), new ExpectFilter(), new ExpectLimit(), + new ExpectEvaluatePatternFilter()}; + std::list pattern_filter_without_types{new ExpectExpand(), new ExpectLimit(), + new ExpectEvaluatePatternFilter()}; + + CheckPlan( + planner.plan(), symbol_table, ExpectScanAll(), + ExpectFilter(std::vector>{pattern_filter_with_types, pattern_filter_without_types}), + ExpectProduce()); + + DeleteListContent(&pattern_filter_with_types); + DeleteListContent(&pattern_filter_without_types); + } +} } // namespace diff --git a/tests/unit/query_plan_checker.hpp b/tests/unit/query_plan_checker.hpp index 1a6e2fcb4..e48a27b29 100644 --- a/tests/unit/query_plan_checker.hpp +++ b/tests/unit/query_plan_checker.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 @@ -64,7 +64,6 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor { PRE_VISIT(ScanAllById); PRE_VISIT(Expand); PRE_VISIT(ExpandVariable); - PRE_VISIT(Filter); PRE_VISIT(ConstructNamedPath); PRE_VISIT(EmptyResult); PRE_VISIT(Produce); @@ -79,6 +78,7 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor { PRE_VISIT(Skip); PRE_VISIT(Limit); PRE_VISIT(OrderBy); + PRE_VISIT(EvaluatePatternFilter); bool PreVisit(Merge &op) override { CheckOp(op); op.input()->Accept(*this); @@ -97,6 +97,12 @@ class PlanChecker : public virtual HierarchicalLogicalOperatorVisitor { return false; } + bool PreVisit(Filter &op) override { + CheckOp(op); + op.input()->Accept(*this); + return false; + } + bool Visit(Once &) override { // Ignore checking Once, it is implicitly at the end. return true; @@ -141,7 +147,6 @@ using ExpectScanAll = OpChecker; using ExpectScanAllByLabel = OpChecker; using ExpectScanAllById = OpChecker; using ExpectExpand = OpChecker; -using ExpectFilter = OpChecker; using ExpectConstructNamedPath = OpChecker; using ExpectProduce = OpChecker; using ExpectEmptyResult = OpChecker; @@ -156,6 +161,23 @@ using ExpectLimit = OpChecker; using ExpectOrderBy = OpChecker; using ExpectUnwind = OpChecker; using ExpectDistinct = OpChecker; +using ExpectEvaluatePatternFilter = OpChecker; + +class ExpectFilter : public OpChecker { + public: + ExpectFilter(const std::vector> &pattern_filters = {}) + : pattern_filters_(pattern_filters) {} + + void ExpectOp(Filter &filter, const SymbolTable &symbol_table) override { + for (auto i = 0; i < filter.pattern_filters_.size(); i++) { + PlanChecker check_updates(pattern_filters_[i], symbol_table); + + filter.pattern_filters_[i]->Accept(check_updates); + } + } + + std::vector> pattern_filters_; +}; class ExpectForeach : public OpChecker { public: diff --git a/tests/unit/query_plan_create_set_remove_delete.cpp b/tests/unit/query_plan_create_set_remove_delete.cpp index 8985a4f28..54c643ae1 100644 --- a/tests/unit/query_plan_create_set_remove_delete.cpp +++ b/tests/unit/query_plan_create_set_remove_delete.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 @@ -1532,7 +1532,7 @@ TEST(QueryPlan, NodeFilterSet) { false, memgraph::storage::View::OLD); auto *filter_expr = EQ(storage.Create(scan_all.node_->identifier_, storage.GetPropertyIx(prop.first)), LITERAL(42)); - auto node_filter = std::make_shared(expand.op_, filter_expr); + auto node_filter = std::make_shared(expand.op_, std::vector>{}, filter_expr); // SET n.prop = n.prop + 1 auto set_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop); auto add = ADD(set_prop, LITERAL(1)); @@ -1569,7 +1569,8 @@ TEST(QueryPlan, FilterRemove) { auto expand = MakeExpand(storage, symbol_table, scan_all.op_, scan_all.sym_, "r", EdgeAtom::Direction::BOTH, {}, "m", false, memgraph::storage::View::OLD); auto filter_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop); - auto filter = std::make_shared(expand.op_, LESS(filter_prop, LITERAL(43))); + auto filter = std::make_shared(expand.op_, std::vector>{}, + LESS(filter_prop, LITERAL(43))); // REMOVE n.prop auto rem_prop = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), prop); auto rem = std::make_shared(filter, prop.second, rem_prop); diff --git a/tests/unit/query_plan_match_filter_return.cpp b/tests/unit/query_plan_match_filter_return.cpp index b11eb92cf..05c5b218b 100644 --- a/tests/unit/query_plan_match_filter_return.cpp +++ b/tests/unit/query_plan_match_filter_return.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 @@ -292,7 +292,7 @@ TEST(QueryPlan, NodeFilterLabelsAndProperties) { // node filtering auto *filter_expr = AND(storage.Create(n.node_->identifier_, n.node_->labels_), EQ(PROPERTY_LOOKUP(n.node_->identifier_, property), LITERAL(42))); - auto node_filter = std::make_shared(n.op_, filter_expr); + auto node_filter = std::make_shared(n.op_, std::vector>{}, filter_expr); // make a named expression and a produce auto output = NEXPR("x", IDENT("n")->MapTo(n.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true)); @@ -344,7 +344,7 @@ TEST(QueryPlan, NodeFilterMultipleLabels) { // node filtering auto *filter_expr = storage.Create(n.node_->identifier_, n.node_->labels_); - auto node_filter = std::make_shared(n.op_, filter_expr); + auto node_filter = std::make_shared(n.op_, std::vector>{}, filter_expr); // make a named expression and a produce auto output = NEXPR("n", IDENT("n")->MapTo(n.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true)); @@ -679,7 +679,7 @@ class QueryPlanExpandVariable : public testing::Test { bool is_reverse = false) { auto n_from = MakeScanAll(storage, symbol_table, node_from, input_op); auto filter_op = std::make_shared( - n_from.op_, + n_from.op_, std::vector>{}, storage.Create( n_from.node_->identifier_, std::vector{storage.GetLabelIx(dba.LabelToName(labels[layer]))})); @@ -1355,7 +1355,7 @@ TEST_F(QueryPlanExpandVariable, ExpandToSameSymbol) { auto n_from = ScanAllTuple{node, logical_op, symbol}; auto filter_op = std::make_shared( - n_from.op_, + n_from.op_, std::vector>{}, storage.Create( n_from.node_->identifier_, std::vector{storage.GetLabelIx(dba.LabelToName(labels[layer]))})); @@ -1546,7 +1546,7 @@ TEST_F(QueryPlanExpandVariable, FineGrainedExpandToSameSymbol) { auto n_from = ScanAllTuple{node, logical_op, symbol}; auto filter_op = std::make_shared( - n_from.op_, + n_from.op_, std::vector>{}, storage.Create( n_from.node_->identifier_, std::vector{storage.GetLabelIx(dba.LabelToName(labels[layer]))})); @@ -1813,7 +1813,8 @@ class QueryPlanExpandWeightedShortestPath : public testing::Test { auto n = MakeScanAll(storage, symbol_table, "n", existing_node_input ? existing_node_input->op_ : nullptr); auto last_op = n.op_; if (node_id) { - last_op = std::make_shared(last_op, EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id))); + last_op = std::make_shared(last_op, std::vector>{}, + EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id))); } auto ident_e = IDENT("e"); @@ -1967,8 +1968,9 @@ TEST_F(QueryPlanExpandWeightedShortestPath, ExistingNode) { // scan the nodes optionally filtering on property value auto n0 = MakeScanAll(storage, symbol_table, "n0"); if (preceeding_node_id) { - auto filter = std::make_shared( - n0.op_, EQ(PROPERTY_LOOKUP(n0.node_->identifier_, prop), LITERAL(*preceeding_node_id))); + auto filter = + std::make_shared(n0.op_, std::vector>{}, + EQ(PROPERTY_LOOKUP(n0.node_->identifier_, prop), LITERAL(*preceeding_node_id))); // inject the filter op into the ScanAllTuple. that way the filter // op can be passed into the ExpandWShortest function without too // much refactor @@ -2243,7 +2245,8 @@ class QueryPlanExpandAllShortestPaths : public testing::Test { auto n = MakeScanAll(storage, symbol_table, "n", existing_node_input ? existing_node_input->op_ : nullptr); auto last_op = n.op_; if (node_id) { - last_op = std::make_shared(last_op, EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id))); + last_op = std::make_shared(last_op, std::vector>{}, + EQ(PROPERTY_LOOKUP(n.node_->identifier_, prop), LITERAL(*node_id))); } auto ident_e = IDENT("e"); @@ -2739,7 +2742,7 @@ TEST(QueryPlan, OptionalMatchThenExpandToMissingNode) { n.node_->labels_.emplace_back(storage.GetLabelIx(label_missing)); auto *filter_expr = storage.Create(n.node_->identifier_, n.node_->labels_); - auto node_filter = std::make_shared(n.op_, filter_expr); + auto node_filter = std::make_shared(n.op_, std::vector>{}, filter_expr); auto optional = std::make_shared(nullptr, node_filter, std::vector{n.sym_}); // WITH n auto n_ne = NEXPR("n", IDENT("n")->MapTo(n.sym_)); @@ -2868,7 +2871,7 @@ TEST(QueryPlan, EdgeFilter) { r_m.edge_->edge_types_.push_back(storage.GetEdgeTypeIx(dba.EdgeTypeToName(edge_type))); std::get<0>(r_m.edge_->properties_)[storage.GetPropertyIx(prop.first)] = LITERAL(42); auto *filter_expr = EQ(PROPERTY_LOOKUP(r_m.edge_->identifier_, prop), LITERAL(42)); - auto edge_filter = std::make_shared(r_m.op_, filter_expr); + auto edge_filter = std::make_shared(r_m.op_, std::vector>{}, filter_expr); // make a named expression and a produce auto output = @@ -2936,7 +2939,7 @@ TEST(QueryPlan, Filter) { auto n = MakeScanAll(storage, symbol_table, "n"); auto e = PROPERTY_LOOKUP(IDENT("n")->MapTo(n.sym_), property); - auto f = std::make_shared(n.op_, e); + auto f = std::make_shared(n.op_, std::vector>{}, e); auto output = NEXPR("x", IDENT("n")->MapTo(n.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true)); auto produce = MakeProduce(f, output); @@ -3441,7 +3444,8 @@ TEST(QueryPlan, ScanAllEqualsScanAllByLabelProperty) { memgraph::query::DbAccessor dba(&storage_dba); auto scan_all = MakeScanAll(storage, symbol_table, "n"); auto e = PROPERTY_LOOKUP(IDENT("n")->MapTo(scan_all.sym_), std::make_pair("prop", prop)); - auto filter = std::make_shared(scan_all.op_, EQ(e, LITERAL(prop_value))); + auto filter = std::make_shared(scan_all.op_, std::vector>{}, + EQ(e, LITERAL(prop_value))); auto output = NEXPR("n", IDENT("n")->MapTo(scan_all.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true)); auto produce = MakeProduce(filter, output); @@ -3455,3 +3459,220 @@ TEST(QueryPlan, ScanAllEqualsScanAllByLabelProperty) { count_with_index(prop_value2, vertex_count - vertex_prop_count); count_with_scan_all(prop_value2, vertex_count - vertex_prop_count); } + +class ExistsFixture : public testing::Test { + protected: + memgraph::storage::Storage db; + memgraph::storage::Storage::Accessor storage_dba{db.Access()}; + memgraph::query::DbAccessor dba{&storage_dba}; + AstStorage storage; + SymbolTable symbol_table; + + std::pair prop = PROPERTY_PAIR("property"); + + memgraph::query::VertexAccessor v1{dba.InsertVertex()}; + memgraph::query::VertexAccessor v2{dba.InsertVertex()}; + memgraph::storage::EdgeTypeId edge_type{db.NameToEdgeType("Edge")}; + memgraph::query::EdgeAccessor r1{*dba.InsertEdge(&v1, &v2, edge_type)}; + + memgraph::query::VertexAccessor v3{dba.InsertVertex()}; + memgraph::query::VertexAccessor v4{dba.InsertVertex()}; + memgraph::storage::EdgeTypeId edge_type_unknown{db.NameToEdgeType("Other")}; + memgraph::query::EdgeAccessor r2{*dba.InsertEdge(&v3, &v4, edge_type_unknown)}; + + void SetUp() override { + // (:l1)-[:Edge]->(:l2), (:l3)-[:Other]->(:l4) + ASSERT_TRUE(v1.AddLabel(dba.NameToLabel("l1")).HasValue()); + ASSERT_TRUE(v2.AddLabel(dba.NameToLabel("l2")).HasValue()); + ASSERT_TRUE(v3.AddLabel(dba.NameToLabel("l3")).HasValue()); + ASSERT_TRUE(v4.AddLabel(dba.NameToLabel("l4")).HasValue()); + + ASSERT_TRUE(v1.SetProperty(prop.second, memgraph::storage::PropertyValue(1)).HasValue()); + ASSERT_TRUE(v2.SetProperty(prop.second, memgraph::storage::PropertyValue(2)).HasValue()); + ASSERT_TRUE(v3.SetProperty(prop.second, memgraph::storage::PropertyValue(3)).HasValue()); + ASSERT_TRUE(v4.SetProperty(prop.second, memgraph::storage::PropertyValue(4)).HasValue()); + + ASSERT_TRUE(r1.SetProperty(prop.second, memgraph::storage::PropertyValue(1)).HasValue()); + memgraph::license::global_license_checker.EnableTesting(); + + dba.AdvanceCommand(); + } + + int TestExists(std::string match_label, EdgeAtom::Direction direction, + std::vector edge_types, + std::optional destination_label = std::nullopt, + std::optional destination_prop = std::nullopt, + std::optional edge_prop = std::nullopt) { + std::vector edge_type_names; + for (const auto &type : edge_types) { + edge_type_names.emplace_back(db.EdgeTypeToName(type)); + } + + auto *source_node = NODE("n"); + auto source_sym = symbol_table.CreateSymbol("n", true); + source_node->identifier_->MapTo(source_sym); + + auto *expansion_edge = EDGE("edge", direction, edge_type_names, false); + auto edge_sym = symbol_table.CreateSymbol("edge", false); + expansion_edge->identifier_->MapTo(edge_sym); + + auto *destination_node = NODE("n2", destination_label, false); + auto dest_sym = symbol_table.CreateSymbol("n2", false); + destination_node->identifier_->MapTo(dest_sym); + + auto *exists_expression = EXISTS(PATTERN(source_node, expansion_edge, destination_node)); + exists_expression->MapTo(symbol_table.CreateAnonymousSymbol()); + + auto scan_all = MakeScanAll(storage, symbol_table, "n"); + scan_all.node_->labels_.emplace_back(storage.GetLabelIx(match_label)); + + std::shared_ptr last_op = std::make_shared( + nullptr, scan_all.sym_, dest_sym, edge_sym, direction, edge_types, false, memgraph::storage::View::OLD); + + if (destination_label.has_value() || destination_prop.has_value() || edge_prop.has_value()) { + Expression *filter_expr = nullptr; + + if (destination_label.has_value()) { + auto labelIx = storage.GetLabelIx(destination_label.value()); + destination_node->labels_.emplace_back(labelIx); + + auto label_expr = static_cast( + storage.Create(destination_node->identifier_, std::vector{labelIx})); + + filter_expr = filter_expr ? AND(filter_expr, label_expr) : label_expr; + } + + if (destination_prop.has_value()) { + auto prop_expr = static_cast( + EQ(PROPERTY_LOOKUP(destination_node->identifier_, prop), LITERAL(destination_prop.value()))); + filter_expr = filter_expr ? AND(filter_expr, prop_expr) : prop_expr; + } + + if (edge_prop.has_value()) { + auto prop_expr = static_cast( + EQ(PROPERTY_LOOKUP(expansion_edge->identifier_, prop), LITERAL(edge_prop.value()))); + filter_expr = filter_expr ? AND(filter_expr, prop_expr) : prop_expr; + } + + last_op = + std::make_shared(std::move(last_op), std::vector>{}, filter_expr); + } + + last_op = std::make_shared(std::move(last_op), storage.Create(1)); + last_op = std::make_shared(std::move(last_op), symbol_table.at(*exists_expression)); + + auto *total_expression = + AND(storage.Create(scan_all.node_->identifier_, scan_all.node_->labels_), exists_expression); + + auto filter = std::make_shared(scan_all.op_, std::vector>{last_op}, + total_expression); + auto output = + NEXPR("n", IDENT("n")->MapTo(scan_all.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true)); + + auto produce = MakeProduce(filter, output); + auto context = MakeContext(storage, symbol_table, &dba); + return PullAll(*produce, &context); + } + + int TestDoubleExists(std::string match_label, EdgeAtom::Direction direction, + std::vector first_edge_type, + std::vector second_edge_type, bool or_flag = false) { + std::vector first_edge_type_names; + for (const auto &type : first_edge_type) { + first_edge_type_names.emplace_back(db.EdgeTypeToName(type)); + } + + std::vector second_edge_type_names; + for (const auto &type : second_edge_type) { + second_edge_type_names.emplace_back(db.EdgeTypeToName(type)); + } + + auto *source_node = NODE("n"); + auto source_sym = symbol_table.CreateSymbol("n", true); + source_node->identifier_->MapTo(source_sym); + + auto *expansion_edge = EDGE("edge", direction, first_edge_type_names, false); + auto edge_sym = symbol_table.CreateSymbol("edge", false); + expansion_edge->identifier_->MapTo(edge_sym); + + auto *destination_node = NODE("n2", std::nullopt, false); + auto dest_sym = symbol_table.CreateSymbol("n2", false); + destination_node->identifier_->MapTo(dest_sym); + + auto *exists_expression = EXISTS(PATTERN(source_node, expansion_edge, destination_node)); + exists_expression->MapTo(symbol_table.CreateAnonymousSymbol()); + + auto *expansion_edge2 = EDGE("edge2", direction, second_edge_type_names, false); + auto edge_sym2 = symbol_table.CreateSymbol("edge2", false); + expansion_edge2->identifier_->MapTo(edge_sym2); + + auto *destination_node2 = NODE("n22", std::nullopt, false); + auto dest_sym2 = symbol_table.CreateSymbol("n22", false); + destination_node2->identifier_->MapTo(dest_sym2); + + auto *exists_expression2 = EXISTS(PATTERN(source_node, expansion_edge2, destination_node2)); + exists_expression2->MapTo(symbol_table.CreateAnonymousSymbol()); + + auto scan_all = MakeScanAll(storage, symbol_table, "n"); + scan_all.node_->labels_.emplace_back(storage.GetLabelIx(match_label)); + + std::shared_ptr last_op = std::make_shared( + nullptr, scan_all.sym_, dest_sym, edge_sym, direction, first_edge_type, false, memgraph::storage::View::OLD); + last_op = std::make_shared(std::move(last_op), storage.Create(1)); + last_op = std::make_shared(std::move(last_op), symbol_table.at(*exists_expression)); + + std::shared_ptr last_op2 = std::make_shared( + nullptr, scan_all.sym_, dest_sym2, edge_sym2, direction, second_edge_type, false, memgraph::storage::View::OLD); + last_op2 = std::make_shared(std::move(last_op2), storage.Create(1)); + last_op2 = std::make_shared(std::move(last_op2), symbol_table.at(*exists_expression2)); + + Expression *total_expression = storage.Create(scan_all.node_->identifier_, scan_all.node_->labels_); + + if (or_flag) { + total_expression = AND(total_expression, OR(exists_expression, exists_expression2)); + } else { + total_expression = AND(total_expression, AND(exists_expression, exists_expression2)); + } + + auto filter = std::make_shared( + scan_all.op_, std::vector>{last_op, last_op2}, total_expression); + auto output = + NEXPR("n", IDENT("n")->MapTo(scan_all.sym_))->MapTo(symbol_table.CreateSymbol("named_expression_1", true)); + + auto produce = MakeProduce(filter, output); + auto context = MakeContext(storage, symbol_table, &dba); + return PullAll(*produce, &context); + } +}; + +TEST_F(ExistsFixture, BasicExists) { + std::vector known_edge_types; + known_edge_types.push_back(edge_type); + std::vector unknown_edge_types; + unknown_edge_types.push_back(edge_type_unknown); + + EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::OUT, {})); + EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {})); + EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::IN, {})); + EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::OUT, known_edge_types)); + EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::OUT, unknown_edge_types)); +} + +TEST_F(ExistsFixture, ExistsWithFiltering) { + EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2")); + EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l3")); + + EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 2)); + EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 1)); + + EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", std::nullopt, 1)); + EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", std::nullopt, 2)); + + EXPECT_EQ(1, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 2, 1)); + EXPECT_EQ(0, TestExists("l1", EdgeAtom::Direction::BOTH, {}, "l2", 1, 1)); +} + +TEST_F(ExistsFixture, DoubleFilters) { + EXPECT_EQ(1, TestDoubleExists("l1", EdgeAtom::Direction::BOTH, {}, {}, true)); + EXPECT_EQ(1, TestDoubleExists("l1", EdgeAtom::Direction::BOTH, {}, {}, false)); +} diff --git a/tests/unit/query_plan_read_write_typecheck.cpp b/tests/unit/query_plan_read_write_typecheck.cpp index 7f84e9b16..1b8ebbadd 100644 --- a/tests/unit/query_plan_read_write_typecheck.cpp +++ b/tests/unit/query_plan_read_write_typecheck.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 @@ -57,7 +57,8 @@ TEST_F(ReadWriteTypeCheckTest, CreateNode) { TEST_F(ReadWriteTypeCheckTest, Filter) { std::shared_ptr scan_all = std::make_shared(nullptr, GetSymbol("node1")); std::shared_ptr filter = - std::make_shared(scan_all, EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(0))); + std::make_shared(scan_all, std::vector>{}, + EQ(PROPERTY_LOOKUP("node1", dba.NameToProperty("prop")), LITERAL(0))); CheckPlanType(filter.get(), RWType::R); } @@ -87,7 +88,8 @@ TEST_F(ReadWriteTypeCheckTest, OrderByAndLimit) { std::shared_ptr last_op = std::make_shared(); last_op = std::make_shared(last_op, node_sym, label); - last_op = std::make_shared(last_op, EQ(PROPERTY_LOOKUP("node", prop), LITERAL(5))); + last_op = std::make_shared(last_op, std::vector>{}, + EQ(PROPERTY_LOOKUP("node", prop), LITERAL(5))); last_op = std::make_shared(last_op, std::vector{NEXPR("n", IDENT("n"))}); last_op = std::make_shared(last_op, std::vector{{Ordering::DESC, PROPERTY_LOOKUP("node", prop)}}, std::vector{node_sym}); diff --git a/tests/unit/query_pretty_print.cpp b/tests/unit/query_pretty_print.cpp index 9bfcab6ec..27103631a 100644 --- a/tests/unit/query_pretty_print.cpp +++ b/tests/unit/query_pretty_print.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 @@ -163,5 +163,4 @@ TEST_F(ExpressionPrettyPrinterTest, NamedExpression) { // n AS 1 EXPECT_EQ(ToString(NEXPR("n", LITERAL(1))), "(NamedExpression \"n\" 1)"); } - } // namespace diff --git a/tests/unit/query_semantic.cpp b/tests/unit/query_semantic.cpp index e0bac0166..85fcb9320 100644 --- a/tests/unit/query_semantic.cpp +++ b/tests/unit/query_semantic.cpp @@ -19,6 +19,7 @@ #include "query/frontend/ast/ast.hpp" #include "query/frontend/semantic/symbol_generator.hpp" #include "query/frontend/semantic/symbol_table.hpp" +#include "query/plan/preprocess.hpp" #include "query_common.hpp" @@ -1179,3 +1180,37 @@ TEST_F(TestSymbolGenerator, Foreach) { query = QUERY(SINGLE_QUERY(FOREACH(i, {CREATE(PATTERN(NODE("n")))}), RETURN("i"))); EXPECT_THROW(memgraph::query::MakeSymbolTable(query), UnboundVariableError); } + +TEST_F(TestSymbolGenerator, Exists) { + auto query = QUERY(SINGLE_QUERY( + MATCH(PATTERN(NODE("n"))), + WHERE(EXISTS(PATTERN(NODE("n"), EDGE("", EdgeAtom::Direction::BOTH, {}, false), NODE("m")))), RETURN("n"))); + EXPECT_THROW(MakeSymbolTable(query), SemanticException); + + query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), + WHERE(EXISTS(PATTERN(NODE("n"), EDGE("r"), NODE("", std::nullopt, false)))), RETURN("n"))); + EXPECT_THROW(MakeSymbolTable(query), SemanticException); + + query = QUERY( + SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), WHERE(EXISTS(PATTERN(NODE("n"), EDGE("r"), NODE("m")))), RETURN("n"))); + EXPECT_THROW(MakeSymbolTable(query), SemanticException); + + // Symbols for match pattern, node symbol, exists pattern, exists edge, exists second node, named expression in return + query = QUERY(SINGLE_QUERY(MATCH(PATTERN(NODE("n"))), + WHERE(EXISTS(PATTERN(NODE("n"), EDGE("edge", EdgeAtom::Direction::BOTH, {}, false), + NODE("node", std::nullopt, false)))), + RETURN("n"))); + auto symbol_table = MakeSymbolTable(query); + ASSERT_EQ(symbol_table.max_position(), 7); + + memgraph::query::plan::UsedSymbolsCollector collector(symbol_table); + auto *match = dynamic_cast(query->single_query_->clauses_[0]); + auto *expression = dynamic_cast(match->where_->expression_); + + expression->Accept(collector); + + ASSERT_EQ(collector.symbols_.size(), 1); + + auto symbol = *collector.symbols_.begin(); + ASSERT_EQ(symbol.name_, "n"); +}