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.
-
-
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