Compare commits
73 Commits
fix-commit
...
use-expr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2faa58fe21 | ||
|
|
40b2e12eaf | ||
|
|
0e613a1ff9 | ||
|
|
4b5cbb99b6 | ||
|
|
925835b080 | ||
|
|
ce788f5f65 | ||
|
|
b8186bea2e | ||
|
|
3c4856dcb7 | ||
|
|
b4d6dc0930 | ||
|
|
817161a915 | ||
|
|
58eb2caf0f | ||
|
|
ecda71168c | ||
|
|
8e1f83acc9 | ||
|
|
f910cb770c | ||
|
|
38d0b89b04 | ||
|
|
a2a6a3855b | ||
|
|
e9f0360fb3 | ||
|
|
fe4955447e | ||
|
|
1631c20df2 | ||
|
|
947baedbe6 | ||
|
|
c6447eb48b | ||
|
|
7e84744d07 | ||
|
|
c0d03888f4 | ||
|
|
efb3c8d03d | ||
|
|
95dbc022c0 | ||
|
|
a40403e3ce | ||
|
|
14c9e68456 | ||
|
|
68b26275a3 | ||
|
|
5012824e05 | ||
|
|
a12a1ea358 | ||
|
|
2891041468 | ||
|
|
cc5ee6a496 | ||
|
|
f57f30c8cf | ||
|
|
462daf3a2b | ||
|
|
2009fefc8a | ||
|
|
eb3f96d1f6 | ||
|
|
80e0e439b7 | ||
|
|
351258ace8 | ||
|
|
74d3663821 | ||
|
|
264b233053 | ||
|
|
2ceaf59767 | ||
|
|
eb0b3141d5 | ||
|
|
c0bee760bf | ||
|
|
ff2f8031a9 | ||
|
|
094d4f282d | ||
|
|
3dd2657320 | ||
|
|
6fe474282a | ||
|
|
7fc0fb6520 | ||
|
|
3f4f66b57f | ||
|
|
063e297e1e | ||
|
|
2998f92595 | ||
|
|
86b1688192 | ||
|
|
f629de7e60 | ||
|
|
b737e53456 | ||
|
|
1bdc32ba5d | ||
|
|
10ca68bb2a | ||
|
|
bfbd8538d4 | ||
|
|
3e0e17d469 | ||
|
|
b57f91fcfc | ||
|
|
066a96c0ae | ||
|
|
1ae6b71c5f | ||
|
|
21870a0e7e | ||
|
|
cbe15e7f44 | ||
|
|
65a7ba01da | ||
|
|
7a2bbd4bb3 | ||
|
|
589e0e098b | ||
|
|
41d4185156 | ||
|
|
599c0a641f | ||
|
|
1fb49c4865 | ||
|
|
df1485aeec | ||
|
|
b2e1056389 | ||
|
|
e4c9411e63 | ||
|
|
a0bc1371dd |
@@ -6,6 +6,7 @@ Checks: '*,
|
||||
-altera-unroll-loops,
|
||||
-android-*,
|
||||
-cert-err58-cpp,
|
||||
-cert-str34-c,
|
||||
-cppcoreguidelines-avoid-c-arrays,
|
||||
-cppcoreguidelines-avoid-goto,
|
||||
-cppcoreguidelines-avoid-magic-numbers,
|
||||
@@ -49,6 +50,7 @@ Checks: '*,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-modernize-avoid-c-arrays,
|
||||
-modernize-concat-nested-namespaces,
|
||||
-modernize-loop-convert,
|
||||
-modernize-pass-by-value,
|
||||
-modernize-use-equals-default,
|
||||
-modernize-use-nodiscard,
|
||||
@@ -61,7 +63,9 @@ Checks: '*,
|
||||
-readability-magic-numbers,
|
||||
-readability-named-parameter,
|
||||
-misc-no-recursion,
|
||||
-concurrency-mt-unsafe'
|
||||
-concurrency-mt-unsafe,
|
||||
-bugprone-easily-swappable-parameters'
|
||||
|
||||
WarningsAsErrors: ''
|
||||
HeaderFilterRegex: 'src/.*'
|
||||
AnalyzeTemporaryDtors: false
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -3,7 +3,9 @@
|
||||
- [ ] Update [changelog](https://docs.memgraph.com/memgraph/changelog)
|
||||
- [ ] Write E2E tests
|
||||
- [ ] Compare the [benchmarking results](https://bench-graph.memgraph.com/) between the master branch and the Epic branch
|
||||
- [ ] Provide the full content or a guide for the final git message
|
||||
|
||||
[master < Task] PR
|
||||
- [ ] Check, and update documentation if necessary
|
||||
- [ ] Update [changelog](https://docs.memgraph.com/memgraph/changelog)
|
||||
- [ ] Provide the full content or a guide for the final git message
|
||||
|
||||
31
.github/workflows/diff.yaml
vendored
31
.github/workflows/diff.yaml
vendored
@@ -1,4 +1,7 @@
|
||||
name: Diff
|
||||
concurrency:
|
||||
group: ${{ github.head_ref || github.sha }}
|
||||
cancel-in-progress: true
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -67,6 +70,11 @@ jobs:
|
||||
# branches and tags. (default: 1)
|
||||
fetch-depth: 0
|
||||
|
||||
# This is also needed if we want do to comparison against other branches
|
||||
# See https://github.community/t/checkout-code-fails-when-it-runs-lerna-run-test-since-master/17920
|
||||
- name: Fetch all history for all tags and branches
|
||||
run: git fetch
|
||||
|
||||
- name: Build combined ASAN, UBSAN and coverage binaries
|
||||
run: |
|
||||
# Activate toolchain.
|
||||
@@ -107,12 +115,22 @@ jobs:
|
||||
name: "Code coverage"
|
||||
path: tools/github/generated/code_coverage.tar.gz
|
||||
|
||||
- name: Set base branch
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
run: |
|
||||
echo "BASE_BRANCH=origin/${{ github.base_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set base branch # if we manually dispatch or push to master
|
||||
if: ${{ github.event_name != 'pull_request' }}
|
||||
run: |
|
||||
echo "BASE_BRANCH=origin/master" >> $GITHUB_ENV
|
||||
|
||||
- name: Run clang-tidy
|
||||
run: |
|
||||
source /opt/toolchain-v4/activate
|
||||
|
||||
# Restrict clang-tidy results only to the modified parts
|
||||
git diff -U0 master... -- src ':!*.hpp' | ./tools/github/clang-tidy/clang-tidy-diff.py -p 1 -j $THREADS -path build | tee ./build/clang_tidy_output.txt
|
||||
git diff -U0 ${{ env.BASE_BRANCH }}... -- src | ./tools/github/clang-tidy/clang-tidy-diff.py -p 1 -j $THREADS -extra-arg="-DMG_CLANG_TIDY_CHECK" -path build | tee ./build/clang_tidy_output.txt
|
||||
|
||||
# Fail if any warning is reported
|
||||
! cat ./build/clang_tidy_output.txt | ./tools/github/clang-tidy/grep_error_lines.sh > /dev/null
|
||||
@@ -153,7 +171,7 @@ jobs:
|
||||
|
||||
# Run leftover CTest tests (all except unit and benchmark tests).
|
||||
cd build
|
||||
ctest -E "(memgraph__unit|memgraph__benchmark)" --output-on-failure
|
||||
ctest -E "(memgraph__unit|memgraph__benchmark|memgraph__simulation)" --output-on-failure
|
||||
|
||||
- name: Run drivers tests
|
||||
run: |
|
||||
@@ -244,6 +262,15 @@ jobs:
|
||||
cd build
|
||||
ctest -R memgraph__unit --output-on-failure -j$THREADS
|
||||
|
||||
- name: Run simulation tests
|
||||
run: |
|
||||
# Activate toolchain.
|
||||
source /opt/toolchain-v4/activate
|
||||
|
||||
# Run unit tests.
|
||||
cd build
|
||||
ctest -R memgraph__simulation --output-on-failure -j$THREADS
|
||||
|
||||
- name: Run e2e tests
|
||||
run: |
|
||||
# TODO(gitbuda): Setup mgclient and pymgclient properly.
|
||||
|
||||
2
.github/workflows/full_clang_tidy.yaml
vendored
2
.github/workflows/full_clang_tidy.yaml
vendored
@@ -39,7 +39,7 @@ jobs:
|
||||
source /opt/toolchain-v4/activate
|
||||
|
||||
# The results are also written to standard output in order to retain them in the logs
|
||||
./tools/github/clang-tidy/run-clang-tidy.py -p build -j $THREADS -clang-tidy-binary=/opt/toolchain-v4/bin/clang-tidy "$PWD/src/*" |
|
||||
./tools/github/clang-tidy/run-clang-tidy.py -p build -j $THREADS -extra-arg="-DMG_CLANG_TIDY_CHECK" -clang-tidy-binary=/opt/toolchain-v4/bin/clang-tidy "$PWD/src/*" |
|
||||
tee ./build/full_clang_tidy_output.txt
|
||||
|
||||
- name: Summarize clang-tidy results
|
||||
|
||||
76
.github/workflows/package_all.yaml
vendored
76
.github/workflows/package_all.yaml
vendored
@@ -6,11 +6,11 @@ on: workflow_dispatch
|
||||
|
||||
jobs:
|
||||
centos-7:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -22,29 +22,29 @@ jobs:
|
||||
name: centos-7
|
||||
path: build/output/centos-7/memgraph*.rpm
|
||||
|
||||
centos-8:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
centos-9:
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
run: |
|
||||
./release/package/run.sh package centos-8
|
||||
./release/package/run.sh package centos-9
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: centos-8
|
||||
path: build/output/centos-8/memgraph*.rpm
|
||||
name: centos-9
|
||||
path: build/output/centos-9/memgraph*.rpm
|
||||
|
||||
debian-10:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -57,11 +57,11 @@ jobs:
|
||||
path: build/output/debian-10/memgraph*.deb
|
||||
|
||||
debian-11:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -74,11 +74,11 @@ jobs:
|
||||
path: build/output/debian-11/memgraph*.deb
|
||||
|
||||
docker:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -93,11 +93,11 @@ jobs:
|
||||
path: build/output/docker/memgraph*.tar.gz
|
||||
|
||||
ubuntu-1804:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -110,11 +110,11 @@ jobs:
|
||||
path: build/output/ubuntu-18.04/memgraph*.deb
|
||||
|
||||
ubuntu-2004:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
@@ -126,12 +126,29 @@ jobs:
|
||||
name: ubuntu-2004
|
||||
path: build/output/ubuntu-20.04/memgraph*.deb
|
||||
|
||||
debian-11-platform:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
ubuntu-2204:
|
||||
runs-on: [self-hosted, DockerMgBuild, X64]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: "Set up repository"
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
run: |
|
||||
./release/package/run.sh package ubuntu-22.04
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ubuntu-2204
|
||||
path: build/output/ubuntu-22.04/memgraph*.deb
|
||||
|
||||
debian-11-platform:
|
||||
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"
|
||||
@@ -142,3 +159,20 @@ jobs:
|
||||
with:
|
||||
name: debian-11-platform
|
||||
path: build/output/debian-11/memgraph*.deb
|
||||
|
||||
debian-11-arm:
|
||||
runs-on: [self-hosted, DockerMgBuild, ARM64, strange]
|
||||
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 debian-11-arm
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: debian-11-arm
|
||||
path: build/output/debian-11-arm/memgraph*.deb
|
||||
|
||||
22
.github/workflows/release_docker.yaml
vendored
22
.github/workflows/release_docker.yaml
vendored
@@ -4,8 +4,12 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Memgraph binary version to publish on Dockerhub."
|
||||
description: "Memgraph binary version to publish on DockerHub."
|
||||
required: true
|
||||
force_release:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
docker_publish:
|
||||
@@ -36,6 +40,22 @@ jobs:
|
||||
curl -L https://download.memgraph.com/memgraph/v${{ github.event.inputs.version }}/debian-11/memgraph_${{ github.event.inputs.version }}-1_amd64.deb > memgraph-amd64.deb
|
||||
curl -L https://download.memgraph.com/memgraph/v${{ github.event.inputs.version }}/debian-11-aarch64/memgraph_${{ github.event.inputs.version }}-1_arm64.deb > memgraph-arm64.deb
|
||||
|
||||
- name: Check if specified version is already pushed
|
||||
run: |
|
||||
EXISTS=$(docker manifest inspect $DOCKER_ORGANIZATION_NAME/$DOCKER_REPOSITORY_NAME:${{ github.event.inputs.version }} > /dev/null; echo $?)
|
||||
echo $EXISTS
|
||||
if [[ ${EXISTS} -eq 0 ]]; then
|
||||
echo 'The specified version has been already released to DockerHub.'
|
||||
if [[ ${{ github.event.inputs.force_release }} = true ]]; then
|
||||
echo 'Forcing the release!'
|
||||
else
|
||||
echo 'Stopping the release!'
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo 'All good the specified version has not been release to DockerHub.'
|
||||
fi
|
||||
|
||||
- name: Build & push docker images
|
||||
run: |
|
||||
cd release/docker
|
||||
|
||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -23,6 +23,8 @@ cmake-build-*
|
||||
cmake/DownloadProject/
|
||||
dist/
|
||||
src/query/frontend/opencypher/generated/
|
||||
src/query/v2/frontend/opencypher/generated/
|
||||
src/parser/opencypher/generated
|
||||
tags
|
||||
ve/
|
||||
ve3/
|
||||
@@ -50,15 +52,25 @@ src/distributed/pull_produce_rpc_messages.hpp
|
||||
src/distributed/storage_gc_rpc_messages.hpp
|
||||
src/distributed/token_sharing_rpc_messages.hpp
|
||||
src/distributed/updates_rpc_messages.hpp
|
||||
src/query/v2/frontend/ast/ast.hpp
|
||||
src/query/frontend/ast/ast.hpp
|
||||
src/storage/v3/bindings/ast/ast.hpp
|
||||
src/query/distributed/frontend/ast/ast_serialization.hpp
|
||||
src/query/v2/distributed/frontend/ast/ast_serialization.hpp
|
||||
src/durability/distributed/state_delta.hpp
|
||||
src/durability/single_node/state_delta.hpp
|
||||
src/durability/single_node_ha/state_delta.hpp
|
||||
src/query/frontend/semantic/symbol.hpp
|
||||
src/query/v2/frontend/semantic/symbol.hpp
|
||||
src/expr/semantic/symbol.hpp
|
||||
src/query/distributed/frontend/semantic/symbol_serialization.hpp
|
||||
src/query/v2/distributed/frontend/semantic/symbol_serialization.hpp
|
||||
src/query/distributed/plan/ops.hpp
|
||||
src/query/v2/distributed/plan/ops.hpp
|
||||
src/query/plan/operator.hpp
|
||||
src/query/v2/plan/operator.hpp
|
||||
src/parser/opencypher/generated
|
||||
src/expr/semantic/symbol.hpp
|
||||
src/raft/log_entry.hpp
|
||||
src/raft/raft_rpc_messages.hpp
|
||||
src/raft/snapshot_metadata.hpp
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
* @antaljanosbenjamin @kostasrim
|
||||
@@ -5,6 +5,9 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "centos-7"
|
||||
check_architecture "x86_64"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc gcc-c++ make # generic build tools
|
||||
wget # used for archive download
|
||||
|
||||
@@ -1,161 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils-common gcc gcc-c++ make # generic build tools
|
||||
wget # used for archive download
|
||||
gnupg2 # used for archive signature verification
|
||||
tar gzip bzip2 xz unzip # used for archive unpacking
|
||||
zlib-devel # zlib library used for all builds
|
||||
expat-devel libipt libipt-devel libbabeltrace-devel xz-devel python36-devel texinfo # for gdb
|
||||
libcurl-devel # for cmake
|
||||
curl # snappy
|
||||
readline-devel # for cmake and llvm
|
||||
libffi-devel libxml2-devel # for llvm
|
||||
libedit-devel pcre-devel automake bison # for swig
|
||||
file
|
||||
openssl-devel
|
||||
gmp-devel
|
||||
gperf
|
||||
patch
|
||||
)
|
||||
|
||||
TOOLCHAIN_RUN_DEPS=(
|
||||
make # generic build tools
|
||||
tar gzip bzip2 xz # used for archive unpacking
|
||||
zlib # zlib library used for all builds
|
||||
expat libipt libbabeltrace xz-libs python36 # for gdb
|
||||
readline # for cmake and llvm
|
||||
libffi libxml2 # for llvm
|
||||
openssl-devel
|
||||
)
|
||||
|
||||
MEMGRAPH_BUILD_DEPS=(
|
||||
git # source code control
|
||||
make pkgconf-pkg-config # build system
|
||||
curl wget # for downloading libs
|
||||
libuuid-devel java-11-openjdk # required by antlr
|
||||
readline-devel # for memgraph console
|
||||
python36-devel # for query modules
|
||||
openssl-devel
|
||||
libseccomp-devel
|
||||
python36 python3-virtualenv python3-pip nmap-ncat # for qa, macro_benchmark and stress tests
|
||||
#
|
||||
# IMPORTANT: python3-yaml does NOT exist on CentOS
|
||||
# Install it manually using `pip3 install PyYAML`
|
||||
#
|
||||
PyYAML # Package name here does not correspond to the yum package!
|
||||
libcurl-devel # mg-requests
|
||||
rpm-build rpmlint # for RPM package building
|
||||
doxygen graphviz # source documentation generators
|
||||
which mono-complete dotnet-sdk-3.1 nodejs golang zip unzip java-11-openjdk-devel # for driver tests
|
||||
sbcl # for custom Lisp C++ preprocessing
|
||||
autoconf # for jemalloc code generation
|
||||
libtool # for protobuf code generation
|
||||
)
|
||||
|
||||
list() {
|
||||
echo "$1"
|
||||
}
|
||||
|
||||
check() {
|
||||
local missing=""
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == "PyYAML" ]; then
|
||||
if ! python3 -c "import yaml" >/dev/null 2>/dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if ! yum list installed "$pkg" >/dev/null 2>/dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
done
|
||||
if [ "$missing" != "" ]; then
|
||||
echo "MISSING PACKAGES: $missing"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
cd "$DIR"
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "Please run as root."
|
||||
exit 1
|
||||
fi
|
||||
# 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
|
||||
dnf install -y epel-release
|
||||
dnf install -y 'dnf-command(config-manager)'
|
||||
dnf config-manager --set-enabled powertools # Required to install texinfo.
|
||||
dnf update -y
|
||||
dnf install -y wget git python36 python3-pip
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == libipt ]; then
|
||||
if ! dnf list installed libipt >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == libipt-devel ]; then
|
||||
if ! yum list installed libipt-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-devel-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
# Install GDB dependencies not present in the standard repos.
|
||||
# https://bugs.centos.org/view.php?id=17068
|
||||
# https://centos.pkgs.org
|
||||
# Since 2020, there is Babeltrace2 (https://babeltrace.org). Not used
|
||||
# within GDB yet (an assumption).
|
||||
# http://mirror.centos.org/centos/8/PowerTools/x86_64/os/Packages/libbabeltrace-devel-1.5.4-3.el8.x86_64.rpm not working
|
||||
if [ "$pkg" == libbabeltrace-devel ]; then
|
||||
if ! dnf list installed libbabeltrace-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://rpmfind.net/linux/centos/8-stream/PowerTools/x86_64/os/Packages/libbabeltrace-devel-1.5.4-3.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == sbcl ]; then
|
||||
if ! dnf list installed cl-asdf >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/cl-asdf-20101028-18.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed common-lisp-controller >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/common-lisp-controller-7.4-20.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed sbcl >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/sbcl-2.0.1-4.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if ! dnf list installed dotnet-sdk-3.1 >/dev/null 2>/dev/null; then
|
||||
wget -nv https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm -O packages-microsoft-prod.rpm
|
||||
rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm
|
||||
dnf update -y
|
||||
dnf install -y dotnet-sdk-3.1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == PyYAML ]; then
|
||||
if [ -z ${SUDO_USER+x} ]; then # Running as root (e.g. Docker).
|
||||
pip3 install --user PyYAML
|
||||
else # Running using sudo.
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install --user PyYAML"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
dnf install -y "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
deps=$2"[*]"
|
||||
"$1" "${!deps}"
|
||||
@@ -5,15 +5,16 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "centos-9"
|
||||
check_architecture "x86_64"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc gcc-c++ make # generic build tools
|
||||
coreutils-common gcc gcc-c++ make # generic build tools
|
||||
wget # used for archive download
|
||||
gnupg2 # used for archive signature verification
|
||||
tar gzip bzip2 xz unzip # used for archive unpacking
|
||||
zlib-devel # zlib library used for all builds
|
||||
expat-devel xz-devel python3-devel texinfo # for gdb
|
||||
libcurl-devel # for cmake
|
||||
curl # snappy
|
||||
expat-devel xz-devel python3-devel texinfo libbabeltrace-devel # for gdb
|
||||
readline-devel # for cmake and llvm
|
||||
libffi-devel libxml2-devel # for llvm
|
||||
libedit-devel pcre-devel automake bison # for swig
|
||||
@@ -21,6 +22,9 @@ TOOLCHAIN_BUILD_DEPS=(
|
||||
openssl-devel
|
||||
gmp-devel
|
||||
gperf
|
||||
diffutils
|
||||
libipt libipt-devel # intel
|
||||
patch
|
||||
)
|
||||
|
||||
TOOLCHAIN_RUN_DEPS=(
|
||||
@@ -31,18 +35,19 @@ TOOLCHAIN_RUN_DEPS=(
|
||||
readline # for cmake and llvm
|
||||
libffi libxml2 # for llvm
|
||||
openssl-devel
|
||||
perl # for openssl
|
||||
)
|
||||
|
||||
MEMGRAPH_BUILD_DEPS=(
|
||||
git # source code control
|
||||
make pkgconf-pkg-config # build system
|
||||
curl wget # for downloading libs
|
||||
wget # for downloading libs
|
||||
libuuid-devel java-11-openjdk # required by antlr
|
||||
readline-devel # for memgraph console
|
||||
python3-devel # for query modules
|
||||
openssl-devel
|
||||
libseccomp-devel
|
||||
python3 python3-virtualenv python3-pip nmap-ncat # for qa, macro_benchmark and stress tests
|
||||
python3 python3-pip python3-virtualenv nmap-ncat # for qa, macro_benchmark and stress tests
|
||||
#
|
||||
# IMPORTANT: python3-yaml does NOT exist on CentOS
|
||||
# Install it manually using `pip3 install PyYAML`
|
||||
@@ -73,12 +78,6 @@ check() {
|
||||
if [ "$pkg" == "python3-virtualenv" ]; then
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == sbcl ]; then
|
||||
if ! sbcl --version &> /dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if ! yum list installed "$pkg" >/dev/null 2>/dev/null; then
|
||||
missing="$pkg $missing"
|
||||
fi
|
||||
@@ -105,13 +104,37 @@ install() {
|
||||
yum update -y
|
||||
yum install -y wget git python3 python3-pip
|
||||
for pkg in $1; do
|
||||
# Since there is no support for libipt-devel for CentOS 9 we install
|
||||
# Fedoras version of same libs, they are the same version but released
|
||||
# for different OS
|
||||
# TODO Update when libipt-devel releases for CentOS 9
|
||||
if [ "$pkg" == libipt ]; then
|
||||
if ! dnf list installed libipt >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == libipt-devel ]; then
|
||||
if ! dnf list installed libipt-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://repo.okay.com.mx/centos/8/x86_64/release/libipt-devel-1.6.1-8.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == libbabeltrace-devel ]; then
|
||||
if ! dnf list installed libbabeltrace-devel >/dev/null 2>/dev/null; then
|
||||
dnf install -y http://mirror.stream.centos.org/9-stream/CRB/x86_64/os/Packages/libbabeltrace-devel-1.5.8-10.el9.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
if [ "$pkg" == sbcl ]; then
|
||||
if ! sbcl --version &> /dev/null; then
|
||||
curl -s https://altushost-swe.dl.sourceforge.net/project/sbcl/sbcl/1.4.2/sbcl-1.4.2-arm64-linux-binary.tar.bz2 -o /tmp/sbcl-arm64.tar.bz2
|
||||
tar xvjf /tmp/sbcl-arm64.tar.bz2 -C /tmp
|
||||
pushd /tmp/sbcl-1.4.2-arm64-linux
|
||||
INSTALL_ROOT=/usr/local sh install.sh
|
||||
popd
|
||||
if ! dnf list installed cl-asdf >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/cl-asdf-20101028-18.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed common-lisp-controller >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/common-lisp-controller-7.4-20.el8.noarch.rpm
|
||||
fi
|
||||
if ! dnf list installed sbcl >/dev/null 2>/dev/null; then
|
||||
dnf install -y https://pkgs.dyn.su/el8/base/x86_64/sbcl-2.0.1-4.el8.x86_64.rpm
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
@@ -125,9 +148,11 @@ install() {
|
||||
fi
|
||||
if [ "$pkg" == python3-virtualenv ]; then
|
||||
if [ -z ${SUDO_USER+x} ]; then # Running as root (e.g. Docker).
|
||||
pip3 install --user virtualenv
|
||||
pip3 install virtualenv
|
||||
pip3 install virtualenvwrapper
|
||||
else # Running using sudo.
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install --user virtualenv"
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install virtualenv"
|
||||
sudo -H -u "$SUDO_USER" bash -c "pip3 install virtualenvwrapper"
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -5,6 +5,9 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "debian-10"
|
||||
check_architecture "x86_64"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc g++ build-essential make # generic build tools
|
||||
wget # used for archive download
|
||||
|
||||
@@ -5,6 +5,9 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "debian-11"
|
||||
check_architecture "arm64"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc g++ build-essential make # generic build tools
|
||||
wget # used for archive download
|
||||
@@ -87,15 +90,6 @@ EOF
|
||||
fi
|
||||
apt install -y wget
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if ! dpkg -s "$pkg" 2>/dev/null >/dev/null; then
|
||||
wget -nv https://packages.microsoft.com/config/debian/10/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-3.1
|
||||
fi
|
||||
continue
|
||||
fi
|
||||
apt install -y "$pkg"
|
||||
done
|
||||
}
|
||||
|
||||
@@ -5,6 +5,9 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "debian-11"
|
||||
check_architecture "x86_64"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc g++ build-essential make # generic build tools
|
||||
wget # used for archive download
|
||||
@@ -87,7 +90,7 @@ EOF
|
||||
fi
|
||||
apt install -y wget
|
||||
for pkg in $1; do
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if [ "$pkg" == dotnet-sdk-3.1 ]; then
|
||||
if ! dpkg -s "$pkg" 2>/dev/null >/dev/null; then
|
||||
wget -nv https://packages.microsoft.com/config/debian/10/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
|
||||
dpkg -i packages-microsoft-prod.deb
|
||||
|
||||
@@ -5,6 +5,8 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "todo-os-name"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
pkg
|
||||
)
|
||||
|
||||
@@ -5,6 +5,9 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "ubuntu-18.04"
|
||||
check_architecture "x86_64"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc g++ build-essential make # generic build tools
|
||||
wget # archive download
|
||||
|
||||
@@ -5,6 +5,9 @@ set -Eeuo pipefail
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
source "$DIR/../util.sh"
|
||||
|
||||
check_operating_system "ubuntu-20.04"
|
||||
check_architecture "x86_64"
|
||||
|
||||
TOOLCHAIN_BUILD_DEPS=(
|
||||
coreutils gcc g++ build-essential make # generic build tools
|
||||
wget # used for archive download
|
||||
|
||||
96
environment/os/ubuntu-22.04.sh
Executable file
96
environment/os/ubuntu-22.04.sh
Executable file
@@ -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 "x86_64"
|
||||
|
||||
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 libipt-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 libipt2 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}"
|
||||
@@ -675,7 +675,7 @@ PROXYGEN_SHA256=5360a8ccdfb2f5a6c7b3eed331ec7ab0e2c792d579c6fff499c85c516c11fe14
|
||||
SNAPPY_SHA256=75c1fbb3d618dd3a0483bff0e26d0a92b495bbe5059c8b4f1c962b478b6e06e7
|
||||
SNAPPY_VERSION=1.1.9
|
||||
XZ_VERSION=5.2.5 # for LZMA
|
||||
ZLIB_VERSION=1.2.11
|
||||
ZLIB_VERSION=1.2.12
|
||||
ZSTD_VERSION=1.5.0
|
||||
WANGLE_SHA256=1002e9c32b6f4837f6a760016e3b3e22f3509880ef3eaad191c80dc92655f23f
|
||||
|
||||
@@ -1178,12 +1178,21 @@ popd
|
||||
|
||||
# create toolchain archive
|
||||
if [ ! -f $NAME-binaries-$DISTRO.tar.gz ]; then
|
||||
DISTRO_FULL_NAME=$DISTRO
|
||||
if [ "$for_arm" = true ]; then
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-aarch64"
|
||||
DISTRO_FULL_NAME=${DISTRO}
|
||||
if [[ "${DISTRO}" == centos* ]]; then
|
||||
if [[ "$for_arm" = "true" ]]; then
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-aarch64"
|
||||
else
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-x86_64"
|
||||
fi
|
||||
else
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-x86_64"
|
||||
if [[ "$for_arm" = "true" ]]; then
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-arm64"
|
||||
else
|
||||
DISTRO_FULL_NAME="$DISTRO_FULL_NAME-amd64"
|
||||
fi
|
||||
fi
|
||||
|
||||
tar --owner=root --group=root -cpvzf $NAME-binaries-$DISTRO_FULL_NAME.tar.gz -C /opt $NAME
|
||||
fi
|
||||
|
||||
|
||||
@@ -5,10 +5,28 @@ operating_system() {
|
||||
sort | cut -d '=' -f 2- | sed 's/"//g' | paste -s -d '-'
|
||||
}
|
||||
|
||||
check_operating_system() {
|
||||
if [ "$(operating_system)" != "$1" ]; then
|
||||
echo "Not the right operating system!"
|
||||
exit 1
|
||||
else
|
||||
echo "The right operating system."
|
||||
fi
|
||||
}
|
||||
|
||||
architecture() {
|
||||
uname -m
|
||||
}
|
||||
|
||||
check_architecture() {
|
||||
if [ "$(architecture)" != "$1" ]; then
|
||||
echo "Not the right architecture!"
|
||||
exit 1
|
||||
else
|
||||
echo "The right architecture."
|
||||
fi
|
||||
}
|
||||
|
||||
check_all_yum() {
|
||||
local missing=""
|
||||
for pkg in $1; do
|
||||
|
||||
686
include/mgp.py
686
include/mgp.py
File diff suppressed because it is too large
Load Diff
7
init
7
init
@@ -24,7 +24,7 @@ function setup_virtualenv () {
|
||||
fi
|
||||
|
||||
# create new virtualenv
|
||||
virtualenv -p python3 ve3 || exit 1
|
||||
python3 -m virtualenv -p python3 ve3 || exit 1
|
||||
source ve3/bin/activate
|
||||
pip --timeout 1000 install -r requirements.txt || exit 1
|
||||
deactivate
|
||||
@@ -66,7 +66,7 @@ fi
|
||||
|
||||
DISTRO=$(operating_system)
|
||||
ARCHITECTURE=$(architecture)
|
||||
if [ "${ARCHITECTURE}" = "arm64" ]; then
|
||||
if [ "${ARCHITECTURE}" = "arm64" ] || [ "${ARCHITECTURE}" = "aarch64" ]; then
|
||||
OS_SCRIPT=$DIR/environment/os/$DISTRO-arm.sh
|
||||
else
|
||||
OS_SCRIPT=$DIR/environment/os/$DISTRO.sh
|
||||
@@ -139,3 +139,6 @@ done;
|
||||
# Install precommit hook
|
||||
python3 -m pip install pre-commit
|
||||
python3 -m pre_commit install
|
||||
|
||||
# Link `include/mgp.py` with `release/mgp/mgp.py`
|
||||
ln -v -f include/mgp.py release/mgp/mgp.py
|
||||
|
||||
2
libs/.gitignore
vendored
2
libs/.gitignore
vendored
@@ -4,5 +4,5 @@
|
||||
!cleanup.sh
|
||||
!CMakeLists.txt
|
||||
!__main.cpp
|
||||
!jemalloc.cmake
|
||||
!pulsar.patch
|
||||
!antlr4.10.1.patch
|
||||
|
||||
@@ -106,6 +106,7 @@ import_external_library(antlr4 STATIC
|
||||
-DWITH_LIBCXX=OFF # because of debian bug
|
||||
-DCMAKE_SKIP_INSTALL_ALL_DEPENDENCY=true
|
||||
-DCMAKE_CXX_STANDARD=20
|
||||
-DANTLR_BUILD_CPP_TESTS=OFF
|
||||
BUILD_COMMAND $(MAKE) antlr4_static
|
||||
INSTALL_COMMAND $(MAKE) install)
|
||||
|
||||
|
||||
13
libs/antlr4.10.1.patch
Normal file
13
libs/antlr4.10.1.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/runtime/Cpp/runtime/CMakeLists.txt b/runtime/Cpp/runtime/CMakeLists.txt
|
||||
index baf46cac9..2e7756de8 100644
|
||||
--- a/runtime/Cpp/runtime/CMakeLists.txt
|
||||
+++ b/runtime/Cpp/runtime/CMakeLists.txt
|
||||
@@ -134,7 +134,7 @@ set_target_properties(antlr4_static
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${LIB_OUTPUT_DIR}
|
||||
COMPILE_FLAGS "${disabled_compile_warnings} ${extra_static_compile_flags}")
|
||||
|
||||
-install(TARGETS antlr4_shared
|
||||
+install(TARGETS antlr4_shared OPTIONAL
|
||||
EXPORT antlr4-targets
|
||||
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
@@ -1,43 +0,0 @@
|
||||
diff --git a/runtime/Cpp/runtime/CMakeLists.txt b/runtime/Cpp/runtime/CMakeLists.txt
|
||||
index a8503bb..11362cf 100644
|
||||
--- a/runtime/Cpp/runtime/CMakeLists.txt
|
||||
+++ b/runtime/Cpp/runtime/CMakeLists.txt
|
||||
@@ -5,8 +5,8 @@ set(THIRDPARTY_DIR ${CMAKE_BINARY_DIR}/runtime/thirdparty)
|
||||
set(UTFCPP_DIR ${THIRDPARTY_DIR}/utfcpp)
|
||||
ExternalProject_Add(
|
||||
utfcpp
|
||||
- GIT_REPOSITORY "git://github.com/nemtrif/utfcpp"
|
||||
- GIT_TAG "v3.1.1"
|
||||
+ GIT_REPOSITORY "https://github.com/nemtrif/utfcpp"
|
||||
+ GIT_TAG "v3.2.1"
|
||||
SOURCE_DIR ${UTFCPP_DIR}
|
||||
UPDATE_DISCONNECTED 1
|
||||
CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${UTFCPP_DIR}/install -Dgtest_force_shared_crt=ON
|
||||
@@ -118,7 +118,7 @@ set_target_properties(antlr4_static
|
||||
ARCHIVE_OUTPUT_DIRECTORY ${LIB_OUTPUT_DIR}
|
||||
COMPILE_FLAGS "${disabled_compile_warnings} ${extra_static_compile_flags}")
|
||||
|
||||
-install(TARGETS antlr4_shared
|
||||
+install(TARGETS antlr4_shared OPTIONAL
|
||||
DESTINATION lib
|
||||
EXPORT antlr4-targets)
|
||||
install(TARGETS antlr4_static
|
||||
diff --git a/runtime/Cpp/runtime/src/support/Any.h b/runtime/Cpp/runtime/src/support/Any.h
|
||||
index 468db98..65a473b 100644
|
||||
--- a/runtime/Cpp/runtime/src/support/Any.h
|
||||
+++ b/runtime/Cpp/runtime/src/support/Any.h
|
||||
@@ -122,12 +122,12 @@ private:
|
||||
}
|
||||
|
||||
private:
|
||||
- template<int N = 0, typename std::enable_if<N == N && std::is_nothrow_copy_constructible<T>::value, int>::type = 0>
|
||||
+ template<int N = 0, typename std::enable_if<N == N && std::is_copy_constructible<T>::value, int>::type = 0>
|
||||
Base* clone() const {
|
||||
return new Derived<T>(value);
|
||||
}
|
||||
|
||||
- template<int N = 0, typename std::enable_if<N == N && !std::is_nothrow_copy_constructible<T>::value, int>::type = 0>
|
||||
+ template<int N = 0, typename std::enable_if<N == N && !std::is_copy_constructible<T>::value, int>::type = 0>
|
||||
Base* clone() const {
|
||||
return nullptr;
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
set(JEMALLOC_DIR "${LIB_DIR}/jemalloc")
|
||||
|
||||
set(JEMALLOC_SRCS
|
||||
${JEMALLOC_DIR}/src/arena.c
|
||||
${JEMALLOC_DIR}/src/background_thread.c
|
||||
${JEMALLOC_DIR}/src/base.c
|
||||
${JEMALLOC_DIR}/src/bin.c
|
||||
${JEMALLOC_DIR}/src/bitmap.c
|
||||
${JEMALLOC_DIR}/src/ckh.c
|
||||
${JEMALLOC_DIR}/src/ctl.c
|
||||
${JEMALLOC_DIR}/src/div.c
|
||||
${JEMALLOC_DIR}/src/extent.c
|
||||
${JEMALLOC_DIR}/src/extent_dss.c
|
||||
${JEMALLOC_DIR}/src/extent_mmap.c
|
||||
${JEMALLOC_DIR}/src/hash.c
|
||||
${JEMALLOC_DIR}/src/hook.c
|
||||
${JEMALLOC_DIR}/src/jemalloc.c
|
||||
${JEMALLOC_DIR}/src/large.c
|
||||
${JEMALLOC_DIR}/src/log.c
|
||||
${JEMALLOC_DIR}/src/malloc_io.c
|
||||
${JEMALLOC_DIR}/src/mutex.c
|
||||
${JEMALLOC_DIR}/src/mutex_pool.c
|
||||
${JEMALLOC_DIR}/src/nstime.c
|
||||
${JEMALLOC_DIR}/src/pages.c
|
||||
${JEMALLOC_DIR}/src/prng.c
|
||||
${JEMALLOC_DIR}/src/prof.c
|
||||
${JEMALLOC_DIR}/src/rtree.c
|
||||
${JEMALLOC_DIR}/src/sc.c
|
||||
${JEMALLOC_DIR}/src/stats.c
|
||||
${JEMALLOC_DIR}/src/sz.c
|
||||
${JEMALLOC_DIR}/src/tcache.c
|
||||
${JEMALLOC_DIR}/src/test_hooks.c
|
||||
${JEMALLOC_DIR}/src/ticker.c
|
||||
${JEMALLOC_DIR}/src/tsd.c
|
||||
${JEMALLOC_DIR}/src/witness.c
|
||||
${JEMALLOC_DIR}/src/safety_check.c
|
||||
)
|
||||
|
||||
add_library(jemalloc ${JEMALLOC_SRCS})
|
||||
target_include_directories(jemalloc PUBLIC "${JEMALLOC_DIR}/include")
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
target_link_libraries(jemalloc PUBLIC Threads::Threads)
|
||||
|
||||
target_compile_definitions(jemalloc PRIVATE -DJEMALLOC_NO_PRIVATE_NAMESPACE)
|
||||
|
||||
if (CMAKE_BUILD_TYPE STREQUAL "DEBUG")
|
||||
target_compile_definitions(jemalloc PRIVATE -DJEMALLOC_DEBUG=1 -DJEMALLOC_PROF=1)
|
||||
endif()
|
||||
|
||||
target_compile_options(jemalloc PRIVATE -Wno-redundant-decls)
|
||||
# for RTLD_NEXT
|
||||
target_compile_definitions(jemalloc PRIVATE _GNU_SOURCE)
|
||||
|
||||
set_property(TARGET jemalloc APPEND PROPERTY INTERFACE_COMPILE_DEFINITIONS USE_JEMALLOC=1)
|
||||
@@ -105,7 +105,7 @@ repo_clone_try_double () {
|
||||
# Download from primary_urls might fail because the cache is not installed.
|
||||
declare -A primary_urls=(
|
||||
["antlr4-code"]="http://$local_cache_host/git/antlr4.git"
|
||||
["antlr4-generator"]="http://$local_cache_host/file/antlr-4.9.2-complete.jar"
|
||||
["antlr4-generator"]="http://$local_cache_host/file/antlr-4.10.1-complete.jar"
|
||||
["cppitertools"]="http://$local_cache_host/git/cppitertools.git"
|
||||
["rapidcheck"]="http://$local_cache_host/git/rapidcheck.git"
|
||||
["gbenchmark"]="http://$local_cache_host/git/benchmark.git"
|
||||
@@ -130,7 +130,7 @@ declare -A primary_urls=(
|
||||
# should fail.
|
||||
declare -A secondary_urls=(
|
||||
["antlr4-code"]="https://github.com/antlr/antlr4.git"
|
||||
["antlr4-generator"]="http://www.antlr.org/download/antlr-4.9.2-complete.jar"
|
||||
["antlr4-generator"]="https://www.antlr.org/download/antlr-4.10.1-complete.jar"
|
||||
["cppitertools"]="https://github.com/ryanhaining/cppitertools.git"
|
||||
["rapidcheck"]="https://github.com/emil-e/rapidcheck.git"
|
||||
["gbenchmark"]="https://github.com/google/benchmark.git"
|
||||
@@ -152,10 +152,10 @@ declare -A secondary_urls=(
|
||||
# antlr
|
||||
file_get_try_double "${primary_urls[antlr4-generator]}" "${secondary_urls[antlr4-generator]}"
|
||||
|
||||
antlr4_tag="4.9.2" # v4.9.2
|
||||
antlr4_tag="4.10.1" # v4.10.1
|
||||
repo_clone_try_double "${primary_urls[antlr4-code]}" "${secondary_urls[antlr4-code]}" "antlr4" "$antlr4_tag" true
|
||||
pushd antlr4
|
||||
git apply ../antlr4.patch
|
||||
git apply ../antlr4.10.1.patch
|
||||
popd
|
||||
|
||||
# cppitertools v2.0 2019-12-23
|
||||
@@ -199,7 +199,7 @@ git apply ../rocksdb.patch
|
||||
popd
|
||||
|
||||
# mgclient
|
||||
mgclient_tag="96e95c6845463cbe88948392be58d26da0d5ffd3" # (2022-02-08)
|
||||
mgclient_tag="v1.4.0" # (2022-06-14)
|
||||
repo_clone_try_double "${primary_urls[mgclient]}" "${secondary_urls[mgclient]}" "mgclient" "$mgclient_tag"
|
||||
sed -i 's/\${CMAKE_INSTALL_LIBDIR}/lib/' mgclient/src/CMakeLists.txt
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ It aims to deliver developers the speed, simplicity and scale required to build
|
||||
the next generation of applications driver by real-time connected data.")
|
||||
# Add `openssl` package to dependencies list. Used to generate SSL certificates.
|
||||
# We also depend on `python3` because we embed it in Memgraph.
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.0.0, curl >= 7.29.0, python3 >= 3.5.0, libstdc >= 6")
|
||||
set(CPACK_RPM_PACKAGE_REQUIRES "openssl >= 1.0.0, curl >= 7.29.0, python3 >= 3.5.0, libstdc >= 6, logrotate")
|
||||
|
||||
# All variables must be set before including.
|
||||
include(CPack)
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
FROM dokken/centos-stream-9
|
||||
# NOTE: If you change the base distro update release/package as well.
|
||||
|
||||
ARG release
|
||||
|
||||
RUN yum update && yum install -y \
|
||||
openssl libcurl libseccomp python3 python3-pip \
|
||||
--nobest --allowerasing \
|
||||
&& rm -rf /tmp/* \
|
||||
&& yum clean all
|
||||
|
||||
RUN pip3 install networkx==2.4 numpy==1.21.4 scipy==1.7.3
|
||||
|
||||
COPY ${release} /
|
||||
|
||||
# Install memgraph package
|
||||
RUN rpm -i ${release}
|
||||
|
||||
# Memgraph listens for Bolt Protocol on this port by default.
|
||||
EXPOSE 7687
|
||||
# Snapshots and logging volumes
|
||||
VOLUME /var/log/memgraph
|
||||
VOLUME /var/lib/memgraph
|
||||
# Configuration volume
|
||||
VOLUME /etc/memgraph
|
||||
|
||||
USER memgraph
|
||||
WORKDIR /usr/lib/memgraph
|
||||
|
||||
ENTRYPOINT ["/usr/lib/memgraph/memgraph"]
|
||||
CMD [""]
|
||||
3
release/mgp/.gitignore
vendored
Normal file
3
release/mgp/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.venv
|
||||
dist
|
||||
mgp.py
|
||||
201
release/mgp/LICENSE
Normal file
201
release/mgp/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
4
release/mgp/README.md
Normal file
4
release/mgp/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# mgp
|
||||
|
||||
PyPi package used for type hinting when creating MAGE modules. The get started
|
||||
using MAGE repository checkout the repository here: https://github.com/memgraph/mage.
|
||||
255
release/mgp/_mgp.py
Normal file
255
release/mgp/_mgp.py
Normal file
@@ -0,0 +1,255 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
class MgpIterable:
|
||||
def get() -> Any:
|
||||
pass
|
||||
|
||||
def next() -> Any:
|
||||
pass
|
||||
|
||||
|
||||
class Vertex:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def underlying_graph_is_mutable() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def iter_properties() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
def get_property(self, property_name: str) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def set_property(self, property_name: str, value: Any) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def get_id() -> "VertexId": # type: ignore
|
||||
pass
|
||||
|
||||
def label_at(self, index: int) -> "Label": # type: ignore
|
||||
pass
|
||||
|
||||
def labels_count() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
def add_label(self, label: Any):
|
||||
pass
|
||||
|
||||
def remove_label(self, label: Any):
|
||||
pass
|
||||
|
||||
def iter_in_edges() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
def iter_out_edges() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Edge:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def underlying_graph_is_mutable() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def iter_properties() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
def get_property(self, property_name: str) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def set_property(self, property_name: str, valuse: Any) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def get_type_name() -> str: # type: ignore
|
||||
pass
|
||||
|
||||
def get_id() -> "EdgeId": # type: ignore
|
||||
pass
|
||||
|
||||
def from_vertex() -> Vertex: # type: ignore
|
||||
pass
|
||||
|
||||
def to_vertex() -> Vertex: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Path:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def make_with_start(vertex: Vertex) -> "Path": # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Graph:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class CypherType:
|
||||
pass
|
||||
|
||||
|
||||
class Message:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def source_type() -> str: # type: ignore
|
||||
pass
|
||||
|
||||
def topic_name() -> str: # type: ignore
|
||||
pass
|
||||
|
||||
def key() -> bytes: # type: ignore
|
||||
pass
|
||||
|
||||
def timestamp() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
def offset() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
def payload() -> bytes: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Messages:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def message_at(self, id: int) -> Message: # type: ignore
|
||||
pass
|
||||
|
||||
def total_messages() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class UnknownError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnableToAllocateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientBufferError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OutOfRangeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LogicErrorError(Exception):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DeletedObjectError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidArgumentError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KeyAlreadyExistsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ImmutableObjectError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ValueConversionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SerializationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def type_nullable(elem: Any):
|
||||
pass
|
||||
|
||||
|
||||
def type_list(elem: Any):
|
||||
pass
|
||||
|
||||
|
||||
def type_bool():
|
||||
pass
|
||||
|
||||
|
||||
def type_string():
|
||||
pass
|
||||
|
||||
|
||||
def type_int():
|
||||
pass
|
||||
|
||||
|
||||
def type_float():
|
||||
pass
|
||||
|
||||
|
||||
def type_number():
|
||||
pass
|
||||
|
||||
|
||||
def type_map():
|
||||
pass
|
||||
|
||||
|
||||
def type_node():
|
||||
pass
|
||||
|
||||
|
||||
def type_relationship():
|
||||
pass
|
||||
|
||||
|
||||
def type_path():
|
||||
pass
|
||||
|
||||
|
||||
def type_date():
|
||||
pass
|
||||
|
||||
|
||||
def type_local_time():
|
||||
pass
|
||||
|
||||
|
||||
def type_local_date_time():
|
||||
pass
|
||||
|
||||
|
||||
def type_duration():
|
||||
pass
|
||||
|
||||
|
||||
def type_any():
|
||||
pass
|
||||
|
||||
|
||||
class _MODULE:
|
||||
@staticmethod
|
||||
def add_read_procedure(wrapper):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_write_procedure(wrapper):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_transformation(wrapper):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_function(wrapper):
|
||||
pass
|
||||
22
release/mgp/instructions.md
Normal file
22
release/mgp/instructions.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# How to publish new versions
|
||||
## Prerequisites
|
||||
1. Installed poetry
|
||||
```
|
||||
pip install poetry
|
||||
```
|
||||
2. Set up [API tokens](https://pypi.org/help/#apitoken)
|
||||
3. Be a collaborator on [pypi](https://pypi.org/project/mgp/)
|
||||
|
||||
## Making changes
|
||||
1. Make changes to the package
|
||||
2. Bump version in `pyproject.tml`
|
||||
3. `poetry build`
|
||||
4. `poetry publish`
|
||||
|
||||
## Why is this not automatized?
|
||||
|
||||
Because someone always has to manually bump up the version in `pyproject.toml`
|
||||
|
||||
## Why does `_mgp.py` exists?
|
||||
Because we are mocking here all the types that are created by Memgraph
|
||||
in order to fix typing errors in `mgp.py`.
|
||||
23
release/mgp/pyproject.toml
Normal file
23
release/mgp/pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[tool.poetry]
|
||||
name = "mgp"
|
||||
version = "1.0.0"
|
||||
description = "Memgraph's module for developing MAGE modules. Used only for type hinting!"
|
||||
authors = [
|
||||
"MasterMedo <mislav.vuletic@gmail.com>",
|
||||
"jbajic <jure.bajic@memgraph.io>",
|
||||
"katarinasupe <katarina.supe@memgraph.io>",
|
||||
"antejavor <ante.javor@memgraph.io>",
|
||||
"antaljanosbenjamin <benjamin.antal@memgraph.io>",
|
||||
]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
include = ["mgp.py", "_mgp.py"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -7,8 +7,8 @@ RUN yum -y update \
|
||||
# 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-centos-7.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-7.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-7.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-centos-7-x86_64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-7-x86_64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-7-x86_64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
FROM centos:8
|
||||
|
||||
ARG TOOLCHAIN_VERSION
|
||||
|
||||
RUN dnf -y update \
|
||||
&& dnf install -y 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-centos-8.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-8.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-8.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
14
release/package/centos-9/Dockerfile
Normal file
14
release/package/centos-9/Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM quay.io/centos/centos:stream9
|
||||
|
||||
ARG TOOLCHAIN_VERSION
|
||||
|
||||
RUN yum -y update \
|
||||
&& yum install -y 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-centos-9-x86_64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-centos-9-x86_64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-centos-9-x86_64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# 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-debian-10.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-10.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-10.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-debian-10-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-10-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-10-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
17
release/package/debian-11-arm/Dockerfile
Normal file
17
release/package/debian-11-arm/Dockerfile
Normal file
@@ -0,0 +1,17 @@
|
||||
FROM debian:11
|
||||
|
||||
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-debian-11-arm64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-11-arm64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-11-arm64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# 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-debian-11.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-11.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-11.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-debian-11-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-debian-11-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-debian-11-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
@@ -4,10 +4,10 @@ services:
|
||||
build:
|
||||
context: centos-7
|
||||
container_name: "mgbuild_centos-7"
|
||||
mgbuild_centos-8:
|
||||
mgbuild_centos-9:
|
||||
build:
|
||||
context: centos-8
|
||||
container_name: "mgbuild_centos-8"
|
||||
context: centos-9
|
||||
container_name: "mgbuild_centos-9"
|
||||
mgbuild_debian-10:
|
||||
build:
|
||||
context: debian-10
|
||||
@@ -24,3 +24,7 @@ services:
|
||||
build:
|
||||
context: ubuntu-20.04
|
||||
container_name: "mgbuild_ubuntu-20.04"
|
||||
mgbuild_ubuntu-22.04:
|
||||
build:
|
||||
context: ubuntu-22.04
|
||||
container_name: "mgbuild_ubuntu-22.04"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
set -Eeuo pipefail
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
SUPPORTED_OS=(centos-7 centos-8 debian-10 debian-11 ubuntu-18.04 ubuntu-20.04)
|
||||
SUPPORTED_OS=(centos-7 centos-9 debian-10 debian-11 ubuntu-18.04 ubuntu-20.04 ubuntu-22.04 debian-11-arm)
|
||||
PROJECT_ROOT="$SCRIPT_DIR/../.."
|
||||
TOOLCHAIN_VERSION="toolchain-v4"
|
||||
ACTIVATE_TOOLCHAIN="source /opt/${TOOLCHAIN_VERSION}/activate"
|
||||
@@ -67,14 +67,18 @@ make_package () {
|
||||
# environment/os/{os}.sh does not come within the toolchain package. When
|
||||
# migrating to the next version of toolchain do that, and remove the
|
||||
# TOOLCHAIN_RUN_DEPS installation from here.
|
||||
echo "Installing dependencies..."
|
||||
echo "Installing dependencies using '/memgraph/environment/os/$os.sh' script..."
|
||||
docker exec "$build_container" bash -c "/memgraph/environment/os/$os.sh install TOOLCHAIN_RUN_DEPS"
|
||||
docker exec "$build_container" bash -c "/memgraph/environment/os/$os.sh install MEMGRAPH_BUILD_DEPS"
|
||||
|
||||
echo "Building targeted package..."
|
||||
docker exec "$build_container" bash -c "cd /memgraph && $ACTIVATE_TOOLCHAIN && ./init"
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && rm -rf ./*"
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && $ACTIVATE_TOOLCHAIN && cmake -DCMAKE_BUILD_TYPE=release $telemetry_id_override_flag .."
|
||||
if [[ "$os" == "debian-11-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 .."
|
||||
fi
|
||||
# ' is used instead of " because we need to run make within the allowed
|
||||
# container resources.
|
||||
# shellcheck disable=SC2016
|
||||
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# 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-18.04.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-18.04-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
@@ -10,8 +10,8 @@ RUN apt update && apt install -y \
|
||||
# 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-20.04.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04.tar.gz -C /opt
|
||||
RUN wget -q https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/${TOOLCHAIN_VERSION}/${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-20.04-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
|
||||
17
release/package/ubuntu-22.04/Dockerfile
Normal file
17
release/package/ubuntu-22.04/Dockerfile
Normal file
@@ -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-amd64.tar.gz \
|
||||
-O ${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-amd64.tar.gz \
|
||||
&& tar xzvf ${TOOLCHAIN_VERSION}-binaries-ubuntu-22.04-amd64.tar.gz -C /opt
|
||||
|
||||
ENTRYPOINT ["sleep", "infinity"]
|
||||
@@ -5,16 +5,22 @@ add_subdirectory(lisp)
|
||||
add_subdirectory(utils)
|
||||
add_subdirectory(requests)
|
||||
add_subdirectory(io)
|
||||
add_subdirectory(io/simulator)
|
||||
add_subdirectory(kvstore)
|
||||
add_subdirectory(telemetry)
|
||||
add_subdirectory(communication)
|
||||
add_subdirectory(memory)
|
||||
add_subdirectory(storage/v2)
|
||||
add_subdirectory(storage/v3)
|
||||
add_subdirectory(integrations)
|
||||
add_subdirectory(query)
|
||||
add_subdirectory(query/v2)
|
||||
add_subdirectory(slk)
|
||||
add_subdirectory(rpc)
|
||||
add_subdirectory(auth)
|
||||
add_subdirectory(parser)
|
||||
add_subdirectory(expr)
|
||||
add_subdirectory(coordinator)
|
||||
|
||||
if (MG_ENTERPRISE)
|
||||
add_subdirectory(audit)
|
||||
|
||||
@@ -37,7 +37,7 @@ const std::vector<Permission> kPermissionsAll = {
|
||||
Permission::CONSTRAINT, Permission::DUMP, Permission::AUTH, Permission::REPLICATION,
|
||||
Permission::DURABILITY, Permission::READ_FILE, Permission::FREE_MEMORY, Permission::TRIGGER,
|
||||
Permission::CONFIG, Permission::STREAM, Permission::MODULE_READ, Permission::MODULE_WRITE,
|
||||
Permission::WEBSOCKET};
|
||||
Permission::WEBSOCKET, Permission::SCHEMA};
|
||||
} // namespace
|
||||
|
||||
std::string PermissionToString(Permission permission) {
|
||||
@@ -84,6 +84,8 @@ std::string PermissionToString(Permission permission) {
|
||||
return "MODULE_WRITE";
|
||||
case Permission::WEBSOCKET:
|
||||
return "WEBSOCKET";
|
||||
case Permission::SCHEMA:
|
||||
return "SCHEMA";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ enum class Permission : uint64_t {
|
||||
STREAM = 1U << 17U,
|
||||
MODULE_READ = 1U << 18U,
|
||||
MODULE_WRITE = 1U << 19U,
|
||||
WEBSOCKET = 1U << 20U
|
||||
WEBSOCKET = 1U << 20U,
|
||||
SCHEMA = 1U << 21U
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
|
||||
19
src/common/types.hpp
Normal file
19
src/common/types.hpp
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace memgraph::common {
|
||||
enum class SchemaType : uint8_t { BOOL, INT, STRING, DATE, LOCALTIME, LOCALDATETIME, DURATION };
|
||||
|
||||
} // namespace memgraph::common
|
||||
@@ -105,9 +105,16 @@ class WebsocketSession : public std::enable_shared_from_this<WebsocketSession<TS
|
||||
boost::asio::socket_base::keep_alive option(true);
|
||||
|
||||
// Set a decorator to change the Server of the handshake
|
||||
ws_.set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::response_type &res) {
|
||||
ws_.set_option(boost::beast::websocket::stream_base::decorator([&req](boost::beast::websocket::response_type &res) {
|
||||
res.set(boost::beast::http::field::server, std::string("Memgraph Bolt WS"));
|
||||
res.set(boost::beast::http::field::sec_websocket_protocol, "binary");
|
||||
|
||||
// We need to do this to support WASM clients, which explicitly send this flag
|
||||
// in their upgrade request
|
||||
// Neo4j client breaks when this flag is sent
|
||||
if (const auto secondary_protocol = req.base().find(boost::beast::http::field::sec_websocket_protocol);
|
||||
secondary_protocol != res.base().end() && secondary_protocol->value() == "binary") {
|
||||
res.set(boost::beast::http::field::sec_websocket_protocol, "binary");
|
||||
}
|
||||
}));
|
||||
ws_.binary(true);
|
||||
|
||||
@@ -162,7 +169,7 @@ class WebsocketSession : public std::enable_shared_from_this<WebsocketSession<TS
|
||||
boost::asio::bind_executor(strand_, std::bind_front(&WebsocketSession::OnRead, shared_from_this())));
|
||||
}
|
||||
|
||||
void OnRead(const boost::system::error_code &ec, [[maybe_unused]] const size_t bytes_transferred) {
|
||||
void OnRead(const boost::system::error_code &ec, const size_t bytes_transferred) {
|
||||
// This indicates that the WebsocketSession was closed
|
||||
if (ec == boost::beast::websocket::error::closed) {
|
||||
return;
|
||||
@@ -322,7 +329,8 @@ class Session final : public std::enable_shared_from_this<Session<TSession, TSes
|
||||
socket.lowest_layer().non_blocking(false);
|
||||
});
|
||||
timeout_timer_.expires_at(boost::asio::steady_timer::time_point::max());
|
||||
spdlog::info("Accepted a connection from {}:", service_name_, remote_endpoint_.address(), remote_endpoint_.port());
|
||||
spdlog::info("Accepted a connection from {}: {}:{}", service_name_, remote_endpoint_.address(),
|
||||
remote_endpoint_.port());
|
||||
}
|
||||
|
||||
void DoRead() {
|
||||
|
||||
9
src/coordinator/CMakeLists.txt
Normal file
9
src/coordinator/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
set(coordinator_src_files
|
||||
coordinator.cpp
|
||||
shard_map.cpp)
|
||||
|
||||
find_package(fmt REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(mg-coordinator STATIC ${coordinator_src_files})
|
||||
target_link_libraries(mg-coordinator stdc++fs Threads::Threads fmt::fmt mg-utils mg-storage-v3)
|
||||
129
src/coordinator/coordinator.cpp
Normal file
129
src/coordinator/coordinator.cpp
Normal file
@@ -0,0 +1,129 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include <coordinator/coordinator.hpp>
|
||||
|
||||
namespace memgraph::coordinator {
|
||||
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(HeartbeatRequest &&heartbeat_request) {
|
||||
spdlog::info("Coordinator handling HeartbeatRequest");
|
||||
|
||||
// add this storage engine to any under-replicated shards that it is not already a part of
|
||||
|
||||
auto initializing_rsms_for_shard_manager =
|
||||
shard_map_.AssignShards(heartbeat_request.from_storage_manager, heartbeat_request.initialized_rsms);
|
||||
|
||||
return HeartbeatResponse{
|
||||
.shards_to_initialize = initializing_rsms_for_shard_manager,
|
||||
};
|
||||
}
|
||||
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(HlcRequest &&hlc_request) {
|
||||
HlcResponse res{};
|
||||
|
||||
auto hlc_shard_map = shard_map_.GetHlc();
|
||||
|
||||
MG_ASSERT(!(hlc_request.last_shard_map_version.logical_id > hlc_shard_map.logical_id));
|
||||
|
||||
res.new_hlc = Hlc{
|
||||
.logical_id = ++highest_allocated_timestamp_,
|
||||
// TODO(tyler) probably pass some more context to the Coordinator here
|
||||
// so that we can use our wall clock and enforce monotonicity.
|
||||
// .coordinator_wall_clock = io_.Now(),
|
||||
};
|
||||
|
||||
// Allways return fresher shard_map for now.
|
||||
res.fresher_shard_map = std::make_optional(shard_map_);
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(AllocateEdgeIdBatchRequest &&ahr) {
|
||||
AllocateEdgeIdBatchResponse res{};
|
||||
|
||||
uint64_t low = highest_allocated_edge_id_;
|
||||
|
||||
highest_allocated_edge_id_ += ahr.batch_size;
|
||||
|
||||
uint64_t high = highest_allocated_edge_id_;
|
||||
|
||||
res.low = low;
|
||||
res.high = high;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// This splits the shard immediately beneath the provided
|
||||
/// split key, keeping the assigned peers identical for now,
|
||||
/// but letting them be gradually migrated over time.
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(SplitShardRequest &&split_shard_request) {
|
||||
SplitShardResponse res{};
|
||||
|
||||
if (split_shard_request.previous_shard_map_version != shard_map_.shard_map_version) {
|
||||
res.success = false;
|
||||
} else {
|
||||
res.success = shard_map_.SplitShard(split_shard_request.previous_shard_map_version, split_shard_request.label_id,
|
||||
split_shard_request.split_key);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// This adds the provided storage engine to the standby storage engine pool,
|
||||
/// which can be used to rebalance storage over time.
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(
|
||||
RegisterStorageEngineRequest && /* register_storage_engine_request */) {
|
||||
RegisterStorageEngineResponse res{};
|
||||
// TODO
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
/// This begins the process of draining the provided storage engine from all raft
|
||||
/// clusters that it might be participating in.
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(
|
||||
DeregisterStorageEngineRequest && /* register_storage_engine_request */) {
|
||||
DeregisterStorageEngineResponse res{};
|
||||
// TODO
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(InitializeLabelRequest &&initialize_label_request) {
|
||||
InitializeLabelResponse res{};
|
||||
|
||||
std::optional<LabelId> new_label_id = shard_map_.InitializeNewLabel(
|
||||
initialize_label_request.label_name, initialize_label_request.schema, initialize_label_request.replication_factor,
|
||||
initialize_label_request.last_shard_map_version);
|
||||
|
||||
if (new_label_id) {
|
||||
res.new_label_id = new_label_id.value();
|
||||
res.fresher_shard_map = std::nullopt;
|
||||
res.success = true;
|
||||
} else {
|
||||
res.fresher_shard_map = shard_map_;
|
||||
res.success = false;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
CoordinatorWriteResponses Coordinator::ApplyWrite(AllocatePropertyIdsRequest &&allocate_property_ids_request) {
|
||||
AllocatePropertyIdsResponse res{};
|
||||
|
||||
auto property_ids = shard_map_.AllocatePropertyIds(allocate_property_ids_request.property_names);
|
||||
|
||||
res.property_ids = property_ids;
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
} // namespace memgraph::coordinator
|
||||
196
src/coordinator/coordinator.hpp
Normal file
196
src/coordinator/coordinator.hpp
Normal file
@@ -0,0 +1,196 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
|
||||
#include <coordinator/hybrid_logical_clock.hpp>
|
||||
#include <coordinator/shard_map.hpp>
|
||||
#include <io/simulator/simulator.hpp>
|
||||
#include <io/time.hpp>
|
||||
#include <io/transport.hpp>
|
||||
#include <storage/v3/id_types.hpp>
|
||||
#include <storage/v3/schemas.hpp>
|
||||
|
||||
namespace memgraph::coordinator {
|
||||
|
||||
using memgraph::io::Address;
|
||||
using memgraph::storage::v3::LabelId;
|
||||
using memgraph::storage::v3::PropertyId;
|
||||
using memgraph::storage::v3::SchemaProperty;
|
||||
using SimT = memgraph::io::simulator::SimulatorTransport;
|
||||
using PrimaryKey = std::vector<PropertyValue>;
|
||||
|
||||
struct HlcRequest {
|
||||
Hlc last_shard_map_version;
|
||||
};
|
||||
|
||||
struct HlcResponse {
|
||||
Hlc new_hlc;
|
||||
std::optional<ShardMap> fresher_shard_map;
|
||||
};
|
||||
|
||||
struct GetShardMapRequest {
|
||||
// No state
|
||||
};
|
||||
|
||||
struct GetShardMapResponse {
|
||||
ShardMap shard_map;
|
||||
};
|
||||
|
||||
struct AllocateHlcBatchRequest {
|
||||
Hlc low;
|
||||
Hlc high;
|
||||
};
|
||||
|
||||
struct AllocateHlcBatchResponse {
|
||||
bool success;
|
||||
Hlc low;
|
||||
Hlc high;
|
||||
};
|
||||
|
||||
struct AllocateEdgeIdBatchRequest {
|
||||
size_t batch_size;
|
||||
};
|
||||
|
||||
struct AllocateEdgeIdBatchResponse {
|
||||
uint64_t low;
|
||||
uint64_t high;
|
||||
};
|
||||
|
||||
struct AllocatePropertyIdsRequest {
|
||||
std::vector<std::string> property_names;
|
||||
};
|
||||
|
||||
struct AllocatePropertyIdsResponse {
|
||||
std::map<std::string, PropertyId> property_ids;
|
||||
};
|
||||
|
||||
struct SplitShardRequest {
|
||||
Hlc previous_shard_map_version;
|
||||
LabelId label_id;
|
||||
PrimaryKey split_key;
|
||||
};
|
||||
|
||||
struct SplitShardResponse {
|
||||
bool success;
|
||||
};
|
||||
|
||||
struct RegisterStorageEngineRequest {
|
||||
Address address;
|
||||
};
|
||||
|
||||
struct RegisterStorageEngineResponse {
|
||||
bool success;
|
||||
};
|
||||
|
||||
struct DeregisterStorageEngineRequest {
|
||||
Address address;
|
||||
};
|
||||
|
||||
struct DeregisterStorageEngineResponse {
|
||||
bool success;
|
||||
};
|
||||
|
||||
struct InitializeLabelRequest {
|
||||
std::string label_name;
|
||||
std::vector<SchemaProperty> schema;
|
||||
size_t replication_factor;
|
||||
Hlc last_shard_map_version;
|
||||
};
|
||||
|
||||
struct InitializeLabelResponse {
|
||||
bool success;
|
||||
LabelId new_label_id;
|
||||
std::optional<ShardMap> fresher_shard_map;
|
||||
};
|
||||
|
||||
struct HeartbeatRequest {
|
||||
Address from_storage_manager;
|
||||
std::set<boost::uuids::uuid> initialized_rsms;
|
||||
};
|
||||
|
||||
struct HeartbeatResponse {
|
||||
std::vector<ShardToInitialize> shards_to_initialize;
|
||||
};
|
||||
|
||||
using CoordinatorWriteRequests =
|
||||
std::variant<HlcRequest, AllocateEdgeIdBatchRequest, SplitShardRequest, RegisterStorageEngineRequest,
|
||||
DeregisterStorageEngineRequest, InitializeLabelRequest, AllocatePropertyIdsRequest, HeartbeatRequest>;
|
||||
using CoordinatorWriteResponses = std::variant<HlcResponse, AllocateEdgeIdBatchResponse, SplitShardResponse,
|
||||
RegisterStorageEngineResponse, DeregisterStorageEngineResponse,
|
||||
InitializeLabelResponse, AllocatePropertyIdsResponse, HeartbeatResponse>;
|
||||
|
||||
using CoordinatorReadRequests = std::variant<GetShardMapRequest>;
|
||||
using CoordinatorReadResponses = std::variant<GetShardMapResponse>;
|
||||
|
||||
class Coordinator {
|
||||
public:
|
||||
explicit Coordinator(ShardMap sm) : shard_map_{std::move(sm)} {}
|
||||
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static
|
||||
CoordinatorReadResponses Read(CoordinatorReadRequests requests) {
|
||||
return std::visit([&](auto &&request) { return HandleRead(std::forward<decltype(request)>(request)); },
|
||||
std::move(requests)); // NOLINT(hicpp-move-const-arg,performance-move-const-arg)
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(readability-convert-member-functions-to-static
|
||||
CoordinatorWriteResponses Apply(CoordinatorWriteRequests requests) {
|
||||
return std::visit([&](auto &&request) mutable { return ApplyWrite(std::forward<decltype(request)>(request)); },
|
||||
std::move(requests));
|
||||
}
|
||||
|
||||
private:
|
||||
ShardMap shard_map_;
|
||||
uint64_t highest_allocated_timestamp_;
|
||||
|
||||
/// Query engines need to periodically request batches of unique edge IDs.
|
||||
uint64_t highest_allocated_edge_id_;
|
||||
|
||||
CoordinatorReadResponses HandleRead(GetShardMapRequest && /* get_shard_map_request */) {
|
||||
GetShardMapResponse res;
|
||||
res.shard_map = shard_map_;
|
||||
return res;
|
||||
}
|
||||
|
||||
CoordinatorWriteResponses ApplyWrite(HeartbeatRequest &&heartbeat_request);
|
||||
|
||||
CoordinatorWriteResponses ApplyWrite(HlcRequest &&hlc_request);
|
||||
|
||||
CoordinatorWriteResponses ApplyWrite(AllocateEdgeIdBatchRequest &&ahr);
|
||||
|
||||
/// This splits the shard immediately beneath the provided
|
||||
/// split key, keeping the assigned peers identical for now,
|
||||
/// but letting them be gradually migrated over time.
|
||||
CoordinatorWriteResponses ApplyWrite(SplitShardRequest &&split_shard_request);
|
||||
|
||||
/// This adds the provided storage engine to the standby storage engine pool,
|
||||
/// which can be used to rebalance storage over time.
|
||||
static CoordinatorWriteResponses ApplyWrite(RegisterStorageEngineRequest && /* register_storage_engine_request */);
|
||||
|
||||
/// This begins the process of draining the provided storage engine from all raft
|
||||
/// clusters that it might be participating in.
|
||||
static CoordinatorWriteResponses ApplyWrite(DeregisterStorageEngineRequest && /* register_storage_engine_request */);
|
||||
|
||||
CoordinatorWriteResponses ApplyWrite(InitializeLabelRequest &&initialize_label_request);
|
||||
|
||||
CoordinatorWriteResponses ApplyWrite(AllocatePropertyIdsRequest &&allocate_property_ids_request);
|
||||
};
|
||||
|
||||
} // namespace memgraph::coordinator
|
||||
24
src/coordinator/coordinator_client.hpp
Normal file
24
src/coordinator/coordinator_client.hpp
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "coordinator/coordinator.hpp"
|
||||
#include "io/rsm/rsm_client.hpp"
|
||||
|
||||
namespace memgraph::coordinator {
|
||||
|
||||
using memgraph::io::rsm::RsmClient;
|
||||
|
||||
template <typename IoImpl>
|
||||
using CoordinatorClient = RsmClient<IoImpl, CoordinatorWriteRequests, CoordinatorWriteResponses,
|
||||
CoordinatorReadRequests, CoordinatorReadResponses>;
|
||||
} // namespace memgraph::coordinator
|
||||
23
src/coordinator/coordinator_rsm.hpp
Normal file
23
src/coordinator/coordinator_rsm.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "coordinator/coordinator.hpp"
|
||||
#include "io/rsm/raft.hpp"
|
||||
|
||||
namespace memgraph::coordinator {
|
||||
|
||||
template <typename IoImpl>
|
||||
using CoordinatorRsm = memgraph::io::rsm::Raft<IoImpl, Coordinator, CoordinatorWriteRequests, CoordinatorWriteResponses,
|
||||
CoordinatorReadRequests, CoordinatorReadResponses>;
|
||||
|
||||
} // namespace memgraph::coordinator
|
||||
36
src/coordinator/hybrid_logical_clock.hpp
Normal file
36
src/coordinator/hybrid_logical_clock.hpp
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
|
||||
#include "io/time.hpp"
|
||||
|
||||
namespace memgraph::coordinator {
|
||||
|
||||
using Time = memgraph::io::Time;
|
||||
|
||||
/// Hybrid-logical clock
|
||||
struct Hlc {
|
||||
uint64_t logical_id;
|
||||
Time coordinator_wall_clock;
|
||||
|
||||
auto operator<=>(const Hlc &other) const { return logical_id <=> other.logical_id; }
|
||||
|
||||
bool operator==(const Hlc &other) const = default;
|
||||
bool operator<(const Hlc &other) const = default;
|
||||
bool operator==(const uint64_t other) const { return logical_id == other; }
|
||||
bool operator<(const uint64_t other) const { return logical_id < other; }
|
||||
bool operator>=(const uint64_t other) const { return logical_id >= other; }
|
||||
};
|
||||
|
||||
} // namespace memgraph::coordinator
|
||||
90
src/coordinator/shard_map.cpp
Normal file
90
src/coordinator/shard_map.cpp
Normal file
@@ -0,0 +1,90 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "coordinator/shard_map.hpp"
|
||||
#include "storage/v3/temporal.hpp"
|
||||
|
||||
namespace memgraph::coordinator {
|
||||
|
||||
using memgraph::common::SchemaType;
|
||||
using memgraph::storage::v3::TemporalData;
|
||||
using memgraph::storage::v3::TemporalType;
|
||||
|
||||
PrimaryKey SchemaToMinKey(const std::vector<SchemaProperty> &schema) {
|
||||
PrimaryKey ret{};
|
||||
|
||||
const int64_t min_int = std::numeric_limits<int64_t>::min();
|
||||
|
||||
const TemporalData date{TemporalType::Date, min_int};
|
||||
const TemporalData local_time{TemporalType::LocalTime, min_int};
|
||||
const TemporalData local_date_time{TemporalType::LocalDateTime, min_int};
|
||||
const TemporalData duration{TemporalType::Duration, min_int};
|
||||
|
||||
for (const auto &schema_property : schema) {
|
||||
switch (schema_property.type) {
|
||||
case SchemaType::BOOL:
|
||||
ret.emplace_back(PropertyValue(false));
|
||||
break;
|
||||
case SchemaType::INT:
|
||||
ret.emplace_back(PropertyValue(min_int));
|
||||
break;
|
||||
case SchemaType::STRING:
|
||||
ret.emplace_back(PropertyValue(""));
|
||||
break;
|
||||
case SchemaType::DATE:
|
||||
ret.emplace_back(PropertyValue(date));
|
||||
break;
|
||||
case SchemaType::LOCALTIME:
|
||||
ret.emplace_back(PropertyValue(local_time));
|
||||
break;
|
||||
case SchemaType::LOCALDATETIME:
|
||||
ret.emplace_back(PropertyValue(local_date_time));
|
||||
break;
|
||||
case SchemaType::DURATION:
|
||||
ret.emplace_back(PropertyValue(duration));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::optional<LabelId> ShardMap::InitializeNewLabel(std::string label_name, std::vector<SchemaProperty> schema,
|
||||
size_t replication_factor, Hlc last_shard_map_version) {
|
||||
if (shard_map_version != last_shard_map_version || labels.contains(label_name)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const LabelId label_id = LabelId::FromUint(++max_label_id);
|
||||
|
||||
labels.emplace(std::move(label_name), label_id);
|
||||
|
||||
PrimaryKey initial_key = SchemaToMinKey(schema);
|
||||
Shard empty_shard = {};
|
||||
|
||||
Shards shards = {
|
||||
{initial_key, empty_shard},
|
||||
};
|
||||
|
||||
LabelSpace label_space{
|
||||
.schema = std::move(schema),
|
||||
.shards = shards,
|
||||
.replication_factor = replication_factor,
|
||||
};
|
||||
|
||||
label_spaces.emplace(label_id, label_space);
|
||||
|
||||
IncrementShardMapVersion();
|
||||
|
||||
return label_id;
|
||||
}
|
||||
|
||||
} // namespace memgraph::coordinator
|
||||
276
src/coordinator/shard_map.hpp
Normal file
276
src/coordinator/shard_map.hpp
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
|
||||
#include "common/types.hpp"
|
||||
#include "coordinator/hybrid_logical_clock.hpp"
|
||||
#include "io/address.hpp"
|
||||
#include "storage/v3/config.hpp"
|
||||
#include "storage/v3/id_types.hpp"
|
||||
#include "storage/v3/property_value.hpp"
|
||||
#include "storage/v3/schemas.hpp"
|
||||
#include "storage/v3/temporal.hpp"
|
||||
|
||||
namespace memgraph::coordinator {
|
||||
|
||||
using memgraph::io::Address;
|
||||
using memgraph::storage::v3::Config;
|
||||
using memgraph::storage::v3::LabelId;
|
||||
using memgraph::storage::v3::PropertyId;
|
||||
using memgraph::storage::v3::PropertyValue;
|
||||
using memgraph::storage::v3::SchemaProperty;
|
||||
|
||||
enum class Status : uint8_t {
|
||||
CONSENSUS_PARTICIPANT,
|
||||
INITIALIZING,
|
||||
// TODO(tyler) this will possibly have more states,
|
||||
// depending on the reconfiguration protocol that we
|
||||
// implement.
|
||||
};
|
||||
|
||||
struct AddressAndStatus {
|
||||
memgraph::io::Address address;
|
||||
Status status;
|
||||
friend bool operator<(const AddressAndStatus &lhs, const AddressAndStatus &rhs) { return lhs.address < rhs.address; }
|
||||
};
|
||||
|
||||
using PrimaryKey = std::vector<PropertyValue>;
|
||||
using Shard = std::vector<AddressAndStatus>;
|
||||
using Shards = std::map<PrimaryKey, Shard>;
|
||||
using LabelName = std::string;
|
||||
using PropertyName = std::string;
|
||||
using PropertyMap = std::map<PropertyName, PropertyId>;
|
||||
|
||||
struct ShardToInitialize {
|
||||
boost::uuids::uuid uuid;
|
||||
LabelId label_id;
|
||||
PrimaryKey min_key;
|
||||
std::optional<PrimaryKey> max_key;
|
||||
Config config;
|
||||
};
|
||||
|
||||
PrimaryKey SchemaToMinKey(const std::vector<SchemaProperty> &schema);
|
||||
|
||||
struct LabelSpace {
|
||||
std::vector<SchemaProperty> schema;
|
||||
std::map<PrimaryKey, Shard> shards;
|
||||
size_t replication_factor;
|
||||
};
|
||||
|
||||
struct ShardMap {
|
||||
Hlc shard_map_version;
|
||||
uint64_t max_property_id;
|
||||
std::map<PropertyName, PropertyId> properties;
|
||||
uint64_t max_label_id;
|
||||
std::map<LabelName, LabelId> labels;
|
||||
std::map<LabelId, LabelSpace> label_spaces;
|
||||
std::map<LabelId, std::vector<SchemaProperty>> schemas;
|
||||
|
||||
Shards GetShards(const LabelName &label) {
|
||||
const auto id = labels.at(label);
|
||||
auto &shards = label_spaces.at(id).shards;
|
||||
return shards;
|
||||
}
|
||||
|
||||
// TODO(gabor) later we will want to update the wallclock time with
|
||||
// the given Io<impl>'s time as well
|
||||
Hlc IncrementShardMapVersion() noexcept {
|
||||
++shard_map_version.logical_id;
|
||||
return shard_map_version;
|
||||
}
|
||||
|
||||
Hlc GetHlc() const noexcept { return shard_map_version; }
|
||||
|
||||
// Returns the shard UUIDs that have been assigned but not yet acknowledged for this storage manager
|
||||
std::vector<ShardToInitialize> AssignShards(Address storage_manager, std::set<boost::uuids::uuid> initialized) {
|
||||
std::vector<ShardToInitialize> ret{};
|
||||
|
||||
bool mutated = false;
|
||||
|
||||
for (auto &[label_id, label_space] : label_spaces) {
|
||||
for (auto &[low_key, shard] : label_space.shards) {
|
||||
// TODO(tyler) avoid these triple-nested loops by having the heartbeat include better info
|
||||
bool machine_contains_shard = false;
|
||||
|
||||
for (auto &aas : shard) {
|
||||
if (initialized.contains(aas.address.unique_id)) {
|
||||
spdlog::info("marking shard as full consensus participant: {}", aas.address.unique_id);
|
||||
aas.status = Status::CONSENSUS_PARTICIPANT;
|
||||
machine_contains_shard = true;
|
||||
} else {
|
||||
const bool same_machine = aas.address.last_known_ip == storage_manager.last_known_ip &&
|
||||
aas.address.last_known_port == storage_manager.last_known_port;
|
||||
if (same_machine) {
|
||||
machine_contains_shard = true;
|
||||
ret.push_back(ShardToInitialize{
|
||||
.uuid = aas.address.unique_id,
|
||||
.label_id = label_id,
|
||||
.min_key = low_key,
|
||||
.max_key = std::nullopt,
|
||||
.config = Config{},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!machine_contains_shard && shard.size() < label_space.replication_factor) {
|
||||
Address address = storage_manager;
|
||||
|
||||
// TODO(tyler) use deterministic UUID so that coordinators don't diverge here
|
||||
address.unique_id = boost::uuids::uuid{boost::uuids::random_generator()()},
|
||||
|
||||
ret.push_back(ShardToInitialize{
|
||||
.uuid = address.unique_id,
|
||||
.label_id = label_id,
|
||||
.min_key = low_key,
|
||||
.max_key = std::nullopt,
|
||||
.config = Config{},
|
||||
});
|
||||
|
||||
AddressAndStatus aas = {
|
||||
.address = address,
|
||||
.status = Status::INITIALIZING,
|
||||
};
|
||||
|
||||
shard.emplace_back(aas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mutated) {
|
||||
IncrementShardMapVersion();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool SplitShard(Hlc previous_shard_map_version, LabelId label_id, const PrimaryKey &key) {
|
||||
if (previous_shard_map_version != shard_map_version) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto &label_space = label_spaces.at(label_id);
|
||||
auto &shards_in_map = label_space.shards;
|
||||
|
||||
MG_ASSERT(!shards_in_map.empty());
|
||||
MG_ASSERT(!shards_in_map.contains(key));
|
||||
MG_ASSERT(label_spaces.contains(label_id));
|
||||
|
||||
// Finding the Shard that the new PrimaryKey should map to.
|
||||
auto prev = std::prev(shards_in_map.upper_bound(key));
|
||||
Shard duplicated_shard = prev->second;
|
||||
|
||||
// Apply the split
|
||||
shards_in_map[key] = duplicated_shard;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<LabelId> InitializeNewLabel(std::string label_name, std::vector<SchemaProperty> schema,
|
||||
size_t replication_factor, Hlc last_shard_map_version);
|
||||
|
||||
void AddServer(Address server_address) {
|
||||
// Find a random place for the server to plug in
|
||||
}
|
||||
|
||||
LabelId GetLabelId(const std::string &label) const { return labels.at(label); }
|
||||
|
||||
Shards GetShardsForRange(const LabelName &label_name, const PrimaryKey &start_key, const PrimaryKey &end_key) const {
|
||||
MG_ASSERT(start_key <= end_key);
|
||||
MG_ASSERT(labels.contains(label_name));
|
||||
|
||||
LabelId label_id = labels.at(label_name);
|
||||
|
||||
const auto &label_space = label_spaces.at(label_id);
|
||||
|
||||
const auto &shards_for_label = label_space.shards;
|
||||
|
||||
MG_ASSERT(shards_for_label.begin()->first <= start_key,
|
||||
"the ShardMap must always contain a minimal key that is less than or equal to any requested key");
|
||||
|
||||
auto it = std::prev(shards_for_label.upper_bound(start_key));
|
||||
const auto end_it = shards_for_label.upper_bound(end_key);
|
||||
|
||||
Shards shards{};
|
||||
|
||||
std::copy(it, end_it, std::inserter(shards, shards.end()));
|
||||
|
||||
return shards;
|
||||
}
|
||||
|
||||
Shard GetShardForKey(const LabelName &label_name, const PrimaryKey &key) const {
|
||||
MG_ASSERT(labels.contains(label_name));
|
||||
|
||||
LabelId label_id = labels.at(label_name);
|
||||
|
||||
const auto &label_space = label_spaces.at(label_id);
|
||||
|
||||
MG_ASSERT(label_space.shards.begin()->first <= key,
|
||||
"the ShardMap must always contain a minimal key that is less than or equal to any requested key");
|
||||
|
||||
return std::prev(label_space.shards.upper_bound(key))->second;
|
||||
}
|
||||
|
||||
Shard GetShardForKey(const LabelId &label_id, const PrimaryKey &key) const {
|
||||
MG_ASSERT(label_spaces.contains(label_id));
|
||||
|
||||
const auto &label_space = label_spaces.at(label_id);
|
||||
|
||||
MG_ASSERT(label_space.shards.begin()->first <= key,
|
||||
"the ShardMap must always contain a minimal key that is less than or equal to any requested key");
|
||||
|
||||
return std::prev(label_space.shards.upper_bound(key))->second;
|
||||
}
|
||||
|
||||
PropertyMap AllocatePropertyIds(const std::vector<PropertyName> &new_properties) {
|
||||
PropertyMap ret{};
|
||||
|
||||
bool mutated = false;
|
||||
|
||||
for (const auto &property_name : new_properties) {
|
||||
if (properties.contains(property_name)) {
|
||||
auto property_id = properties.at(property_name);
|
||||
ret.emplace(property_name, property_id);
|
||||
} else {
|
||||
mutated = true;
|
||||
|
||||
const PropertyId property_id = PropertyId::FromUint(++max_property_id);
|
||||
ret.emplace(property_name, property_id);
|
||||
properties.emplace(property_name, property_id);
|
||||
}
|
||||
}
|
||||
|
||||
if (mutated) {
|
||||
IncrementShardMapVersion();
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::optional<PropertyId> GetPropertyId(const std::string &property_name) const {
|
||||
if (properties.contains(property_name)) {
|
||||
return properties.at(property_name);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace memgraph::coordinator
|
||||
20
src/expr/CMakeLists.txt
Normal file
20
src/expr/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
define_add_lcp(add_lcp_expr lcp_expr_cpp_files generated_lcp_expr_files)
|
||||
|
||||
add_lcp_expr(semantic/symbol.lcp)
|
||||
|
||||
add_custom_target(generate_lcp_expr DEPENDS ${generated_lcp_expr_files})
|
||||
|
||||
set(mg_expr_sources
|
||||
${lcp_expr_cpp_files}
|
||||
parsing.cpp)
|
||||
|
||||
find_package(Boost REQUIRED)
|
||||
|
||||
add_library(mg-expr STATIC ${mg_expr_sources})
|
||||
add_dependencies(mg-expr generate_lcp_expr)
|
||||
target_include_directories(mg-expr PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
target_include_directories(mg-expr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
target_include_directories(mg-expr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/ast)
|
||||
target_include_directories(mg-expr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/interpret)
|
||||
target_include_directories(mg-expr PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/semantic)
|
||||
target_link_libraries(mg-expr cppitertools Boost::headers mg-utils mg-parser)
|
||||
35
src/expr/ast.hpp
Normal file
35
src/expr/ast.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
#pragma once
|
||||
|
||||
#ifndef MG_AST_INCLUDE_PATH
|
||||
#ifdef MG_CLANG_TIDY_CHECK
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define MG_AST_INCLUDE_PATH "query/v2/frontend/ast/ast.hpp"
|
||||
#else
|
||||
#error Missing AST include path
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef MG_INJECTED_NAMESPACE_NAME
|
||||
#ifdef MG_CLANG_TIDY_CHECK
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define MG_INJECTED_NAMESPACE_NAME memgraph::query::v2
|
||||
#else
|
||||
#error Missing AST namespace
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include MG_AST_INCLUDE_PATH
|
||||
|
||||
namespace memgraph::expr {
|
||||
using namespace MG_INJECTED_NAMESPACE_NAME; // NOLINT(google-build-using-namespace)
|
||||
} // namespace memgraph::expr
|
||||
135
src/expr/ast/ast_visitor.hpp
Normal file
135
src/expr/ast/ast_visitor.hpp
Normal file
@@ -0,0 +1,135 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "utils/visitor.hpp"
|
||||
|
||||
namespace MG_INJECTED_NAMESPACE_NAME {
|
||||
|
||||
// Forward declares for Tree visitors.
|
||||
class CypherQuery;
|
||||
class SingleQuery;
|
||||
class CypherUnion;
|
||||
class NamedExpression;
|
||||
class Identifier;
|
||||
class PropertyLookup;
|
||||
class LabelsTest;
|
||||
class Aggregation;
|
||||
class Function;
|
||||
class Reduce;
|
||||
class Coalesce;
|
||||
class Extract;
|
||||
class All;
|
||||
class Single;
|
||||
class Any;
|
||||
class None;
|
||||
class ParameterLookup;
|
||||
class CallProcedure;
|
||||
class Create;
|
||||
class Match;
|
||||
class Return;
|
||||
class With;
|
||||
class Pattern;
|
||||
class NodeAtom;
|
||||
class EdgeAtom;
|
||||
class PrimitiveLiteral;
|
||||
class ListLiteral;
|
||||
class MapLiteral;
|
||||
class OrOperator;
|
||||
class XorOperator;
|
||||
class AndOperator;
|
||||
class NotOperator;
|
||||
class AdditionOperator;
|
||||
class SubtractionOperator;
|
||||
class MultiplicationOperator;
|
||||
class DivisionOperator;
|
||||
class ModOperator;
|
||||
class UnaryPlusOperator;
|
||||
class UnaryMinusOperator;
|
||||
class IsNullOperator;
|
||||
class NotEqualOperator;
|
||||
class EqualOperator;
|
||||
class LessOperator;
|
||||
class GreaterOperator;
|
||||
class LessEqualOperator;
|
||||
class GreaterEqualOperator;
|
||||
class InListOperator;
|
||||
class SubscriptOperator;
|
||||
class ListSlicingOperator;
|
||||
class IfOperator;
|
||||
class Delete;
|
||||
class Where;
|
||||
class SetProperty;
|
||||
class SetProperties;
|
||||
class SetLabels;
|
||||
class RemoveProperty;
|
||||
class RemoveLabels;
|
||||
class Merge;
|
||||
class Unwind;
|
||||
class AuthQuery;
|
||||
class ExplainQuery;
|
||||
class ProfileQuery;
|
||||
class IndexQuery;
|
||||
class InfoQuery;
|
||||
class ConstraintQuery;
|
||||
class RegexMatch;
|
||||
class DumpQuery;
|
||||
class ReplicationQuery;
|
||||
class LockPathQuery;
|
||||
class LoadCsv;
|
||||
class FreeMemoryQuery;
|
||||
class TriggerQuery;
|
||||
class IsolationLevelQuery;
|
||||
class CreateSnapshotQuery;
|
||||
class StreamQuery;
|
||||
class SettingQuery;
|
||||
class VersionQuery;
|
||||
class Foreach;
|
||||
class SchemaQuery;
|
||||
|
||||
using TreeCompositeVisitor = memgraph::utils::CompositeVisitor<
|
||||
SingleQuery, CypherUnion, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator,
|
||||
SubtractionOperator, MultiplicationOperator, DivisionOperator, ModOperator, NotEqualOperator, EqualOperator,
|
||||
LessOperator, GreaterOperator, LessEqualOperator, GreaterEqualOperator, InListOperator, SubscriptOperator,
|
||||
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>;
|
||||
|
||||
using TreeLeafVisitor = memgraph::utils::LeafVisitor<Identifier, PrimitiveLiteral, ParameterLookup>;
|
||||
|
||||
class HierarchicalTreeVisitor : public TreeCompositeVisitor, public TreeLeafVisitor {
|
||||
public:
|
||||
using TreeCompositeVisitor::PostVisit;
|
||||
using TreeCompositeVisitor::PreVisit;
|
||||
using TreeLeafVisitor::Visit;
|
||||
using typename TreeLeafVisitor::ReturnType;
|
||||
};
|
||||
|
||||
template <class TResult>
|
||||
class ExpressionVisitor
|
||||
: public memgraph::utils::Visitor<
|
||||
TResult, NamedExpression, OrOperator, XorOperator, AndOperator, NotOperator, AdditionOperator,
|
||||
SubtractionOperator, MultiplicationOperator, DivisionOperator, ModOperator, NotEqualOperator, EqualOperator,
|
||||
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> {};
|
||||
|
||||
template <class TResult>
|
||||
class QueryVisitor
|
||||
: public memgraph::utils::Visitor<TResult, CypherQuery, ExplainQuery, ProfileQuery, IndexQuery, AuthQuery,
|
||||
InfoQuery, ConstraintQuery, DumpQuery, ReplicationQuery, LockPathQuery,
|
||||
FreeMemoryQuery, TriggerQuery, IsolationLevelQuery, CreateSnapshotQuery,
|
||||
StreamQuery, SettingQuery, VersionQuery, SchemaQuery> {};
|
||||
|
||||
} // namespace MG_INJECTED_NAMESPACE_NAME
|
||||
3035
src/expr/ast/cypher_main_visitor.hpp
Normal file
3035
src/expr/ast/cypher_main_visitor.hpp
Normal file
File diff suppressed because it is too large
Load Diff
271
src/expr/ast/pretty_print.hpp
Normal file
271
src/expr/ast/pretty_print.hpp
Normal file
@@ -0,0 +1,271 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <type_traits>
|
||||
|
||||
#include "expr/ast.hpp"
|
||||
#include "expr/typed_value.hpp"
|
||||
#include "utils/algorithm.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/string.hpp"
|
||||
|
||||
namespace memgraph::expr {
|
||||
namespace detail {
|
||||
template <typename T>
|
||||
void PrintObject(std::ostream *out, const T &arg) {
|
||||
static_assert(!std::is_convertible<T, Expression *>::value,
|
||||
"This overload shouldn't be called with pointers convertible "
|
||||
"to Expression *. This means your other PrintObject overloads aren't "
|
||||
"being called for certain AST nodes when they should (or perhaps such "
|
||||
"overloads don't exist yet).");
|
||||
*out << arg;
|
||||
}
|
||||
|
||||
inline void PrintObject(std::ostream *out, const std::string &str) { *out << utils::Escape(str); }
|
||||
|
||||
inline void PrintObject(std::ostream *out, Aggregation::Op op) { *out << Aggregation::OpToString(op); }
|
||||
|
||||
inline void PrintObject(std::ostream *out, Expression *expr);
|
||||
|
||||
inline void PrintObject(std::ostream *out, Identifier *expr) { PrintObject(out, static_cast<Expression *>(expr)); }
|
||||
|
||||
template <typename T>
|
||||
void PrintObject(std::ostream *out, const std::vector<T> &vec) {
|
||||
*out << "[";
|
||||
utils::PrintIterable(*out, vec, ", ", [](auto &stream, const auto &item) { PrintObject(&stream, item); });
|
||||
*out << "]";
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void PrintObject(std::ostream *out, const std::vector<T, utils::Allocator<T>> &vec) {
|
||||
*out << "[";
|
||||
utils::PrintIterable(*out, vec, ", ", [](auto &stream, const auto &item) { PrintObject(&stream, item); });
|
||||
*out << "]";
|
||||
}
|
||||
|
||||
template <typename K, typename V>
|
||||
void PrintObject(std::ostream *out, const std::map<K, V> &map) {
|
||||
*out << "{";
|
||||
utils::PrintIterable(*out, map, ", ", [](auto &stream, const auto &item) {
|
||||
PrintObject(&stream, item.first);
|
||||
stream << ": ";
|
||||
PrintObject(&stream, item.second);
|
||||
});
|
||||
*out << "}";
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void PrintObject(std::ostream *out, const utils::pmr::map<utils::pmr::string, T> &map) {
|
||||
*out << "{";
|
||||
utils::PrintIterable(*out, map, ", ", [](auto &stream, const auto &item) {
|
||||
PrintObject(&stream, item.first);
|
||||
stream << ": ";
|
||||
PrintObject(&stream, item.second);
|
||||
});
|
||||
*out << "}";
|
||||
}
|
||||
|
||||
template <typename T1, typename T2, typename T3>
|
||||
inline void PrintObject(std::ostream *out, const TypedValueT<T1, T2, T3> &value) {
|
||||
using TypedValue = TypedValueT<T1, T2, T3>;
|
||||
switch (value.type()) {
|
||||
case TypedValue::Type::Null:
|
||||
*out << "null";
|
||||
break;
|
||||
case TypedValue::Type::String:
|
||||
PrintObject(out, value.ValueString());
|
||||
break;
|
||||
case TypedValue::Type::Bool:
|
||||
*out << (value.ValueBool() ? "true" : "false");
|
||||
break;
|
||||
case TypedValue::Type::Int:
|
||||
PrintObject(out, value.ValueInt());
|
||||
break;
|
||||
case TypedValue::Type::Double:
|
||||
PrintObject(out, value.ValueDouble());
|
||||
break;
|
||||
case TypedValue::Type::List:
|
||||
PrintObject(out, value.ValueList());
|
||||
break;
|
||||
case TypedValue::Type::Map:
|
||||
PrintObject(out, value.ValueMap());
|
||||
break;
|
||||
case TypedValue::Type::Date:
|
||||
PrintObject(out, value.ValueDate());
|
||||
break;
|
||||
case TypedValue::Type::Duration:
|
||||
PrintObject(out, value.ValueDuration());
|
||||
break;
|
||||
case TypedValue::Type::LocalTime:
|
||||
PrintObject(out, value.ValueLocalTime());
|
||||
break;
|
||||
case TypedValue::Type::LocalDateTime:
|
||||
PrintObject(out, value.ValueLocalDateTime());
|
||||
break;
|
||||
default:
|
||||
MG_ASSERT(false, "PrintObject(std::ostream *out, const TypedValue &value) should not reach here");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void PrintOperatorArgs(std::ostream *out, const T &arg) {
|
||||
*out << " ";
|
||||
PrintObject(out, arg);
|
||||
*out << ")";
|
||||
}
|
||||
|
||||
template <typename T, typename... Ts>
|
||||
void PrintOperatorArgs(std::ostream *out, const T &arg, const Ts &...args) {
|
||||
*out << " ";
|
||||
PrintObject(out, arg);
|
||||
PrintOperatorArgs(out, args...);
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
void PrintOperator(std::ostream *out, const std::string &name, const Ts &...args) {
|
||||
*out << "(" << name;
|
||||
PrintOperatorArgs(out, args...);
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
class ExpressionPrettyPrinter : public ExpressionVisitor<void> {
|
||||
public:
|
||||
explicit ExpressionPrettyPrinter(std::ostream *out) : out_(out) {}
|
||||
|
||||
// Unary operators
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define UNARY_OPERATOR_VISIT(OP_NODE, OP_STR) \
|
||||
/* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \
|
||||
void Visit(OP_NODE &op) override { detail::PrintOperator(out_, OP_STR, op.expression_); }
|
||||
|
||||
UNARY_OPERATOR_VISIT(NotOperator, "Not");
|
||||
UNARY_OPERATOR_VISIT(UnaryPlusOperator, "+");
|
||||
UNARY_OPERATOR_VISIT(UnaryMinusOperator, "-");
|
||||
UNARY_OPERATOR_VISIT(IsNullOperator, "IsNull");
|
||||
|
||||
#undef UNARY_OPERATOR_VISIT
|
||||
|
||||
// Binary operators
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define BINARY_OPERATOR_VISIT(OP_NODE, OP_STR) \
|
||||
/* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \
|
||||
void Visit(OP_NODE &op) override { detail::PrintOperator(out_, OP_STR, op.expression1_, op.expression2_); }
|
||||
|
||||
BINARY_OPERATOR_VISIT(OrOperator, "Or");
|
||||
BINARY_OPERATOR_VISIT(XorOperator, "Xor");
|
||||
BINARY_OPERATOR_VISIT(AndOperator, "And");
|
||||
BINARY_OPERATOR_VISIT(AdditionOperator, "+");
|
||||
BINARY_OPERATOR_VISIT(SubtractionOperator, "-");
|
||||
BINARY_OPERATOR_VISIT(MultiplicationOperator, "*");
|
||||
BINARY_OPERATOR_VISIT(DivisionOperator, "/");
|
||||
BINARY_OPERATOR_VISIT(ModOperator, "%");
|
||||
BINARY_OPERATOR_VISIT(NotEqualOperator, "!=");
|
||||
BINARY_OPERATOR_VISIT(EqualOperator, "==");
|
||||
BINARY_OPERATOR_VISIT(LessOperator, "<");
|
||||
BINARY_OPERATOR_VISIT(GreaterOperator, ">");
|
||||
BINARY_OPERATOR_VISIT(LessEqualOperator, "<=");
|
||||
BINARY_OPERATOR_VISIT(GreaterEqualOperator, ">=");
|
||||
BINARY_OPERATOR_VISIT(InListOperator, "In");
|
||||
BINARY_OPERATOR_VISIT(SubscriptOperator, "Subscript");
|
||||
|
||||
#undef BINARY_OPERATOR_VISIT
|
||||
|
||||
// Other
|
||||
void Visit(ListSlicingOperator &op) override {
|
||||
detail::PrintOperator(out_, "ListSlicing", op.list_, op.lower_bound_, op.upper_bound_);
|
||||
}
|
||||
|
||||
void Visit(IfOperator &op) override {
|
||||
detail::PrintOperator(out_, "If", op.condition_, op.then_expression_, op.else_expression_);
|
||||
}
|
||||
|
||||
void Visit(ListLiteral &op) override { detail::PrintOperator(out_, "ListLiteral", op.elements_); }
|
||||
|
||||
void Visit(MapLiteral &op) override {
|
||||
std::map<std::string, Expression *> map;
|
||||
for (const auto &kv : op.elements_) {
|
||||
map[kv.first.name] = kv.second;
|
||||
}
|
||||
detail::PrintObject(out_, map);
|
||||
}
|
||||
|
||||
void Visit(LabelsTest &op) override { detail::PrintOperator(out_, "LabelsTest", op.expression_); }
|
||||
|
||||
void Visit(Aggregation &op) override { detail::PrintOperator(out_, "Aggregation", op.op_); }
|
||||
|
||||
void Visit(Function &op) override { detail::PrintOperator(out_, "Function", op.function_name_, op.arguments_); }
|
||||
|
||||
void Visit(Reduce &op) override {
|
||||
detail::PrintOperator(out_, "Reduce", op.accumulator_, op.initializer_, op.identifier_, op.list_, op.expression_);
|
||||
}
|
||||
|
||||
void Visit(Coalesce &op) override { detail::PrintOperator(out_, "Coalesce", op.expressions_); }
|
||||
|
||||
void Visit(Extract &op) override { detail::PrintOperator(out_, "Extract", op.identifier_, op.list_, op.expression_); }
|
||||
|
||||
void Visit(All &op) override {
|
||||
detail::PrintOperator(out_, "All", op.identifier_, op.list_expression_, op.where_->expression_);
|
||||
}
|
||||
|
||||
void Visit(Single &op) override {
|
||||
detail::PrintOperator(out_, "Single", op.identifier_, op.list_expression_, op.where_->expression_);
|
||||
}
|
||||
|
||||
void Visit(Any &op) override {
|
||||
detail::PrintOperator(out_, "Any", op.identifier_, op.list_expression_, op.where_->expression_);
|
||||
}
|
||||
|
||||
void Visit(None &op) override {
|
||||
detail::PrintOperator(out_, "None", op.identifier_, op.list_expression_, op.where_->expression_);
|
||||
}
|
||||
|
||||
void Visit(Identifier &op) override { detail::PrintOperator(out_, "Identifier", op.name_); }
|
||||
|
||||
void Visit(PrimitiveLiteral &op) override { detail::PrintObject(out_, op.value_); }
|
||||
|
||||
void Visit(PropertyLookup &op) override {
|
||||
detail::PrintOperator(out_, "PropertyLookup", op.expression_, op.property_.name);
|
||||
}
|
||||
|
||||
void Visit(ParameterLookup &op) override { detail::PrintOperator(out_, "ParameterLookup", op.token_position_); }
|
||||
|
||||
void Visit(NamedExpression &op) override { detail::PrintOperator(out_, "NamedExpression", op.name_, op.expression_); }
|
||||
|
||||
void Visit(RegexMatch &op) override { detail::PrintOperator(out_, "=~", op.string_expr_, op.regex_); }
|
||||
|
||||
private:
|
||||
std::ostream *out_;
|
||||
};
|
||||
|
||||
namespace detail {
|
||||
inline void PrintObject(std::ostream *out, Expression *expr) {
|
||||
if (expr) {
|
||||
ExpressionPrettyPrinter printer{out};
|
||||
expr->Accept(printer);
|
||||
} else {
|
||||
*out << "<null>";
|
||||
}
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
inline void PrintExpression(Expression *expr, std::ostream *out) {
|
||||
ExpressionPrettyPrinter printer{out};
|
||||
expr->Accept(printer);
|
||||
}
|
||||
|
||||
inline void PrintExpression(NamedExpression *expr, std::ostream *out) {
|
||||
ExpressionPrettyPrinter printer{out};
|
||||
expr->Accept(printer);
|
||||
}
|
||||
} // namespace memgraph::expr
|
||||
52
src/expr/exceptions.hpp
Normal file
52
src/expr/exceptions.hpp
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "utils/exceptions.hpp"
|
||||
|
||||
namespace memgraph::expr {
|
||||
|
||||
class SyntaxException : public utils::BasicException {
|
||||
public:
|
||||
using utils::BasicException::BasicException;
|
||||
SyntaxException() : SyntaxException("") {}
|
||||
};
|
||||
|
||||
class SemanticException : public utils::BasicException {
|
||||
public:
|
||||
using utils::BasicException::BasicException;
|
||||
SemanticException() : BasicException("") {}
|
||||
};
|
||||
|
||||
class ExpressionRuntimeException : public utils::BasicException {
|
||||
public:
|
||||
using utils::BasicException::BasicException;
|
||||
};
|
||||
|
||||
class RedeclareVariableError : public SemanticException {
|
||||
public:
|
||||
explicit RedeclareVariableError(const std::string &name) : SemanticException("Redeclaring variable: " + name + ".") {}
|
||||
};
|
||||
|
||||
class UnboundVariableError : public SemanticException {
|
||||
public:
|
||||
explicit UnboundVariableError(const std::string &name) : SemanticException("Unbound variable: " + name + ".") {}
|
||||
};
|
||||
|
||||
class TypeMismatchError : public SemanticException {
|
||||
public:
|
||||
TypeMismatchError(const std::string &name, const std::string &datum, const std::string &expected)
|
||||
: SemanticException(fmt::format("Type mismatch: {} already defined as {}, expected {}.", name, datum, expected)) {
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace memgraph::expr
|
||||
826
src/expr/interpret/eval.hpp
Normal file
826
src/expr/interpret/eval.hpp
Normal file
@@ -0,0 +1,826 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
/// @file
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <regex>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
|
||||
#include "expr/ast.hpp"
|
||||
#include "expr/exceptions.hpp"
|
||||
#include "expr/interpret/frame.hpp"
|
||||
#include "expr/semantic/symbol_table.hpp"
|
||||
#include "utils/exceptions.hpp"
|
||||
|
||||
namespace memgraph::expr {
|
||||
|
||||
struct StorageTag {};
|
||||
struct QueryEngineTag {};
|
||||
|
||||
template <typename TypedValue, typename EvaluationContext, typename DbAccessor, typename StorageView, typename LabelId,
|
||||
typename PropertyValue, typename ConvFunctor, typename Error, typename Tag = StorageTag>
|
||||
class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
||||
public:
|
||||
ExpressionEvaluator(Frame<TypedValue> *frame, const SymbolTable &symbol_table, const EvaluationContext &ctx,
|
||||
DbAccessor *dba, StorageView view)
|
||||
: frame_(frame), symbol_table_(&symbol_table), ctx_(&ctx), dba_(dba), view_(view) {}
|
||||
|
||||
using ExpressionVisitor<TypedValue>::Visit;
|
||||
|
||||
utils::MemoryResource *GetMemoryResource() const { return ctx_->memory; }
|
||||
|
||||
TypedValue Visit(NamedExpression &named_expression) override {
|
||||
const auto &symbol = symbol_table_->at(named_expression);
|
||||
auto value = named_expression.expression_->Accept(*this);
|
||||
frame_->at(symbol) = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
TypedValue Visit(Identifier &ident) override {
|
||||
return TypedValue(frame_->at(symbol_table_->at(ident)), ctx_->memory);
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define BINARY_OPERATOR_VISITOR(OP_NODE, CPP_OP, CYPHER_OP) \
|
||||
/* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \
|
||||
TypedValue Visit(OP_NODE &op) override { \
|
||||
auto val1 = op.expression1_->Accept(*this); \
|
||||
auto val2 = op.expression2_->Accept(*this); \
|
||||
try { \
|
||||
return val1 CPP_OP val2; \
|
||||
} catch (const TypedValueException &) { \
|
||||
throw ExpressionRuntimeException("Invalid types: {} and {} for '{}'.", val1.type(), val2.type(), #CYPHER_OP); \
|
||||
} \
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define UNARY_OPERATOR_VISITOR(OP_NODE, CPP_OP, CYPHER_OP) \
|
||||
/* NOLINTNEXTLINE(bugprone-macro-parentheses) */ \
|
||||
TypedValue Visit(OP_NODE &op) override { \
|
||||
auto val = op.expression_->Accept(*this); \
|
||||
try { \
|
||||
return CPP_OP val; \
|
||||
} catch (const TypedValueException &) { \
|
||||
throw ExpressionRuntimeException("Invalid type {} for '{}'.", val.type(), #CYPHER_OP); \
|
||||
} \
|
||||
}
|
||||
|
||||
BINARY_OPERATOR_VISITOR(OrOperator, ||, OR);
|
||||
BINARY_OPERATOR_VISITOR(XorOperator, ^, XOR);
|
||||
BINARY_OPERATOR_VISITOR(AdditionOperator, +, +);
|
||||
BINARY_OPERATOR_VISITOR(SubtractionOperator, -, -);
|
||||
BINARY_OPERATOR_VISITOR(MultiplicationOperator, *, *);
|
||||
BINARY_OPERATOR_VISITOR(DivisionOperator, /, /);
|
||||
BINARY_OPERATOR_VISITOR(ModOperator, %, %);
|
||||
BINARY_OPERATOR_VISITOR(NotEqualOperator, !=, <>);
|
||||
BINARY_OPERATOR_VISITOR(EqualOperator, ==, =);
|
||||
BINARY_OPERATOR_VISITOR(LessOperator, <, <);
|
||||
BINARY_OPERATOR_VISITOR(GreaterOperator, >, >);
|
||||
BINARY_OPERATOR_VISITOR(LessEqualOperator, <=, <=);
|
||||
BINARY_OPERATOR_VISITOR(GreaterEqualOperator, >=, >=);
|
||||
|
||||
UNARY_OPERATOR_VISITOR(NotOperator, !, NOT);
|
||||
UNARY_OPERATOR_VISITOR(UnaryPlusOperator, +, +);
|
||||
UNARY_OPERATOR_VISITOR(UnaryMinusOperator, -, -);
|
||||
|
||||
#undef BINARY_OPERATOR_VISITOR
|
||||
#undef UNARY_OPERATOR_VISITOR
|
||||
|
||||
TypedValue Visit(AndOperator &op) override {
|
||||
auto value1 = op.expression1_->Accept(*this);
|
||||
if (value1.IsBool() && !value1.ValueBool()) {
|
||||
// If first expression is false, don't evaluate the second one.
|
||||
return value1;
|
||||
}
|
||||
auto value2 = op.expression2_->Accept(*this);
|
||||
try {
|
||||
return value1 && value2;
|
||||
} catch (const TypedValueException &) {
|
||||
throw ExpressionRuntimeException("Invalid types: {} and {} for AND.", value1.type(), value2.type());
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue Visit(IfOperator &if_operator) override {
|
||||
auto condition = if_operator.condition_->Accept(*this);
|
||||
if (condition.IsNull()) {
|
||||
return if_operator.else_expression_->Accept(*this);
|
||||
}
|
||||
if (condition.type() != TypedValue::Type::Bool) {
|
||||
// At the moment IfOperator is used only in CASE construct.
|
||||
throw ExpressionRuntimeException("CASE expected boolean expression, got {}.", condition.type());
|
||||
}
|
||||
if (condition.ValueBool()) {
|
||||
return if_operator.then_expression_->Accept(*this);
|
||||
}
|
||||
return if_operator.else_expression_->Accept(*this);
|
||||
}
|
||||
|
||||
TypedValue Visit(InListOperator &in_list) override {
|
||||
auto literal = in_list.expression1_->Accept(*this);
|
||||
auto _list = in_list.expression2_->Accept(*this);
|
||||
if (_list.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
// Exceptions have higher priority than returning nulls when list expression
|
||||
// is not null.
|
||||
if (_list.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("IN expected a list, got {}.", _list.type());
|
||||
}
|
||||
const auto &list = _list.ValueList();
|
||||
|
||||
// If literal is NULL there is no need to try to compare it with every
|
||||
// element in the list since result of every comparison will be NULL. There
|
||||
// is one special case that we must test explicitly: if list is empty then
|
||||
// result is false since no comparison will be performed.
|
||||
if (list.empty()) return TypedValue(false, ctx_->memory);
|
||||
if (literal.IsNull()) return TypedValue(ctx_->memory);
|
||||
|
||||
auto has_null = false;
|
||||
for (const auto &element : list) {
|
||||
auto result = literal == element;
|
||||
if (result.IsNull()) {
|
||||
has_null = true;
|
||||
} else if (result.ValueBool()) {
|
||||
return TypedValue(true, ctx_->memory);
|
||||
}
|
||||
}
|
||||
if (has_null) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
return TypedValue(false, ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(SubscriptOperator &list_indexing) override {
|
||||
auto lhs = list_indexing.expression1_->Accept(*this);
|
||||
auto index = list_indexing.expression2_->Accept(*this);
|
||||
if (!lhs.IsList() && !lhs.IsMap() && !lhs.IsVertex() && !lhs.IsEdge() && !lhs.IsNull())
|
||||
throw ExpressionRuntimeException(
|
||||
"Expected a list, a map, a node or an edge to index with '[]', got "
|
||||
"{}.",
|
||||
lhs.type());
|
||||
if (lhs.IsNull() || index.IsNull()) return TypedValue(ctx_->memory);
|
||||
if (lhs.IsList()) {
|
||||
if (!index.IsInt())
|
||||
throw ExpressionRuntimeException("Expected an integer as a list index, got {}.", index.type());
|
||||
auto index_int = index.ValueInt();
|
||||
// NOTE: Take non-const reference to list, so that we can move out the
|
||||
// indexed element as the result.
|
||||
auto &list = lhs.ValueList();
|
||||
if (index_int < 0) {
|
||||
index_int += static_cast<int64_t>(list.size());
|
||||
}
|
||||
if (index_int >= static_cast<int64_t>(list.size()) || index_int < 0) return TypedValue(ctx_->memory);
|
||||
// NOTE: Explicit move is needed, so that we return the move constructed
|
||||
// value and preserve the correct MemoryResource.
|
||||
return std::move(list[index_int]);
|
||||
}
|
||||
|
||||
if (lhs.IsMap()) {
|
||||
if (!index.IsString())
|
||||
throw ExpressionRuntimeException("Expected a string as a map index, got {}.", index.type());
|
||||
// NOTE: Take non-const reference to map, so that we can move out the
|
||||
// looked-up element as the result.
|
||||
auto &map = lhs.ValueMap();
|
||||
auto found = map.find(index.ValueString());
|
||||
if (found == map.end()) return TypedValue(ctx_->memory);
|
||||
// NOTE: Explicit move is needed, so that we return the move constructed
|
||||
// value and preserve the correct MemoryResource.
|
||||
return std::move(found->second);
|
||||
}
|
||||
|
||||
if (lhs.IsVertex()) {
|
||||
if (!index.IsString())
|
||||
throw ExpressionRuntimeException("Expected a string as a property name, got {}.", index.type());
|
||||
return TypedValue(GetProperty(lhs.ValueVertex(), index.ValueString()), ctx_->memory);
|
||||
}
|
||||
|
||||
if (lhs.IsEdge()) {
|
||||
if (!index.IsString())
|
||||
throw ExpressionRuntimeException("Expected a string as a property name, got {}.", index.type());
|
||||
return TypedValue(GetProperty(lhs.ValueEdge(), index.ValueString()), ctx_->memory);
|
||||
}
|
||||
|
||||
// lhs is Null
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(ListSlicingOperator &op) override {
|
||||
// If some type is null we can't return null, because throwing exception
|
||||
// on illegal type has higher priority.
|
||||
auto is_null = false;
|
||||
auto get_bound = [&](Expression *bound_expr, int64_t default_value) {
|
||||
if (bound_expr) {
|
||||
auto bound = bound_expr->Accept(*this);
|
||||
if (bound.type() == TypedValue::Type::Null) {
|
||||
is_null = true;
|
||||
} else if (bound.type() != TypedValue::Type::Int) {
|
||||
throw ExpressionRuntimeException("Expected an integer for a bound in list slicing, got {}.", bound.type());
|
||||
}
|
||||
return bound;
|
||||
}
|
||||
return TypedValue(default_value, ctx_->memory);
|
||||
};
|
||||
auto _upper_bound = get_bound(op.upper_bound_, std::numeric_limits<int64_t>::max());
|
||||
auto _lower_bound = get_bound(op.lower_bound_, 0);
|
||||
|
||||
auto _list = op.list_->Accept(*this);
|
||||
if (_list.type() == TypedValue::Type::Null) {
|
||||
is_null = true;
|
||||
} else if (_list.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("Expected a list to slice, got {}.", _list.type());
|
||||
}
|
||||
|
||||
if (is_null) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
const auto &list = _list.ValueList();
|
||||
auto normalise_bound = [&](int64_t bound) {
|
||||
if (bound < 0) {
|
||||
bound = static_cast<int64_t>(list.size()) + bound;
|
||||
}
|
||||
return std::max(static_cast<int64_t>(0), std::min(bound, static_cast<int64_t>(list.size())));
|
||||
};
|
||||
auto lower_bound = normalise_bound(_lower_bound.ValueInt());
|
||||
auto upper_bound = normalise_bound(_upper_bound.ValueInt());
|
||||
if (upper_bound <= lower_bound) {
|
||||
return TypedValue(typename TypedValue::TVector(ctx_->memory), ctx_->memory);
|
||||
}
|
||||
return TypedValue(
|
||||
typename TypedValue::TVector(list.begin() + lower_bound, list.begin() + upper_bound, ctx_->memory));
|
||||
}
|
||||
|
||||
TypedValue Visit(IsNullOperator &is_null) override {
|
||||
auto value = is_null.expression_->Accept(*this);
|
||||
return TypedValue(value.IsNull(), ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(PropertyLookup &property_lookup) override {
|
||||
auto expression_result = property_lookup.expression_->Accept(*this);
|
||||
auto maybe_date = [this](const auto &date, const auto &prop_name) -> std::optional<TypedValue> {
|
||||
if (prop_name == "year") {
|
||||
return TypedValue(date.year, ctx_->memory);
|
||||
}
|
||||
if (prop_name == "month") {
|
||||
return TypedValue(date.month, ctx_->memory);
|
||||
}
|
||||
if (prop_name == "day") {
|
||||
return TypedValue(date.day, ctx_->memory);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
auto maybe_local_time = [this](const auto <, const auto &prop_name) -> std::optional<TypedValue> {
|
||||
if (prop_name == "hour") {
|
||||
return TypedValue(lt.hour, ctx_->memory);
|
||||
}
|
||||
if (prop_name == "minute") {
|
||||
return TypedValue(lt.minute, ctx_->memory);
|
||||
}
|
||||
if (prop_name == "second") {
|
||||
return TypedValue(lt.second, ctx_->memory);
|
||||
}
|
||||
if (prop_name == "millisecond") {
|
||||
return TypedValue(lt.millisecond, ctx_->memory);
|
||||
}
|
||||
if (prop_name == "microsecond") {
|
||||
return TypedValue(lt.microsecond, ctx_->memory);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
auto maybe_duration = [this](const auto &dur, const auto &prop_name) -> std::optional<TypedValue> {
|
||||
if (prop_name == "day") {
|
||||
return TypedValue(dur.Days(), ctx_->memory);
|
||||
}
|
||||
if (prop_name == "hour") {
|
||||
return TypedValue(dur.SubDaysAsHours(), ctx_->memory);
|
||||
}
|
||||
if (prop_name == "minute") {
|
||||
return TypedValue(dur.SubDaysAsMinutes(), ctx_->memory);
|
||||
}
|
||||
if (prop_name == "second") {
|
||||
return TypedValue(dur.SubDaysAsSeconds(), ctx_->memory);
|
||||
}
|
||||
if (prop_name == "millisecond") {
|
||||
return TypedValue(dur.SubDaysAsMilliseconds(), ctx_->memory);
|
||||
}
|
||||
if (prop_name == "microsecond") {
|
||||
return TypedValue(dur.SubDaysAsMicroseconds(), ctx_->memory);
|
||||
}
|
||||
if (prop_name == "nanosecond") {
|
||||
return TypedValue(dur.SubDaysAsNanoseconds(), ctx_->memory);
|
||||
}
|
||||
return std::nullopt;
|
||||
};
|
||||
switch (expression_result.type()) {
|
||||
case TypedValue::Type::Null:
|
||||
return TypedValue(ctx_->memory);
|
||||
case TypedValue::Type::Vertex:
|
||||
return GetProperty(expression_result.ValueVertex(), property_lookup.property_);
|
||||
case TypedValue::Type::Edge:
|
||||
return GetProperty(expression_result.ValueEdge(), property_lookup.property_);
|
||||
case TypedValue::Type::Map: {
|
||||
// NOTE: Take non-const reference to map, so that we can move out the
|
||||
// looked-up element as the result.
|
||||
auto &map = expression_result.ValueMap();
|
||||
auto found = map.find(property_lookup.property_.name.c_str());
|
||||
if (found == map.end()) return TypedValue(ctx_->memory);
|
||||
// NOTE: Explicit move is needed, so that we return the move constructed
|
||||
// value and preserve the correct MemoryResource.
|
||||
return std::move(found->second);
|
||||
}
|
||||
case TypedValue::Type::Duration: {
|
||||
const auto &prop_name = property_lookup.property_.name;
|
||||
const auto &dur = expression_result.ValueDuration();
|
||||
if (auto dur_field = maybe_duration(dur, prop_name); dur_field) {
|
||||
return std::move(*dur_field);
|
||||
}
|
||||
throw ExpressionRuntimeException("Invalid property name {} for Duration", prop_name);
|
||||
}
|
||||
case TypedValue::Type::Date: {
|
||||
const auto &prop_name = property_lookup.property_.name;
|
||||
const auto &date = expression_result.ValueDate();
|
||||
if (auto date_field = maybe_date(date, prop_name); date_field) {
|
||||
return std::move(*date_field);
|
||||
}
|
||||
throw ExpressionRuntimeException("Invalid property name {} for Date", prop_name);
|
||||
}
|
||||
case TypedValue::Type::LocalTime: {
|
||||
const auto &prop_name = property_lookup.property_.name;
|
||||
const auto < = expression_result.ValueLocalTime();
|
||||
if (auto lt_field = maybe_local_time(lt, prop_name); lt_field) {
|
||||
return std::move(*lt_field);
|
||||
}
|
||||
throw ExpressionRuntimeException("Invalid property name {} for LocalTime", prop_name);
|
||||
}
|
||||
case TypedValue::Type::LocalDateTime: {
|
||||
const auto &prop_name = property_lookup.property_.name;
|
||||
const auto &ldt = expression_result.ValueLocalDateTime();
|
||||
if (auto date_field = maybe_date(ldt.date, prop_name); date_field) {
|
||||
return std::move(*date_field);
|
||||
}
|
||||
if (auto lt_field = maybe_local_time(ldt.local_time, prop_name); lt_field) {
|
||||
return std::move(*lt_field);
|
||||
}
|
||||
throw ExpressionRuntimeException("Invalid property name {} for LocalDateTime", prop_name);
|
||||
}
|
||||
default:
|
||||
throw ExpressionRuntimeException("Only nodes, edges, maps and temporal types have properties to be looked-up.");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename VertexAccessor, typename TTag = Tag,
|
||||
typename TReturnType = std::enable_if_t<std::is_same_v<TTag, StorageTag>, bool>>
|
||||
TReturnType HasLabelImpl(const VertexAccessor &vertex, const LabelIx &label, StorageTag /*tag*/) {
|
||||
auto has_label = vertex.HasLabel(view_, GetLabel(label));
|
||||
if (has_label.HasError() && has_label.GetError() == Error::NONEXISTENT_OBJECT) {
|
||||
// This is a very nasty and temporary hack in order to make MERGE
|
||||
// work. The old storage had the following logic when returning an
|
||||
// `OLD` view: `return old ? old : new`. That means that if the
|
||||
// `OLD` view didn't exist, it returned the NEW view. With this hack
|
||||
// we simulate that behavior.
|
||||
// TODO (mferencevic, teon.banek): Remove once MERGE is
|
||||
// reimplemented.
|
||||
has_label = vertex.HasLabel(StorageView::NEW, GetLabel(label));
|
||||
}
|
||||
if (has_label.HasError()) {
|
||||
switch (has_label.GetError()) {
|
||||
case Error::DELETED_OBJECT:
|
||||
throw ExpressionRuntimeException("Trying to access labels on a deleted node.");
|
||||
case Error::NONEXISTENT_OBJECT:
|
||||
throw ExpressionRuntimeException("Trying to access labels from a node that doesn't exist.");
|
||||
case Error::SERIALIZATION_ERROR:
|
||||
case Error::VERTEX_HAS_EDGES:
|
||||
case Error::PROPERTIES_DISABLED:
|
||||
throw ExpressionRuntimeException("Unexpected error when accessing labels.");
|
||||
}
|
||||
}
|
||||
return *has_label;
|
||||
}
|
||||
|
||||
template <typename VertexAccessor, typename TTag = Tag,
|
||||
typename TReturnType = std::enable_if_t<std::is_same_v<TTag, QueryEngineTag>, bool>>
|
||||
TReturnType HasLabelImpl(const VertexAccessor &vertex, const LabelIx &label_ix, QueryEngineTag /*tag*/) {
|
||||
auto label = typename VertexAccessor::Label{LabelId::FromUint(label_ix.ix)};
|
||||
auto has_label = vertex.HasLabel(label);
|
||||
return !has_label;
|
||||
}
|
||||
|
||||
TypedValue Visit(LabelsTest &labels_test) override {
|
||||
auto expression_result = labels_test.expression_->Accept(*this);
|
||||
switch (expression_result.type()) {
|
||||
case TypedValue::Type::Null:
|
||||
return TypedValue(ctx_->memory);
|
||||
case TypedValue::Type::Vertex: {
|
||||
const auto &vertex = expression_result.ValueVertex();
|
||||
if (std::ranges::all_of(labels_test.labels_, [&vertex, this](const auto label_test) {
|
||||
return this->HasLabelImpl(vertex, label_test, Tag{});
|
||||
})) {
|
||||
return TypedValue(true, ctx_->memory);
|
||||
}
|
||||
return TypedValue(false, ctx_->memory);
|
||||
}
|
||||
default:
|
||||
throw ExpressionRuntimeException("Only nodes have labels.");
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue Visit(PrimitiveLiteral &literal) override {
|
||||
// TODO: no need to evaluate constants, we can write it to frame in one
|
||||
// of the previous phases.
|
||||
return TypedValue(literal.value_, ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(ListLiteral &literal) override {
|
||||
typename TypedValue::TVector result(ctx_->memory);
|
||||
result.reserve(literal.elements_.size());
|
||||
for (const auto &expression : literal.elements_) result.emplace_back(expression->Accept(*this));
|
||||
return TypedValue(result, ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(MapLiteral &literal) override {
|
||||
typename TypedValue::TMap result(ctx_->memory);
|
||||
for (const auto &pair : literal.elements_) result.emplace(pair.first.name, pair.second->Accept(*this));
|
||||
return TypedValue(result, ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(Aggregation &aggregation) override {
|
||||
return TypedValue(frame_->at(symbol_table_->at(aggregation)), ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(Coalesce &coalesce) override {
|
||||
auto &exprs = coalesce.expressions_;
|
||||
|
||||
if (exprs.size() == 0) {
|
||||
throw ExpressionRuntimeException("'coalesce' requires at least one argument.");
|
||||
}
|
||||
|
||||
for (int64_t i = 0; i < exprs.size(); ++i) {
|
||||
TypedValue val(exprs[i]->Accept(*this), ctx_->memory);
|
||||
if (!val.IsNull()) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(Function &function) override {
|
||||
FunctionContext function_ctx{dba_, ctx_->memory, ctx_->timestamp, &ctx_->counters, view_};
|
||||
// Stack allocate evaluated arguments when there's a small number of them.
|
||||
if (function.arguments_.size() <= 8) {
|
||||
TypedValue arguments[8] = {TypedValue(ctx_->memory), TypedValue(ctx_->memory), TypedValue(ctx_->memory),
|
||||
TypedValue(ctx_->memory), TypedValue(ctx_->memory), TypedValue(ctx_->memory),
|
||||
TypedValue(ctx_->memory), TypedValue(ctx_->memory)};
|
||||
for (size_t i = 0; i < function.arguments_.size(); ++i) {
|
||||
arguments[i] = function.arguments_[i]->Accept(*this);
|
||||
}
|
||||
auto res = function.function_(arguments, function.arguments_.size(), function_ctx);
|
||||
MG_ASSERT(res.GetMemoryResource() == ctx_->memory);
|
||||
return res;
|
||||
} else {
|
||||
typename TypedValue::TVector arguments(ctx_->memory);
|
||||
arguments.reserve(function.arguments_.size());
|
||||
for (const auto &argument : function.arguments_) {
|
||||
arguments.emplace_back(argument->Accept(*this));
|
||||
}
|
||||
auto res = function.function_(arguments.data(), arguments.size(), function_ctx);
|
||||
MG_ASSERT(res.GetMemoryResource() == ctx_->memory);
|
||||
return res;
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue Visit(Reduce &reduce) override {
|
||||
auto list_value = reduce.list_->Accept(*this);
|
||||
if (list_value.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (list_value.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("REDUCE expected a list, got {}.", list_value.type());
|
||||
}
|
||||
const auto &list = list_value.ValueList();
|
||||
const auto &element_symbol = symbol_table_->at(*reduce.identifier_);
|
||||
const auto &accumulator_symbol = symbol_table_->at(*reduce.accumulator_);
|
||||
auto accumulator = reduce.initializer_->Accept(*this);
|
||||
for (const auto &element : list) {
|
||||
frame_->at(accumulator_symbol) = accumulator;
|
||||
frame_->at(element_symbol) = element;
|
||||
accumulator = reduce.expression_->Accept(*this);
|
||||
}
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
TypedValue Visit(Extract &extract) override {
|
||||
auto list_value = extract.list_->Accept(*this);
|
||||
if (list_value.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (list_value.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("EXTRACT expected a list, got {}.", list_value.type());
|
||||
}
|
||||
const auto &list = list_value.ValueList();
|
||||
const auto &element_symbol = symbol_table_->at(*extract.identifier_);
|
||||
typename TypedValue::TVector result(ctx_->memory);
|
||||
result.reserve(list.size());
|
||||
for (const auto &element : list) {
|
||||
if (element.IsNull()) {
|
||||
result.emplace_back();
|
||||
} else {
|
||||
frame_->at(element_symbol) = element;
|
||||
result.emplace_back(extract.expression_->Accept(*this));
|
||||
}
|
||||
}
|
||||
return TypedValue(result, ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(All &all) override {
|
||||
auto list_value = all.list_expression_->Accept(*this);
|
||||
if (list_value.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (list_value.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("ALL expected a list, got {}.", list_value.type());
|
||||
}
|
||||
const auto &list = list_value.ValueList();
|
||||
const auto &symbol = symbol_table_->at(*all.identifier_);
|
||||
bool has_null_elements = false;
|
||||
bool has_value = false;
|
||||
for (const auto &element : list) {
|
||||
frame_->at(symbol) = element;
|
||||
auto result = all.where_->expression_->Accept(*this);
|
||||
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
||||
throw ExpressionRuntimeException("Predicate of ALL must evaluate to boolean, got {}.", result.type());
|
||||
}
|
||||
if (!result.IsNull()) {
|
||||
has_value = true;
|
||||
if (!result.ValueBool()) {
|
||||
return TypedValue(false, ctx_->memory);
|
||||
}
|
||||
} else {
|
||||
has_null_elements = true;
|
||||
}
|
||||
}
|
||||
if (!has_value) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (has_null_elements) {
|
||||
return TypedValue(false, ctx_->memory);
|
||||
} else {
|
||||
return TypedValue(true, ctx_->memory);
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue Visit(Single &single) override {
|
||||
auto list_value = single.list_expression_->Accept(*this);
|
||||
if (list_value.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (list_value.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("SINGLE expected a list, got {}.", list_value.type());
|
||||
}
|
||||
const auto &list = list_value.ValueList();
|
||||
const auto &symbol = symbol_table_->at(*single.identifier_);
|
||||
bool has_value = false;
|
||||
bool predicate_satisfied = false;
|
||||
for (const auto &element : list) {
|
||||
frame_->at(symbol) = element;
|
||||
auto result = single.where_->expression_->Accept(*this);
|
||||
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
||||
throw ExpressionRuntimeException("Predicate of SINGLE must evaluate to boolean, got {}.", result.type());
|
||||
}
|
||||
if (result.type() == TypedValue::Type::Bool) {
|
||||
has_value = true;
|
||||
}
|
||||
if (result.IsNull() || !result.ValueBool()) {
|
||||
continue;
|
||||
}
|
||||
// Return false if more than one element satisfies the predicate.
|
||||
if (predicate_satisfied) {
|
||||
return TypedValue(false, ctx_->memory);
|
||||
} else {
|
||||
predicate_satisfied = true;
|
||||
}
|
||||
}
|
||||
if (!has_value) {
|
||||
return TypedValue(ctx_->memory);
|
||||
} else {
|
||||
return TypedValue(predicate_satisfied, ctx_->memory);
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue Visit(Any &any) override {
|
||||
auto list_value = any.list_expression_->Accept(*this);
|
||||
if (list_value.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (list_value.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("ANY expected a list, got {}.", list_value.type());
|
||||
}
|
||||
const auto &list = list_value.ValueList();
|
||||
const auto &symbol = symbol_table_->at(*any.identifier_);
|
||||
bool has_value = false;
|
||||
for (const auto &element : list) {
|
||||
frame_->at(symbol) = element;
|
||||
auto result = any.where_->expression_->Accept(*this);
|
||||
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
||||
throw ExpressionRuntimeException("Predicate of ANY must evaluate to boolean, got {}.", result.type());
|
||||
}
|
||||
if (!result.IsNull()) {
|
||||
has_value = true;
|
||||
if (result.ValueBool()) {
|
||||
return TypedValue(true, ctx_->memory);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return Null if all elements are Null
|
||||
if (!has_value) {
|
||||
return TypedValue(ctx_->memory);
|
||||
} else {
|
||||
return TypedValue(false, ctx_->memory);
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue Visit(None &none) override {
|
||||
auto list_value = none.list_expression_->Accept(*this);
|
||||
if (list_value.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (list_value.type() != TypedValue::Type::List) {
|
||||
throw ExpressionRuntimeException("NONE expected a list, got {}.", list_value.type());
|
||||
}
|
||||
const auto &list = list_value.ValueList();
|
||||
const auto &symbol = symbol_table_->at(*none.identifier_);
|
||||
bool has_value = false;
|
||||
for (const auto &element : list) {
|
||||
frame_->at(symbol) = element;
|
||||
auto result = none.where_->expression_->Accept(*this);
|
||||
if (!result.IsNull() && result.type() != TypedValue::Type::Bool) {
|
||||
throw ExpressionRuntimeException("Predicate of NONE must evaluate to boolean, got {}.", result.type());
|
||||
}
|
||||
if (!result.IsNull()) {
|
||||
has_value = true;
|
||||
if (result.ValueBool()) {
|
||||
return TypedValue(false, ctx_->memory);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Return Null if all elements are Null
|
||||
if (!has_value) {
|
||||
return TypedValue(ctx_->memory);
|
||||
} else {
|
||||
return TypedValue(true, ctx_->memory);
|
||||
}
|
||||
}
|
||||
|
||||
TypedValue Visit(ParameterLookup ¶m_lookup) override {
|
||||
return TypedValue(conv_(ctx_->parameters.AtTokenPosition(param_lookup.token_position_)), ctx_->memory);
|
||||
}
|
||||
|
||||
TypedValue Visit(RegexMatch ®ex_match) override {
|
||||
auto target_string_value = regex_match.string_expr_->Accept(*this);
|
||||
auto regex_value = regex_match.regex_->Accept(*this);
|
||||
if (target_string_value.IsNull() || regex_value.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
if (regex_value.type() != TypedValue::Type::String) {
|
||||
throw ExpressionRuntimeException("Regular expression must evaluate to a string, got {}.", regex_value.type());
|
||||
}
|
||||
if (target_string_value.type() != TypedValue::Type::String) {
|
||||
// Instead of error, we return Null which makes it compatible in case we
|
||||
// use indexed lookup which filters out any non-string properties.
|
||||
// Assuming a property lookup is the target_string_value.
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
const auto &target_string = target_string_value.ValueString();
|
||||
try {
|
||||
std::regex regex(regex_value.ValueString());
|
||||
return TypedValue(std::regex_match(target_string, regex), ctx_->memory);
|
||||
} catch (const std::regex_error &e) {
|
||||
throw ExpressionRuntimeException("Regex error in '{}': {}", regex_value.ValueString(), e.what());
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
template <class TRecordAccessor, class TTag = Tag,
|
||||
class TReturnType = std::enable_if_t<std::is_same_v<TTag, QueryEngineTag>, TypedValue>>
|
||||
TReturnType GetProperty(const TRecordAccessor &record_accessor, PropertyIx prop) {
|
||||
auto maybe_prop = record_accessor.GetProperty(prop.name);
|
||||
// Handler non existent property
|
||||
return conv_(maybe_prop);
|
||||
}
|
||||
|
||||
template <class TRecordAccessor, class TTag = Tag,
|
||||
class TReturnType = std::enable_if_t<std::is_same_v<TTag, QueryEngineTag>, TypedValue>>
|
||||
TReturnType GetProperty(const TRecordAccessor &record_accessor, const std::string_view name) {
|
||||
auto maybe_prop = record_accessor.GetProperty(std::string(name));
|
||||
// Handler non existent property
|
||||
return conv_(maybe_prop);
|
||||
}
|
||||
|
||||
template <class TRecordAccessor, class TTag = Tag,
|
||||
class TReturnType = std::enable_if_t<std::is_same_v<TTag, StorageTag>, TypedValue>>
|
||||
TypedValue GetProperty(const TRecordAccessor &record_accessor, PropertyIx prop) {
|
||||
auto maybe_prop = record_accessor.GetProperty(view_, ctx_->properties[prop.ix]);
|
||||
if (maybe_prop.HasError() && maybe_prop.GetError() == Error::NONEXISTENT_OBJECT) {
|
||||
// This is a very nasty and temporary hack in order to make MERGE work.
|
||||
// The old storage had the following logic when returning an `OLD` view:
|
||||
// `return old ? old : new`. That means that if the `OLD` view didn't
|
||||
// exist, it returned the NEW view. With this hack we simulate that
|
||||
// behavior.
|
||||
// TODO (mferencevic, teon.banek): Remove once MERGE is reimplemented.
|
||||
maybe_prop = record_accessor.GetProperty(StorageView::NEW, ctx_->properties[prop.ix]);
|
||||
}
|
||||
if (maybe_prop.HasError()) {
|
||||
switch (maybe_prop.GetError()) {
|
||||
case Error::DELETED_OBJECT:
|
||||
throw ExpressionRuntimeException("Trying to get a property from a deleted object.");
|
||||
case Error::NONEXISTENT_OBJECT:
|
||||
throw ExpressionRuntimeException("Trying to get a property from an object that doesn't exist.");
|
||||
case Error::SERIALIZATION_ERROR:
|
||||
case Error::VERTEX_HAS_EDGES:
|
||||
case Error::PROPERTIES_DISABLED:
|
||||
throw ExpressionRuntimeException("Unexpected error when getting a property.");
|
||||
}
|
||||
}
|
||||
return conv_(*maybe_prop, ctx_->memory);
|
||||
}
|
||||
|
||||
template <class TRecordAccessor, class TTag = Tag,
|
||||
class TReturnType = std::enable_if_t<std::is_same_v<TTag, StorageTag>, TypedValue>>
|
||||
TypedValue GetProperty(const TRecordAccessor &record_accessor, const std::string_view name) {
|
||||
auto maybe_prop = record_accessor.GetProperty(view_, dba_->NameToProperty(name));
|
||||
if (maybe_prop.HasError() && maybe_prop.GetError() == Error::NONEXISTENT_OBJECT) {
|
||||
// This is a very nasty and temporary hack in order to make MERGE work.
|
||||
// The old storage had the following logic when returning an `OLD` view:
|
||||
// `return old ? old : new`. That means that if the `OLD` view didn't
|
||||
// exist, it returned the NEW view. With this hack we simulate that
|
||||
// behavior.
|
||||
// TODO (mferencevic, teon.banek): Remove once MERGE is reimplemented.
|
||||
maybe_prop = record_accessor.GetProperty(view_, dba_->NameToProperty(name));
|
||||
}
|
||||
if (maybe_prop.HasError()) {
|
||||
switch (maybe_prop.GetError()) {
|
||||
case Error::DELETED_OBJECT:
|
||||
throw ExpressionRuntimeException("Trying to get a property from a deleted object.");
|
||||
case Error::NONEXISTENT_OBJECT:
|
||||
throw ExpressionRuntimeException("Trying to get a property from an object that doesn't exist.");
|
||||
case Error::SERIALIZATION_ERROR:
|
||||
case Error::VERTEX_HAS_EDGES:
|
||||
case Error::PROPERTIES_DISABLED:
|
||||
throw ExpressionRuntimeException("Unexpected error when getting a property.");
|
||||
}
|
||||
}
|
||||
return conv_(*maybe_prop, ctx_->memory);
|
||||
}
|
||||
|
||||
LabelId GetLabel(LabelIx label) { return ctx_->labels[label.ix]; }
|
||||
|
||||
Frame<TypedValue> *frame_;
|
||||
const SymbolTable *symbol_table_;
|
||||
const EvaluationContext *ctx_;
|
||||
DbAccessor *dba_;
|
||||
// which switching approach should be used when evaluating
|
||||
StorageView view_;
|
||||
ConvFunctor conv_;
|
||||
};
|
||||
|
||||
/// A helper function for evaluating an expression that's an int.
|
||||
///
|
||||
/// @param what - Name of what's getting evaluated. Used for user feedback (via
|
||||
/// exception) when the evaluated value is not an int.
|
||||
/// @throw ExpressionRuntimeException if expression doesn't evaluate to an int.
|
||||
template <typename ExpressionEvaluator>
|
||||
int64_t EvaluateInt(ExpressionEvaluator *evaluator, Expression *expr, const std::string &what) {
|
||||
TypedValue value = expr->Accept(*evaluator);
|
||||
try {
|
||||
return value.ValueInt();
|
||||
} catch (TypedValueException &e) {
|
||||
throw ExpressionRuntimeException(what + " must be an int");
|
||||
}
|
||||
}
|
||||
|
||||
template <typename ExpressionEvaluator>
|
||||
std::optional<size_t> EvaluateMemoryLimit(ExpressionEvaluator *eval, Expression *memory_limit, size_t memory_scale) {
|
||||
if (!memory_limit) return std::nullopt;
|
||||
auto limit_value = memory_limit->Accept(*eval);
|
||||
if (!limit_value.IsInt() || limit_value.ValueInt() <= 0)
|
||||
throw ExpressionRuntimeException("Memory limit must be a non-negative integer.");
|
||||
size_t limit = limit_value.ValueInt();
|
||||
if (std::numeric_limits<size_t>::max() / memory_scale < limit)
|
||||
throw ExpressionRuntimeException("Memory limit overflow.");
|
||||
return limit * memory_scale;
|
||||
}
|
||||
|
||||
} // namespace memgraph::expr
|
||||
45
src/expr/interpret/frame.hpp
Normal file
45
src/expr/interpret/frame.hpp
Normal file
@@ -0,0 +1,45 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "expr/semantic/symbol_table.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/memory.hpp"
|
||||
#include "utils/pmr/vector.hpp"
|
||||
|
||||
namespace memgraph::expr {
|
||||
|
||||
template <typename TypedValue>
|
||||
class Frame {
|
||||
public:
|
||||
/// Create a Frame of given size backed by a utils::NewDeleteResource()
|
||||
explicit Frame(int64_t size) : elems_(size, utils::NewDeleteResource()) { MG_ASSERT(size >= 0); }
|
||||
|
||||
Frame(int64_t size, utils::MemoryResource *memory) : elems_(size, memory) { MG_ASSERT(size >= 0); }
|
||||
|
||||
TypedValue &operator[](const Symbol &symbol) { return elems_[symbol.position()]; }
|
||||
const TypedValue &operator[](const Symbol &symbol) const { return elems_[symbol.position()]; }
|
||||
|
||||
TypedValue &at(const Symbol &symbol) { return elems_.at(symbol.position()); }
|
||||
const TypedValue &at(const Symbol &symbol) const { return elems_.at(symbol.position()); }
|
||||
|
||||
auto &elems() { return elems_; }
|
||||
|
||||
utils::MemoryResource *GetMemoryResource() const { return elems_.get_allocator().GetMemoryResource(); }
|
||||
|
||||
private:
|
||||
utils::pmr::vector<TypedValue> elems_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::expr
|
||||
184
src/expr/parsing.cpp
Normal file
184
src/expr/parsing.cpp
Normal file
@@ -0,0 +1,184 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "expr/parsing.hpp"
|
||||
|
||||
#include <cctype>
|
||||
#include <codecvt>
|
||||
#include <locale>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "expr/exceptions.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/string.hpp"
|
||||
|
||||
namespace memgraph::expr {
|
||||
|
||||
int64_t ParseIntegerLiteral(const std::string &s) {
|
||||
try {
|
||||
// Not really correct since long long can have a bigger range than int64_t.
|
||||
return static_cast<int64_t>(std::stoll(s, 0, 0));
|
||||
} catch (const std::out_of_range &) {
|
||||
throw SemanticException("Integer literal exceeds 64 bits.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string ParseStringLiteral(const std::string &s) {
|
||||
// These functions is declared as lambda since its semantics is highly
|
||||
// specific for this conxtext and shouldn't be used elsewhere.
|
||||
auto EncodeEscapedUnicodeCodepointUtf32 = [](const std::string &s, int &i) {
|
||||
const int kLongUnicodeLength = 8;
|
||||
int j = i + 1;
|
||||
while (j < static_cast<int>(s.size()) - 1 && j < i + kLongUnicodeLength + 1 && isxdigit(s[j])) {
|
||||
++j;
|
||||
}
|
||||
if (j - i == kLongUnicodeLength + 1) {
|
||||
char32_t t = stoi(s.substr(i + 1, kLongUnicodeLength), 0, 16);
|
||||
i += kLongUnicodeLength;
|
||||
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> converter;
|
||||
return converter.to_bytes(t);
|
||||
}
|
||||
throw SyntaxException(
|
||||
"Expected 8 hex digits as unicode codepoint started with \\U. "
|
||||
"Use \\u for 4 hex digits format.");
|
||||
};
|
||||
auto EncodeEscapedUnicodeCodepointUtf16 = [](const std::string &s, int &i) {
|
||||
const int kShortUnicodeLength = 4;
|
||||
int j = i + 1;
|
||||
while (j < static_cast<int>(s.size()) - 1 && j < i + kShortUnicodeLength + 1 && isxdigit(s[j])) {
|
||||
++j;
|
||||
}
|
||||
if (j - i >= kShortUnicodeLength + 1) {
|
||||
char16_t t = stoi(s.substr(i + 1, kShortUnicodeLength), 0, 16);
|
||||
if (t >= 0xD800 && t <= 0xDBFF) {
|
||||
// t is high surrogate pair. Expect one more utf16 codepoint.
|
||||
j = i + kShortUnicodeLength + 1;
|
||||
if (j >= static_cast<int>(s.size()) - 1 || s[j] != '\\') {
|
||||
throw SemanticException("Invalid UTF codepoint.");
|
||||
}
|
||||
++j;
|
||||
if (j >= static_cast<int>(s.size()) - 1 || (s[j] != 'u' && s[j] != 'U')) {
|
||||
throw SemanticException("Invalid UTF codepoint.");
|
||||
}
|
||||
++j;
|
||||
int k = j;
|
||||
while (k < static_cast<int>(s.size()) - 1 && k < j + kShortUnicodeLength && isxdigit(s[k])) {
|
||||
++k;
|
||||
}
|
||||
if (k != j + kShortUnicodeLength) {
|
||||
throw SemanticException("Invalid UTF codepoint.");
|
||||
}
|
||||
char16_t surrogates[3] = {t, static_cast<char16_t>(stoi(s.substr(j, kShortUnicodeLength), 0, 16)), 0};
|
||||
i += kShortUnicodeLength + 2 + kShortUnicodeLength;
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter;
|
||||
return converter.to_bytes(surrogates);
|
||||
} else {
|
||||
i += kShortUnicodeLength;
|
||||
std::wstring_convert<std::codecvt_utf8_utf16<char16_t>, char16_t> converter;
|
||||
return converter.to_bytes(t);
|
||||
}
|
||||
}
|
||||
throw SyntaxException(
|
||||
"Expected 4 hex digits as unicode codepoint started with \\u. "
|
||||
"Use \\U for 8 hex digits format.");
|
||||
};
|
||||
|
||||
std::string unescaped;
|
||||
bool escape = false;
|
||||
|
||||
// First and last char is quote, we don't need to look at them.
|
||||
for (int i = 1; i < static_cast<int>(s.size()) - 1; ++i) {
|
||||
if (escape) {
|
||||
switch (s[i]) {
|
||||
case '\\':
|
||||
unescaped += '\\';
|
||||
break;
|
||||
case '\'':
|
||||
unescaped += '\'';
|
||||
break;
|
||||
case '"':
|
||||
unescaped += '"';
|
||||
break;
|
||||
case 'B':
|
||||
case 'b':
|
||||
unescaped += '\b';
|
||||
break;
|
||||
case 'F':
|
||||
case 'f':
|
||||
unescaped += '\f';
|
||||
break;
|
||||
case 'N':
|
||||
case 'n':
|
||||
unescaped += '\n';
|
||||
break;
|
||||
case 'R':
|
||||
case 'r':
|
||||
unescaped += '\r';
|
||||
break;
|
||||
case 'T':
|
||||
case 't':
|
||||
unescaped += '\t';
|
||||
break;
|
||||
case 'U':
|
||||
try {
|
||||
unescaped += EncodeEscapedUnicodeCodepointUtf32(s, i);
|
||||
} catch (const std::range_error &) {
|
||||
throw SemanticException("Invalid UTF codepoint.");
|
||||
}
|
||||
break;
|
||||
case 'u':
|
||||
try {
|
||||
unescaped += EncodeEscapedUnicodeCodepointUtf16(s, i);
|
||||
} catch (const std::range_error &) {
|
||||
throw SemanticException("Invalid UTF codepoint.");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// This should never happen, except grammar changes and we don't
|
||||
// notice change in this production.
|
||||
DLOG_FATAL("can't happen");
|
||||
throw std::exception();
|
||||
}
|
||||
escape = false;
|
||||
} else if (s[i] == '\\') {
|
||||
escape = true;
|
||||
} else {
|
||||
unescaped += s[i];
|
||||
}
|
||||
}
|
||||
return unescaped;
|
||||
}
|
||||
|
||||
double ParseDoubleLiteral(const std::string &s) {
|
||||
try {
|
||||
return utils::ParseDouble(s);
|
||||
} catch (const utils::BasicException &) {
|
||||
throw SemanticException("Couldn't parse string to double.");
|
||||
}
|
||||
}
|
||||
|
||||
std::string ParseParameter(const std::string &s) {
|
||||
DMG_ASSERT(s[0] == '$', "Invalid string passed as parameter name");
|
||||
if (s[1] != '`') return s.substr(1);
|
||||
// If parameter name is escaped symbolic name then symbolic name should be
|
||||
// unescaped and leading and trailing backquote should be removed.
|
||||
DMG_ASSERT(s.size() > 3U && s.back() == '`', "Invalid string passed as parameter name");
|
||||
std::string out;
|
||||
for (int i = 2; i < static_cast<int>(s.size()) - 1; ++i) {
|
||||
if (s[i] == '`') {
|
||||
++i;
|
||||
}
|
||||
out.push_back(s[i]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
} // namespace memgraph::expr
|
||||
27
src/expr/parsing.hpp
Normal file
27
src/expr/parsing.hpp
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
/// @file
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace memgraph::expr {
|
||||
|
||||
// These are the functions for parsing literals and parameter names from
|
||||
// opencypher query.
|
||||
int64_t ParseIntegerLiteral(const std::string &s);
|
||||
std::string ParseStringLiteral(const std::string &s);
|
||||
double ParseDoubleLiteral(const std::string &s);
|
||||
std::string ParseParameter(const std::string &s);
|
||||
|
||||
} // namespace memgraph::expr
|
||||
87
src/expr/semantic/symbol.lcp
Normal file
87
src/expr/semantic/symbol.lcp
Normal file
@@ -0,0 +1,87 @@
|
||||
;; Copyright 2022 Memgraph Ltd.
|
||||
;;
|
||||
;; Use of this software is governed by the Business Source License
|
||||
;; included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
;; License, and you may not use this file except in compliance with the Business Source License.
|
||||
;;
|
||||
;; As of the Change Date specified in that file, in accordance with
|
||||
;; the Business Source License, use of this software will be governed
|
||||
;; by the Apache License, Version 2.0, included in the file
|
||||
;; licenses/APL.txt.
|
||||
|
||||
#>cpp
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "utils/typeinfo.hpp"
|
||||
cpp<#
|
||||
|
||||
(lcp:namespace memgraph)
|
||||
(lcp:namespace expr)
|
||||
|
||||
(lcp:define-class symbol ()
|
||||
((name "std::string" :scope :public)
|
||||
(position :int64_t :scope :public)
|
||||
(user-declared :bool :initval "true" :scope :public)
|
||||
(type "Type" :initval "Type::ANY" :scope :public)
|
||||
(token-position :int64_t :initval "-1" :scope :public))
|
||||
(:public
|
||||
;; This is similar to TypedValue::Type, but this has `Any` type.
|
||||
;; TODO: Make a better Type structure which can store a generic List.
|
||||
(lcp:define-enum type (any vertex edge path number edge-list)
|
||||
(:serialize))
|
||||
#>cpp
|
||||
// TODO: Generate enum to string conversion from LCP. Note, that this is
|
||||
// displayed to the end user, so we may want to have a pretty name of each
|
||||
// value.
|
||||
static std::string TypeToString(Type type) {
|
||||
const char *enum_string[] = {"Any", "Vertex", "Edge",
|
||||
"Path", "Number", "EdgeList"};
|
||||
return enum_string[static_cast<int>(type)];
|
||||
}
|
||||
|
||||
Symbol() {}
|
||||
Symbol(const std::string &name, int position, bool user_declared,
|
||||
Type type = Type::ANY, int token_position = -1)
|
||||
: name_(name),
|
||||
position_(position),
|
||||
user_declared_(user_declared),
|
||||
type_(type),
|
||||
token_position_(token_position) {}
|
||||
|
||||
bool operator==(const Symbol &other) const {
|
||||
return position_ == other.position_ && name_ == other.name_ &&
|
||||
type_ == other.type_;
|
||||
}
|
||||
bool operator!=(const Symbol &other) const { return !operator==(other); }
|
||||
|
||||
// TODO: Remove these since members are public
|
||||
const auto &name() const { return name_; }
|
||||
int position() const { return position_; }
|
||||
Type type() const { return type_; }
|
||||
bool user_declared() const { return user_declared_; }
|
||||
int token_position() const { return token_position_; }
|
||||
cpp<#)
|
||||
(:serialize (:slk)))
|
||||
|
||||
(lcp:pop-namespace) ;; expr
|
||||
(lcp:pop-namespace) ;; memgraph
|
||||
|
||||
#>cpp
|
||||
namespace std {
|
||||
|
||||
template <>
|
||||
struct hash<memgraph::expr::Symbol> {
|
||||
size_t operator()(const memgraph::expr::Symbol &symbol) const {
|
||||
size_t prime = 265443599u;
|
||||
size_t hash = std::hash<int>{}(symbol.position());
|
||||
hash ^= prime * std::hash<std::string>{}(symbol.name());
|
||||
hash ^= prime * std::hash<int>{}(static_cast<int>(symbol.type()));
|
||||
return hash;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace std
|
||||
|
||||
cpp<#
|
||||
712
src/expr/semantic/symbol_generator.hpp
Normal file
712
src/expr/semantic/symbol_generator.hpp
Normal file
@@ -0,0 +1,712 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
// Copyright 2017 Memgraph
|
||||
//
|
||||
// Created by Teon Banek on 11-03-2017
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <optional>
|
||||
#include <ranges>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include "expr/ast.hpp"
|
||||
#include "expr/ast/ast_visitor.hpp"
|
||||
#include "expr/exceptions.hpp"
|
||||
#include "expr/semantic/symbol_table.hpp"
|
||||
|
||||
namespace memgraph::expr {
|
||||
namespace detail {
|
||||
inline std::unordered_map<std::string, Identifier *> GeneratePredefinedIdentifierMap(
|
||||
const std::vector<Identifier *> &predefined_identifiers) {
|
||||
std::unordered_map<std::string, Identifier *> identifier_map;
|
||||
for (const auto &identifier : predefined_identifiers) {
|
||||
identifier_map.emplace(identifier->name_, identifier);
|
||||
}
|
||||
|
||||
return identifier_map;
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
/// Visits the AST and generates symbols for variables.
|
||||
///
|
||||
/// During the process of symbol generation, simple semantic checks are
|
||||
/// performed. Such as, redeclaring a variable or conflicting expectations of
|
||||
/// variable types.
|
||||
class SymbolGenerator : public HierarchicalTreeVisitor {
|
||||
public:
|
||||
explicit SymbolGenerator(SymbolTable *symbol_table, const std::vector<Identifier *> &predefined_identifiers)
|
||||
: symbol_table_(symbol_table),
|
||||
predefined_identifiers_{detail::GeneratePredefinedIdentifierMap(predefined_identifiers)},
|
||||
scopes_(1, Scope()) {}
|
||||
|
||||
using HierarchicalTreeVisitor::PostVisit;
|
||||
using HierarchicalTreeVisitor::PreVisit;
|
||||
using HierarchicalTreeVisitor::Visit;
|
||||
using typename HierarchicalTreeVisitor::ReturnType;
|
||||
|
||||
// Query
|
||||
bool PreVisit(SingleQuery & /*unused*/) override {
|
||||
prev_return_names_ = curr_return_names_;
|
||||
curr_return_names_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Union
|
||||
bool PreVisit(CypherUnion & /*unused*/) override {
|
||||
scopes_.back() = Scope();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(CypherUnion &cypher_union) override {
|
||||
if (prev_return_names_ != curr_return_names_) {
|
||||
throw SemanticException("All subqueries in an UNION must have the same column names.");
|
||||
}
|
||||
|
||||
// create new symbols for the result of the union
|
||||
for (const auto &name : curr_return_names_) {
|
||||
auto symbol = CreateSymbol(name, false);
|
||||
cypher_union.union_symbols_.push_back(symbol);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Clauses
|
||||
bool PreVisit(Create & /*unused*/) override {
|
||||
scopes_.back().in_create = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Create & /*unused*/) override {
|
||||
scopes_.back().in_create = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(CallProcedure &call_proc) override {
|
||||
for (auto *expr : call_proc.arguments_) {
|
||||
expr->Accept(*this);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PostVisit(CallProcedure &call_proc) override {
|
||||
for (auto *ident : call_proc.result_identifiers_) {
|
||||
if (HasSymbolLocalScope(ident->name_)) {
|
||||
throw RedeclareVariableError(ident->name_);
|
||||
}
|
||||
ident->MapTo(CreateSymbol(ident->name_, true));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(LoadCsv & /*unused*/) override { return false; }
|
||||
|
||||
bool PostVisit(LoadCsv &load_csv) override {
|
||||
if (HasSymbolLocalScope(load_csv.row_var_->name_)) {
|
||||
throw RedeclareVariableError(load_csv.row_var_->name_);
|
||||
}
|
||||
load_csv.row_var_->MapTo(CreateSymbol(load_csv.row_var_->name_, true));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(Return &ret) override {
|
||||
auto &scope = scopes_.back();
|
||||
scope.in_return = true;
|
||||
VisitReturnBody(ret.body_);
|
||||
scope.in_return = false;
|
||||
return false; // We handled the traversal ourselves.
|
||||
}
|
||||
|
||||
bool PostVisit(Return & /*unused*/) override {
|
||||
for (const auto &name_symbol : scopes_.back().symbols) curr_return_names_.insert(name_symbol.first);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(With &with) override {
|
||||
auto &scope = scopes_.back();
|
||||
scope.in_with = true;
|
||||
VisitReturnBody(with.body_, with.where_);
|
||||
scope.in_with = false;
|
||||
return false; // We handled the traversal ourselves.
|
||||
}
|
||||
|
||||
bool PreVisit(Where & /*unused*/) override {
|
||||
scopes_.back().in_where = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Where & /*unused*/) override {
|
||||
scopes_.back().in_where = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(Merge & /*unused*/) override {
|
||||
scopes_.back().in_merge = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Merge & /*unused*/) override {
|
||||
scopes_.back().in_merge = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Unwind &unwind) override {
|
||||
const auto &name = unwind.named_expression_->name_;
|
||||
if (HasSymbolLocalScope(name)) {
|
||||
throw RedeclareVariableError(name);
|
||||
}
|
||||
unwind.named_expression_->MapTo(CreateSymbol(name, true));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(Match & /*unused*/) override {
|
||||
scopes_.back().in_match = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Match & /*unused*/) override {
|
||||
auto &scope = scopes_.back();
|
||||
scope.in_match = false;
|
||||
// Check variables in property maps after visiting Match, so that they can
|
||||
// reference symbols out of bind order.
|
||||
for (auto &ident : scope.identifiers_in_match) {
|
||||
if (!HasSymbolLocalScope(ident->name_) && !ConsumePredefinedIdentifier(ident->name_))
|
||||
throw UnboundVariableError(ident->name_);
|
||||
ident->MapTo(scope.symbols[ident->name_]);
|
||||
}
|
||||
scope.identifiers_in_match.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(Foreach &for_each) override {
|
||||
const auto &name = for_each.named_expression_->name_;
|
||||
scopes_.emplace_back(Scope());
|
||||
scopes_.back().in_foreach = true;
|
||||
for_each.named_expression_->MapTo(
|
||||
CreateSymbol(name, true, Symbol::Type::ANY, for_each.named_expression_->token_position_));
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Foreach & /*unused*/) override {
|
||||
scopes_.pop_back();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Expressions
|
||||
ReturnType Visit(Identifier &ident) override {
|
||||
auto &scope = scopes_.back();
|
||||
if (scope.in_skip || scope.in_limit) {
|
||||
throw SemanticException("Variables are not allowed in {}.", scope.in_skip ? "SKIP" : "LIMIT");
|
||||
}
|
||||
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
|
||||
// identifier is the pattern name.
|
||||
symbol = GetOrCreateSymbolLocalScope(ident.name_, ident.user_declared_, Symbol::Type::PATH);
|
||||
} else if (scope.in_pattern && scope.in_pattern_atom_identifier) {
|
||||
// Patterns used to create nodes and edges cannot redeclare already
|
||||
// established bindings. Declaration only happens in single node
|
||||
// patterns and in edge patterns. OpenCypher example,
|
||||
// `MATCH (n) CREATE (n)` should throw an error that `n` is already
|
||||
// declared. While `MATCH (n) CREATE (n) -[:R]-> (n)` is allowed,
|
||||
// since `n` now references the bound node instead of declaring it.
|
||||
if ((scope.in_create_node || scope.in_create_edge) && HasSymbolLocalScope(ident.name_)) {
|
||||
throw RedeclareVariableError(ident.name_);
|
||||
}
|
||||
auto type = Symbol::Type::VERTEX;
|
||||
if (scope.visiting_edge) {
|
||||
// Edge referencing is not allowed (like in Neo4j):
|
||||
// `MATCH (n) - [r] -> (n) - [r] -> (n) RETURN r` is not allowed.
|
||||
if (HasSymbolLocalScope(ident.name_)) {
|
||||
throw RedeclareVariableError(ident.name_);
|
||||
}
|
||||
type = scope.visiting_edge->IsVariable() ? Symbol::Type::EDGE_LIST : Symbol::Type::EDGE;
|
||||
}
|
||||
symbol = GetOrCreateSymbolLocalScope(ident.name_, ident.user_declared_, type);
|
||||
} else if (scope.in_pattern && !scope.in_pattern_atom_identifier && scope.in_match) {
|
||||
if (scope.in_edge_range && scope.visiting_edge->identifier_->name_ == ident.name_) {
|
||||
// Prevent variable path bounds to reference the identifier which is bound
|
||||
// by the variable path itself.
|
||||
throw UnboundVariableError(ident.name_);
|
||||
}
|
||||
// Variables in property maps or bounds of variable length path during MATCH
|
||||
// can reference symbols bound later in the same MATCH. We collect them
|
||||
// here, so that they can be checked after visiting Match.
|
||||
scope.identifiers_in_match.emplace_back(&ident);
|
||||
} else {
|
||||
// Everything else references a bound symbol.
|
||||
if (!HasSymbol(ident.name_) && !ConsumePredefinedIdentifier(ident.name_)) throw UnboundVariableError(ident.name_);
|
||||
symbol = GetOrCreateSymbol(ident.name_, ident.user_declared_, Symbol::Type::ANY);
|
||||
}
|
||||
ident.MapTo(symbol);
|
||||
return true;
|
||||
}
|
||||
|
||||
ReturnType Visit(PrimitiveLiteral & /*unused*/) override { return true; }
|
||||
|
||||
ReturnType Visit(ParameterLookup & /*unused*/) override { return true; }
|
||||
|
||||
bool PreVisit(Aggregation &aggr) override {
|
||||
auto &scope = scopes_.back();
|
||||
// Check if the aggregation can be used in this context. This check should
|
||||
// probably move to a separate phase, which checks if the query is well
|
||||
// formed.
|
||||
if ((!scope.in_return && !scope.in_with) || scope.in_order_by || scope.in_skip || scope.in_limit ||
|
||||
scope.in_where) {
|
||||
throw SemanticException("Aggregation functions are only allowed in WITH and RETURN.");
|
||||
}
|
||||
if (scope.in_aggregation) {
|
||||
throw SemanticException(
|
||||
"Using aggregation functions inside aggregation functions is not "
|
||||
"allowed.");
|
||||
}
|
||||
if (scope.num_if_operators) {
|
||||
// Neo allows aggregations here and produces very interesting behaviors.
|
||||
// To simplify implementation at this moment we decided to completely
|
||||
// disallow aggregations inside of the CASE.
|
||||
// However, in some cases aggregation makes perfect sense, for example:
|
||||
// CASE count(n) WHEN 10 THEN "YES" ELSE "NO" END.
|
||||
// TODO: Rethink of allowing aggregations in some parts of the CASE
|
||||
// construct.
|
||||
throw SemanticException("Using aggregation functions inside of CASE is not allowed.");
|
||||
}
|
||||
// Create a virtual symbol for aggregation result.
|
||||
// Currently, we only have aggregation operators which return numbers.
|
||||
auto aggr_name = Aggregation::OpToString(aggr.op_) + std::to_string(aggr.symbol_pos_);
|
||||
aggr.MapTo(CreateSymbol(aggr_name, false, Symbol::Type::NUMBER));
|
||||
scope.in_aggregation = true;
|
||||
scope.has_aggregation = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Aggregation & /*unused*/) override {
|
||||
scopes_.back().in_aggregation = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(IfOperator & /*unused*/) override {
|
||||
++scopes_.back().num_if_operators;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(IfOperator & /*unused*/) override {
|
||||
--scopes_.back().num_if_operators;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(All &all) override {
|
||||
all.list_expression_->Accept(*this);
|
||||
VisitWithIdentifiers(all.where_->expression_, {all.identifier_});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreVisit(Single &single) override {
|
||||
single.list_expression_->Accept(*this);
|
||||
VisitWithIdentifiers(single.where_->expression_, {single.identifier_});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreVisit(Any &any) override {
|
||||
any.list_expression_->Accept(*this);
|
||||
VisitWithIdentifiers(any.where_->expression_, {any.identifier_});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreVisit(None &none) override {
|
||||
none.list_expression_->Accept(*this);
|
||||
VisitWithIdentifiers(none.where_->expression_, {none.identifier_});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreVisit(Reduce &reduce) override {
|
||||
reduce.initializer_->Accept(*this);
|
||||
reduce.list_->Accept(*this);
|
||||
VisitWithIdentifiers(reduce.expression_, {reduce.accumulator_, reduce.identifier_});
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PreVisit(Extract &extract) override {
|
||||
extract.list_->Accept(*this);
|
||||
VisitWithIdentifiers(extract.expression_, {extract.identifier_});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pattern and its subparts.
|
||||
bool PreVisit(Pattern &pattern) override {
|
||||
auto &scope = scopes_.back();
|
||||
scope.in_pattern = true;
|
||||
if ((scope.in_create || scope.in_merge) && pattern.atoms_.size() == 1U) {
|
||||
MG_ASSERT(utils::IsSubtype(*pattern.atoms_[0], NodeAtom::kType), "Expected a single NodeAtom in Pattern");
|
||||
scope.in_create_node = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PostVisit(Pattern & /*unused*/) override {
|
||||
auto &scope = scopes_.back();
|
||||
scope.in_pattern = false;
|
||||
scope.in_create_node = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(NodeAtom &node_atom) override {
|
||||
auto &scope = scopes_.back();
|
||||
auto check_node_semantic = [&node_atom, &scope, this](const bool props_or_labels) {
|
||||
const auto &node_name = node_atom.identifier_->name_;
|
||||
if ((scope.in_create || scope.in_merge) && props_or_labels && HasSymbolLocalScope(node_name)) {
|
||||
throw SemanticException("Cannot create node '" + node_name +
|
||||
"' with labels or properties, because it is already declared.");
|
||||
}
|
||||
scope.in_pattern_atom_identifier = true;
|
||||
node_atom.identifier_->Accept(*this);
|
||||
scope.in_pattern_atom_identifier = false;
|
||||
};
|
||||
|
||||
scope.in_node_atom = true;
|
||||
if (auto *properties = std::get_if<std::unordered_map<PropertyIx, Expression *>>(&node_atom.properties_)) {
|
||||
bool props_or_labels = !properties->empty() || !node_atom.labels_.empty();
|
||||
|
||||
check_node_semantic(props_or_labels);
|
||||
for (auto kv : *properties) {
|
||||
kv.second->Accept(*this);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
auto &properties_parameter = std::get<ParameterLookup *>(node_atom.properties_);
|
||||
bool props_or_labels = !properties_parameter || !node_atom.labels_.empty();
|
||||
|
||||
check_node_semantic(props_or_labels);
|
||||
properties_parameter->Accept(*this);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PostVisit(NodeAtom & /*unused*/) override {
|
||||
scopes_.back().in_node_atom = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PreVisit(EdgeAtom &edge_atom) override {
|
||||
auto &scope = scopes_.back();
|
||||
scope.visiting_edge = &edge_atom;
|
||||
if (scope.in_create || scope.in_merge) {
|
||||
scope.in_create_edge = true;
|
||||
if (edge_atom.edge_types_.size() != 1U) {
|
||||
throw SemanticException(
|
||||
"A single relationship type must be specified "
|
||||
"when creating an edge.");
|
||||
}
|
||||
if (scope.in_create && // Merge allows bidirectionality
|
||||
edge_atom.direction_ == EdgeAtom::Direction::BOTH) {
|
||||
throw SemanticException(
|
||||
"Bidirectional relationship are not supported "
|
||||
"when creating an edge");
|
||||
}
|
||||
if (edge_atom.IsVariable()) {
|
||||
throw SemanticException(
|
||||
"Variable length relationships are not supported when creating an "
|
||||
"edge.");
|
||||
}
|
||||
}
|
||||
if (auto *properties = std::get_if<std::unordered_map<PropertyIx, Expression *>>(&edge_atom.properties_)) {
|
||||
for (auto kv : *properties) {
|
||||
kv.second->Accept(*this);
|
||||
}
|
||||
} else {
|
||||
std::get<ParameterLookup *>(edge_atom.properties_)->Accept(*this);
|
||||
}
|
||||
if (edge_atom.IsVariable()) {
|
||||
scope.in_edge_range = true;
|
||||
if (edge_atom.lower_bound_) {
|
||||
edge_atom.lower_bound_->Accept(*this);
|
||||
}
|
||||
if (edge_atom.upper_bound_) {
|
||||
edge_atom.upper_bound_->Accept(*this);
|
||||
}
|
||||
scope.in_edge_range = false;
|
||||
scope.in_pattern = false;
|
||||
if (edge_atom.filter_lambda_.expression) {
|
||||
VisitWithIdentifiers(edge_atom.filter_lambda_.expression,
|
||||
{edge_atom.filter_lambda_.inner_edge, edge_atom.filter_lambda_.inner_node});
|
||||
} else {
|
||||
// Create inner symbols, but don't bind them in scope, since they are to
|
||||
// be used in the missing filter expression.
|
||||
auto *inner_edge = edge_atom.filter_lambda_.inner_edge;
|
||||
inner_edge->MapTo(
|
||||
symbol_table_->CreateSymbol(inner_edge->name_, inner_edge->user_declared_, Symbol::Type::EDGE));
|
||||
auto *inner_node = edge_atom.filter_lambda_.inner_node;
|
||||
inner_node->MapTo(
|
||||
symbol_table_->CreateSymbol(inner_node->name_, inner_node->user_declared_, Symbol::Type::VERTEX));
|
||||
}
|
||||
if (edge_atom.weight_lambda_.expression) {
|
||||
VisitWithIdentifiers(edge_atom.weight_lambda_.expression,
|
||||
{edge_atom.weight_lambda_.inner_edge, edge_atom.weight_lambda_.inner_node});
|
||||
}
|
||||
scope.in_pattern = true;
|
||||
}
|
||||
scope.in_pattern_atom_identifier = true;
|
||||
edge_atom.identifier_->Accept(*this);
|
||||
scope.in_pattern_atom_identifier = false;
|
||||
if (edge_atom.total_weight_) {
|
||||
if (HasSymbolLocalScope(edge_atom.total_weight_->name_)) {
|
||||
throw RedeclareVariableError(edge_atom.total_weight_->name_);
|
||||
}
|
||||
edge_atom.total_weight_->MapTo(GetOrCreateSymbolLocalScope(
|
||||
edge_atom.total_weight_->name_, edge_atom.total_weight_->user_declared_, Symbol::Type::NUMBER));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PostVisit(EdgeAtom & /*unused*/) override {
|
||||
auto &scope = scopes_.back();
|
||||
scope.visiting_edge = nullptr;
|
||||
scope.in_create_edge = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
// Scope stores the state of where we are when visiting the AST and a map of
|
||||
// names to symbols.
|
||||
struct Scope {
|
||||
bool in_pattern{false};
|
||||
bool in_merge{false};
|
||||
bool in_create{false};
|
||||
// in_create_node is true if we are creating or merging *only* a node.
|
||||
// Therefore, it is *not* equivalent to (in_create || in_merge) &&
|
||||
// in_node_atom.
|
||||
bool in_create_node{false};
|
||||
// True if creating an edge;
|
||||
// shortcut for (in_create || in_merge) && visiting_edge.
|
||||
bool in_create_edge{false};
|
||||
bool in_node_atom{false};
|
||||
EdgeAtom *visiting_edge{nullptr};
|
||||
bool in_aggregation{false};
|
||||
bool in_return{false};
|
||||
bool in_with{false};
|
||||
bool in_skip{false};
|
||||
bool in_limit{false};
|
||||
bool in_order_by{false};
|
||||
bool in_where{false};
|
||||
bool in_match{false};
|
||||
bool in_foreach{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};
|
||||
// True when visiting range bounds of a variable path.
|
||||
bool in_edge_range{false};
|
||||
// True if the return/with contains an aggregation in any named expression.
|
||||
bool has_aggregation{false};
|
||||
// Map from variable names to symbols.
|
||||
std::map<std::string, Symbol> symbols;
|
||||
// Identifiers found in property maps of patterns or as variable length path
|
||||
// bounds in a single Match clause. They need to be checked after visiting
|
||||
// Match. Identifiers created by naming vertices, edges and paths are *not*
|
||||
// stored in here.
|
||||
std::vector<Identifier *> identifiers_in_match;
|
||||
// Number of nested IfOperators.
|
||||
int num_if_operators{0};
|
||||
};
|
||||
|
||||
inline static std::optional<Symbol> FindSymbolInScope(const std::string &name, const Scope &scope,
|
||||
Symbol::Type type) {
|
||||
if (auto it = scope.symbols.find(name); it != scope.symbols.end()) {
|
||||
const auto &symbol = it->second;
|
||||
// Unless we have `ANY` type, check that types match.
|
||||
if (type != Symbol::Type::ANY && symbol.type() != Symbol::Type::ANY && type != symbol.type()) {
|
||||
throw TypeMismatchError(name, Symbol::TypeToString(symbol.type()), Symbol::TypeToString(type));
|
||||
}
|
||||
return symbol;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool HasSymbol(const std::string &name) const {
|
||||
return std::ranges::any_of(scopes_, [&name](const auto &scope) { return scope.symbols.contains(name); });
|
||||
}
|
||||
|
||||
bool HasSymbolLocalScope(const std::string &name) const { return scopes_.back().symbols.contains(name); }
|
||||
|
||||
// @return true if it added a predefined identifier with that name
|
||||
bool ConsumePredefinedIdentifier(const std::string &name) {
|
||||
auto it = predefined_identifiers_.find(name);
|
||||
|
||||
if (it == predefined_identifiers_.end()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// we can only use the predefined identifier in a single scope so we remove it after creating
|
||||
// a symbol for it
|
||||
auto &identifier = it->second;
|
||||
MG_ASSERT(!identifier->user_declared_, "Predefined symbols cannot be user declared!");
|
||||
identifier->MapTo(CreateSymbol(identifier->name_, identifier->user_declared_));
|
||||
predefined_identifiers_.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Returns a freshly generated symbol. Previous mapping of the same name to a
|
||||
// different symbol is replaced with the new one.
|
||||
Symbol CreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY,
|
||||
int token_position = -1) {
|
||||
auto symbol = symbol_table_->CreateSymbol(name, user_declared, type, token_position);
|
||||
scopes_.back().symbols[name] = symbol;
|
||||
return symbol;
|
||||
}
|
||||
|
||||
Symbol GetOrCreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY) {
|
||||
// NOLINTNEXTLINE
|
||||
for (auto scope = scopes_.rbegin(); scope != scopes_.rend(); ++scope) {
|
||||
if (auto maybe_symbol = FindSymbolInScope(name, *scope, type); maybe_symbol) {
|
||||
return *maybe_symbol;
|
||||
}
|
||||
}
|
||||
return CreateSymbol(name, user_declared, type);
|
||||
}
|
||||
|
||||
// Returns the symbol by name. If the mapping already exists, checks if the
|
||||
// types match. Otherwise, returns a new symbol.
|
||||
Symbol GetOrCreateSymbolLocalScope(const std::string &name, bool user_declared,
|
||||
Symbol::Type type = Symbol::Type::ANY) {
|
||||
auto &scope = scopes_.back();
|
||||
if (auto maybe_symbol = FindSymbolInScope(name, scope, type); maybe_symbol) {
|
||||
return *maybe_symbol;
|
||||
}
|
||||
return CreateSymbol(name, user_declared, type);
|
||||
}
|
||||
|
||||
void VisitReturnBody(ReturnBody &body, Where *where = nullptr) {
|
||||
auto &scope = scopes_.back();
|
||||
for (auto &expr : body.named_expressions) {
|
||||
expr->Accept(*this);
|
||||
}
|
||||
std::vector<Symbol> user_symbols;
|
||||
if (body.all_identifiers) {
|
||||
// Carry over user symbols because '*' appeared.
|
||||
for (const auto &sym_pair : scope.symbols) {
|
||||
if (!sym_pair.second.user_declared()) {
|
||||
continue;
|
||||
}
|
||||
user_symbols.emplace_back(sym_pair.second);
|
||||
}
|
||||
if (user_symbols.empty()) {
|
||||
throw SemanticException("There are no variables in scope to use for '*'.");
|
||||
}
|
||||
}
|
||||
// WITH/RETURN clause removes declarations of all the previous variables and
|
||||
// declares only those established through named expressions. New declarations
|
||||
// must not be visible inside named expressions themselves.
|
||||
bool removed_old_names = false;
|
||||
if ((!where && body.order_by.empty()) || scope.has_aggregation) {
|
||||
// WHERE and ORDER BY need to see both the old and new symbols, unless we
|
||||
// have an aggregation. Therefore, we can clear the symbols immediately if
|
||||
// there is neither ORDER BY nor WHERE, or we have an aggregation.
|
||||
scope.symbols.clear();
|
||||
removed_old_names = true;
|
||||
}
|
||||
// Create symbols for named expressions.
|
||||
std::unordered_set<std::string> new_names;
|
||||
for (const auto &user_sym : user_symbols) {
|
||||
new_names.insert(user_sym.name());
|
||||
scope.symbols[user_sym.name()] = user_sym;
|
||||
}
|
||||
for (auto &named_expr : body.named_expressions) {
|
||||
const auto &name = named_expr->name_;
|
||||
if (!new_names.insert(name).second) {
|
||||
throw SemanticException("Multiple results with the same name '{}' are not allowed.", name);
|
||||
}
|
||||
// An improvement would be to infer the type of the expression, so that the
|
||||
// new symbol would have a more specific type.
|
||||
named_expr->MapTo(CreateSymbol(name, true, Symbol::Type::ANY, named_expr->token_position_));
|
||||
}
|
||||
scope.in_order_by = true;
|
||||
for (const auto &order_pair : body.order_by) {
|
||||
order_pair.expression->Accept(*this);
|
||||
}
|
||||
scope.in_order_by = false;
|
||||
if (body.skip) {
|
||||
scope.in_skip = true;
|
||||
body.skip->Accept(*this);
|
||||
scope.in_skip = false;
|
||||
}
|
||||
if (body.limit) {
|
||||
scope.in_limit = true;
|
||||
body.limit->Accept(*this);
|
||||
scope.in_limit = false;
|
||||
}
|
||||
if (where) where->Accept(*this);
|
||||
if (!removed_old_names) {
|
||||
// We have an ORDER BY or WHERE, but no aggregation, which means we didn't
|
||||
// clear the old symbols, so do it now. We cannot just call clear, because
|
||||
// we've added new symbols.
|
||||
for (auto sym_it = scope.symbols.begin(); sym_it != scope.symbols.end();) {
|
||||
if (new_names.find(sym_it->first) == new_names.end()) {
|
||||
sym_it = scope.symbols.erase(sym_it);
|
||||
} else {
|
||||
sym_it++;
|
||||
}
|
||||
}
|
||||
}
|
||||
scopes_.back().has_aggregation = false;
|
||||
}
|
||||
|
||||
void VisitWithIdentifiers(Expression *expr, const std::vector<Identifier *> &identifiers) {
|
||||
auto &scope = scopes_.back();
|
||||
std::vector<std::pair<std::optional<Symbol>, Identifier *>> prev_symbols;
|
||||
// Collect previous symbols if they exist.
|
||||
for (const auto &identifier : identifiers) {
|
||||
std::optional<Symbol> prev_symbol;
|
||||
auto prev_symbol_it = scope.symbols.find(identifier->name_);
|
||||
if (prev_symbol_it != scope.symbols.end()) {
|
||||
prev_symbol = prev_symbol_it->second;
|
||||
}
|
||||
identifier->MapTo(CreateSymbol(identifier->name_, identifier->user_declared_));
|
||||
prev_symbols.emplace_back(prev_symbol, identifier);
|
||||
}
|
||||
// Visit the expression with the new symbols bound.
|
||||
expr->Accept(*this);
|
||||
// Restore back to previous symbols.
|
||||
for (const auto &prev : prev_symbols) {
|
||||
const auto &prev_symbol = prev.first;
|
||||
const auto &identifier = prev.second;
|
||||
if (prev_symbol) {
|
||||
scope.symbols[identifier->name_] = *prev_symbol;
|
||||
} else {
|
||||
scope.symbols.erase(identifier->name_);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SymbolTable *symbol_table_;
|
||||
|
||||
// Identifiers which are injected from outside the query. Each identifier
|
||||
// is mapped by its name.
|
||||
std::unordered_map<std::string, Identifier *> predefined_identifiers_;
|
||||
std::vector<Scope> scopes_;
|
||||
std::unordered_set<std::string> prev_return_names_;
|
||||
std::unordered_set<std::string> curr_return_names_;
|
||||
};
|
||||
|
||||
inline SymbolTable MakeSymbolTable(CypherQuery *query, const std::vector<Identifier *> &predefined_identifiers = {}) {
|
||||
SymbolTable symbol_table;
|
||||
SymbolGenerator symbol_generator(&symbol_table, predefined_identifiers);
|
||||
query->single_query_->Accept(symbol_generator);
|
||||
for (auto *cypher_union : query->cypher_unions_) {
|
||||
cypher_union->Accept(symbol_generator);
|
||||
}
|
||||
return symbol_table;
|
||||
}
|
||||
|
||||
} // namespace memgraph::expr
|
||||
64
src/expr/semantic/symbol_table.hpp
Normal file
64
src/expr/semantic/symbol_table.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
|
||||
#include "expr/ast.hpp"
|
||||
#include "expr/semantic/symbol.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
|
||||
namespace memgraph::expr {
|
||||
|
||||
class SymbolTable final {
|
||||
public:
|
||||
SymbolTable() {}
|
||||
const Symbol &CreateSymbol(const std::string &name, bool user_declared, Symbol::Type type = Symbol::Type::ANY,
|
||||
int32_t token_position = -1) {
|
||||
MG_ASSERT(table_.size() <= std::numeric_limits<int32_t>::max(),
|
||||
"SymbolTable size doesn't fit into 32-bit integer!");
|
||||
auto got = table_.emplace(position_, Symbol(name, position_, user_declared, type, token_position));
|
||||
MG_ASSERT(got.second, "Duplicate symbol ID!");
|
||||
position_++;
|
||||
return got.first->second;
|
||||
}
|
||||
|
||||
// TODO(buda): This is the same logic as in the cypher_main_visitor. During
|
||||
// parsing phase symbol table doesn't exist. Figure out a better solution.
|
||||
const Symbol &CreateAnonymousSymbol(Symbol::Type type = Symbol::Type::ANY) {
|
||||
int id = 1;
|
||||
while (true) {
|
||||
static const std::string &kAnonPrefix = "anon";
|
||||
std::string name_candidate = kAnonPrefix + std::to_string(id++);
|
||||
if (std::find_if(std::begin(table_), std::end(table_), [&name_candidate](const auto &item) -> bool {
|
||||
return item.second.name_ == name_candidate;
|
||||
}) == std::end(table_)) {
|
||||
return CreateSymbol(name_candidate, false, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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_); }
|
||||
|
||||
// TODO: Remove these since members are public
|
||||
int32_t max_position() const { return static_cast<int32_t>(table_.size()); }
|
||||
|
||||
const auto &table() const { return table_; }
|
||||
|
||||
int32_t position_{0};
|
||||
std::map<int32_t, Symbol> table_;
|
||||
};
|
||||
|
||||
} // namespace memgraph::expr
|
||||
1514
src/expr/typed_value.hpp
Normal file
1514
src/expr/typed_value.hpp
Normal file
File diff suppressed because it is too large
Load Diff
64
src/glue/v2/auth.cpp
Normal file
64
src/glue/v2/auth.cpp
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "glue/v2/auth.hpp"
|
||||
|
||||
namespace memgraph::glue::v2 {
|
||||
|
||||
auth::Permission PrivilegeToPermission(query::v2::AuthQuery::Privilege privilege) {
|
||||
switch (privilege) {
|
||||
case query::v2::AuthQuery::Privilege::MATCH:
|
||||
return auth::Permission::MATCH;
|
||||
case query::v2::AuthQuery::Privilege::CREATE:
|
||||
return auth::Permission::CREATE;
|
||||
case query::v2::AuthQuery::Privilege::MERGE:
|
||||
return auth::Permission::MERGE;
|
||||
case query::v2::AuthQuery::Privilege::DELETE:
|
||||
return auth::Permission::DELETE;
|
||||
case query::v2::AuthQuery::Privilege::SET:
|
||||
return auth::Permission::SET;
|
||||
case query::v2::AuthQuery::Privilege::REMOVE:
|
||||
return auth::Permission::REMOVE;
|
||||
case query::v2::AuthQuery::Privilege::INDEX:
|
||||
return auth::Permission::INDEX;
|
||||
case query::v2::AuthQuery::Privilege::STATS:
|
||||
return auth::Permission::STATS;
|
||||
case query::v2::AuthQuery::Privilege::CONSTRAINT:
|
||||
return auth::Permission::CONSTRAINT;
|
||||
case query::v2::AuthQuery::Privilege::DUMP:
|
||||
return auth::Permission::DUMP;
|
||||
case query::v2::AuthQuery::Privilege::REPLICATION:
|
||||
return auth::Permission::REPLICATION;
|
||||
case query::v2::AuthQuery::Privilege::DURABILITY:
|
||||
return auth::Permission::DURABILITY;
|
||||
case query::v2::AuthQuery::Privilege::READ_FILE:
|
||||
return auth::Permission::READ_FILE;
|
||||
case query::v2::AuthQuery::Privilege::FREE_MEMORY:
|
||||
return auth::Permission::FREE_MEMORY;
|
||||
case query::v2::AuthQuery::Privilege::TRIGGER:
|
||||
return auth::Permission::TRIGGER;
|
||||
case query::v2::AuthQuery::Privilege::CONFIG:
|
||||
return auth::Permission::CONFIG;
|
||||
case query::v2::AuthQuery::Privilege::AUTH:
|
||||
return auth::Permission::AUTH;
|
||||
case query::v2::AuthQuery::Privilege::STREAM:
|
||||
return auth::Permission::STREAM;
|
||||
case query::v2::AuthQuery::Privilege::MODULE_READ:
|
||||
return auth::Permission::MODULE_READ;
|
||||
case query::v2::AuthQuery::Privilege::MODULE_WRITE:
|
||||
return auth::Permission::MODULE_WRITE;
|
||||
case query::v2::AuthQuery::Privilege::WEBSOCKET:
|
||||
return auth::Permission::WEBSOCKET;
|
||||
case query::v2::AuthQuery::Privilege::SCHEMA:
|
||||
return auth::Permission::SCHEMA;
|
||||
}
|
||||
}
|
||||
} // namespace memgraph::glue::v2
|
||||
23
src/glue/v2/auth.hpp
Normal file
23
src/glue/v2/auth.hpp
Normal file
@@ -0,0 +1,23 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "auth/models.hpp"
|
||||
#include "query/v2/frontend/ast/ast.hpp"
|
||||
|
||||
namespace memgraph::glue::v2 {
|
||||
|
||||
/**
|
||||
* This function converts query::AuthQuery::Privilege to its corresponding
|
||||
* auth::Permission.
|
||||
*/
|
||||
auth::Permission PrivilegeToPermission(query::v2::AuthQuery::Privilege privilege);
|
||||
|
||||
} // namespace memgraph::glue::v2
|
||||
277
src/glue/v2/communication.cpp
Normal file
277
src/glue/v2/communication.cpp
Normal file
@@ -0,0 +1,277 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "glue/v2/communication.hpp"
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "storage/v3/edge_accessor.hpp"
|
||||
#include "storage/v3/shard.hpp"
|
||||
#include "storage/v3/vertex_accessor.hpp"
|
||||
#include "utils/temporal.hpp"
|
||||
|
||||
using memgraph::communication::bolt::Value;
|
||||
|
||||
namespace memgraph::glue::v2 {
|
||||
|
||||
query::v2::TypedValue ToTypedValue(const Value &value) {
|
||||
switch (value.type()) {
|
||||
case Value::Type::Null:
|
||||
return {};
|
||||
case Value::Type::Bool:
|
||||
return query::v2::TypedValue(value.ValueBool());
|
||||
case Value::Type::Int:
|
||||
return query::v2::TypedValue(value.ValueInt());
|
||||
case Value::Type::Double:
|
||||
return query::v2::TypedValue(value.ValueDouble());
|
||||
case Value::Type::String:
|
||||
return query::v2::TypedValue(value.ValueString());
|
||||
case Value::Type::List: {
|
||||
std::vector<query::v2::TypedValue> list;
|
||||
list.reserve(value.ValueList().size());
|
||||
for (const auto &v : value.ValueList()) list.push_back(ToTypedValue(v));
|
||||
return query::v2::TypedValue(std::move(list));
|
||||
}
|
||||
case Value::Type::Map: {
|
||||
std::map<std::string, query::v2::TypedValue> map;
|
||||
for (const auto &kv : value.ValueMap()) map.emplace(kv.first, ToTypedValue(kv.second));
|
||||
return query::v2::TypedValue(std::move(map));
|
||||
}
|
||||
case Value::Type::Vertex:
|
||||
case Value::Type::Edge:
|
||||
case Value::Type::UnboundedEdge:
|
||||
case Value::Type::Path:
|
||||
throw communication::bolt::ValueException("Unsupported conversion from Value to TypedValue");
|
||||
case Value::Type::Date:
|
||||
return query::v2::TypedValue(value.ValueDate());
|
||||
case Value::Type::LocalTime:
|
||||
return query::v2::TypedValue(value.ValueLocalTime());
|
||||
case Value::Type::LocalDateTime:
|
||||
return query::v2::TypedValue(value.ValueLocalDateTime());
|
||||
case Value::Type::Duration:
|
||||
return query::v2::TypedValue(value.ValueDuration());
|
||||
}
|
||||
}
|
||||
|
||||
storage::v3::Result<communication::bolt::Vertex> ToBoltVertex(const query::v2::VertexAccessor &vertex,
|
||||
const storage::v3::Shard &db, storage::v3::View view) {
|
||||
return ToBoltVertex(vertex.impl_, db, view);
|
||||
}
|
||||
|
||||
storage::v3::Result<communication::bolt::Edge> ToBoltEdge(const query::v2::EdgeAccessor &edge,
|
||||
const storage::v3::Shard &db, storage::v3::View view) {
|
||||
return ToBoltEdge(edge.impl_, db, view);
|
||||
}
|
||||
|
||||
storage::v3::Result<Value> ToBoltValue(const query::v2::TypedValue &value, const storage::v3::Shard &db,
|
||||
storage::v3::View view) {
|
||||
switch (value.type()) {
|
||||
case query::v2::TypedValue::Type::Null:
|
||||
return Value();
|
||||
case query::v2::TypedValue::Type::Bool:
|
||||
return Value(value.ValueBool());
|
||||
case query::v2::TypedValue::Type::Int:
|
||||
return Value(value.ValueInt());
|
||||
case query::v2::TypedValue::Type::Double:
|
||||
return Value(value.ValueDouble());
|
||||
case query::v2::TypedValue::Type::String:
|
||||
return Value(std::string(value.ValueString()));
|
||||
case query::v2::TypedValue::Type::List: {
|
||||
std::vector<Value> values;
|
||||
values.reserve(value.ValueList().size());
|
||||
for (const auto &v : value.ValueList()) {
|
||||
auto maybe_value = ToBoltValue(v, db, view);
|
||||
if (maybe_value.HasError()) return maybe_value.GetError();
|
||||
values.emplace_back(std::move(*maybe_value));
|
||||
}
|
||||
return Value(std::move(values));
|
||||
}
|
||||
case query::v2::TypedValue::Type::Map: {
|
||||
std::map<std::string, Value> map;
|
||||
for (const auto &kv : value.ValueMap()) {
|
||||
auto maybe_value = ToBoltValue(kv.second, db, view);
|
||||
if (maybe_value.HasError()) return maybe_value.GetError();
|
||||
map.emplace(kv.first, std::move(*maybe_value));
|
||||
}
|
||||
return Value(std::move(map));
|
||||
}
|
||||
case query::v2::TypedValue::Type::Vertex: {
|
||||
auto maybe_vertex = ToBoltVertex(value.ValueVertex(), db, view);
|
||||
if (maybe_vertex.HasError()) return maybe_vertex.GetError();
|
||||
return Value(std::move(*maybe_vertex));
|
||||
}
|
||||
case query::v2::TypedValue::Type::Edge: {
|
||||
auto maybe_edge = ToBoltEdge(value.ValueEdge(), db, view);
|
||||
if (maybe_edge.HasError()) return maybe_edge.GetError();
|
||||
return Value(std::move(*maybe_edge));
|
||||
}
|
||||
case query::v2::TypedValue::Type::Path: {
|
||||
auto maybe_path = ToBoltPath(value.ValuePath(), db, view);
|
||||
if (maybe_path.HasError()) return maybe_path.GetError();
|
||||
return Value(std::move(*maybe_path));
|
||||
}
|
||||
case query::v2::TypedValue::Type::Date:
|
||||
return Value(value.ValueDate());
|
||||
case query::v2::TypedValue::Type::LocalTime:
|
||||
return Value(value.ValueLocalTime());
|
||||
case query::v2::TypedValue::Type::LocalDateTime:
|
||||
return Value(value.ValueLocalDateTime());
|
||||
case query::v2::TypedValue::Type::Duration:
|
||||
return Value(value.ValueDuration());
|
||||
}
|
||||
}
|
||||
|
||||
storage::v3::Result<communication::bolt::Vertex> ToBoltVertex(const storage::v3::VertexAccessor &vertex,
|
||||
const storage::v3::Shard &db, storage::v3::View view) {
|
||||
// TODO(jbajic) Fix bolt communication
|
||||
auto id = communication::bolt::Id::FromUint(0);
|
||||
auto maybe_labels = vertex.Labels(view);
|
||||
if (maybe_labels.HasError()) return maybe_labels.GetError();
|
||||
std::vector<std::string> labels;
|
||||
labels.reserve(maybe_labels->size());
|
||||
for (const auto &label : *maybe_labels) {
|
||||
labels.push_back(db.LabelToName(label));
|
||||
}
|
||||
auto maybe_properties = vertex.Properties(view);
|
||||
if (maybe_properties.HasError()) return maybe_properties.GetError();
|
||||
std::map<std::string, Value> properties;
|
||||
for (const auto &prop : *maybe_properties) {
|
||||
properties[db.PropertyToName(prop.first)] = ToBoltValue(prop.second);
|
||||
}
|
||||
return communication::bolt::Vertex{id, labels, properties};
|
||||
}
|
||||
|
||||
storage::v3::Result<communication::bolt::Edge> ToBoltEdge(const storage::v3::EdgeAccessor &edge,
|
||||
const storage::v3::Shard &db, storage::v3::View view) {
|
||||
// TODO(jbajic) Fix bolt communication
|
||||
auto id = communication::bolt::Id::FromUint(0);
|
||||
auto from = communication::bolt::Id::FromUint(0);
|
||||
auto to = communication::bolt::Id::FromUint(0);
|
||||
const auto &type = db.EdgeTypeToName(edge.EdgeType());
|
||||
auto maybe_properties = edge.Properties(view);
|
||||
if (maybe_properties.HasError()) return maybe_properties.GetError();
|
||||
std::map<std::string, Value> properties;
|
||||
for (const auto &prop : *maybe_properties) {
|
||||
properties[db.PropertyToName(prop.first)] = ToBoltValue(prop.second);
|
||||
}
|
||||
return communication::bolt::Edge{id, from, to, type, properties};
|
||||
}
|
||||
|
||||
storage::v3::Result<communication::bolt::Path> ToBoltPath(const query::v2::Path &path, const storage::v3::Shard &db,
|
||||
storage::v3::View view) {
|
||||
std::vector<communication::bolt::Vertex> vertices;
|
||||
vertices.reserve(path.vertices().size());
|
||||
for (const auto &v : path.vertices()) {
|
||||
auto maybe_vertex = ToBoltVertex(v, db, view);
|
||||
if (maybe_vertex.HasError()) return maybe_vertex.GetError();
|
||||
vertices.emplace_back(std::move(*maybe_vertex));
|
||||
}
|
||||
std::vector<communication::bolt::Edge> edges;
|
||||
edges.reserve(path.edges().size());
|
||||
for (const auto &e : path.edges()) {
|
||||
auto maybe_edge = ToBoltEdge(e, db, view);
|
||||
if (maybe_edge.HasError()) return maybe_edge.GetError();
|
||||
edges.emplace_back(std::move(*maybe_edge));
|
||||
}
|
||||
return communication::bolt::Path(vertices, edges);
|
||||
}
|
||||
|
||||
storage::v3::PropertyValue ToPropertyValue(const Value &value) {
|
||||
switch (value.type()) {
|
||||
case Value::Type::Null:
|
||||
return {};
|
||||
case Value::Type::Bool:
|
||||
return storage::v3::PropertyValue(value.ValueBool());
|
||||
case Value::Type::Int:
|
||||
return storage::v3::PropertyValue(value.ValueInt());
|
||||
case Value::Type::Double:
|
||||
return storage::v3::PropertyValue(value.ValueDouble());
|
||||
case Value::Type::String:
|
||||
return storage::v3::PropertyValue(value.ValueString());
|
||||
case Value::Type::List: {
|
||||
std::vector<storage::v3::PropertyValue> vec;
|
||||
vec.reserve(value.ValueList().size());
|
||||
for (const auto &value : value.ValueList()) vec.emplace_back(ToPropertyValue(value));
|
||||
return storage::v3::PropertyValue(std::move(vec));
|
||||
}
|
||||
case Value::Type::Map: {
|
||||
std::map<std::string, storage::v3::PropertyValue> map;
|
||||
for (const auto &kv : value.ValueMap()) map.emplace(kv.first, ToPropertyValue(kv.second));
|
||||
return storage::v3::PropertyValue(std::move(map));
|
||||
}
|
||||
case Value::Type::Vertex:
|
||||
case Value::Type::Edge:
|
||||
case Value::Type::UnboundedEdge:
|
||||
case Value::Type::Path:
|
||||
throw communication::bolt::ValueException("Unsupported conversion from Value to PropertyValue");
|
||||
case Value::Type::Date:
|
||||
return storage::v3::PropertyValue(
|
||||
storage::v3::TemporalData(storage::v3::TemporalType::Date, value.ValueDate().MicrosecondsSinceEpoch()));
|
||||
case Value::Type::LocalTime:
|
||||
return storage::v3::PropertyValue(storage::v3::TemporalData(storage::v3::TemporalType::LocalTime,
|
||||
value.ValueLocalTime().MicrosecondsSinceEpoch()));
|
||||
case Value::Type::LocalDateTime:
|
||||
return storage::v3::PropertyValue(storage::v3::TemporalData(storage::v3::TemporalType::LocalDateTime,
|
||||
value.ValueLocalDateTime().MicrosecondsSinceEpoch()));
|
||||
case Value::Type::Duration:
|
||||
return storage::v3::PropertyValue(
|
||||
storage::v3::TemporalData(storage::v3::TemporalType::Duration, value.ValueDuration().microseconds));
|
||||
}
|
||||
}
|
||||
|
||||
Value ToBoltValue(const storage::v3::PropertyValue &value) {
|
||||
switch (value.type()) {
|
||||
case storage::v3::PropertyValue::Type::Null:
|
||||
return {};
|
||||
case storage::v3::PropertyValue::Type::Bool:
|
||||
return {value.ValueBool()};
|
||||
case storage::v3::PropertyValue::Type::Int:
|
||||
return {value.ValueInt()};
|
||||
break;
|
||||
case storage::v3::PropertyValue::Type::Double:
|
||||
return {value.ValueDouble()};
|
||||
case storage::v3::PropertyValue::Type::String:
|
||||
return {value.ValueString()};
|
||||
case storage::v3::PropertyValue::Type::List: {
|
||||
const auto &values = value.ValueList();
|
||||
std::vector<Value> vec;
|
||||
vec.reserve(values.size());
|
||||
for (const auto &v : values) {
|
||||
vec.push_back(ToBoltValue(v));
|
||||
}
|
||||
return {std::move(vec)};
|
||||
}
|
||||
case storage::v3::PropertyValue::Type::Map: {
|
||||
const auto &map = value.ValueMap();
|
||||
std::map<std::string, Value> dv_map;
|
||||
for (const auto &kv : map) {
|
||||
dv_map.emplace(kv.first, ToBoltValue(kv.second));
|
||||
}
|
||||
return {std::move(dv_map)};
|
||||
}
|
||||
case storage::v3::PropertyValue::Type::TemporalData:
|
||||
const auto &type = value.ValueTemporalData();
|
||||
switch (type.type) {
|
||||
case storage::v3::TemporalType::Date:
|
||||
return {utils::Date(type.microseconds)};
|
||||
case storage::v3::TemporalType::LocalTime:
|
||||
return {utils::LocalTime(type.microseconds)};
|
||||
case storage::v3::TemporalType::LocalDateTime:
|
||||
return {utils::LocalDateTime(type.microseconds)};
|
||||
case storage::v3::TemporalType::Duration:
|
||||
return {utils::Duration(type.microseconds)};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace memgraph::glue::v2
|
||||
68
src/glue/v2/communication.hpp
Normal file
68
src/glue/v2/communication.hpp
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
/// @file Conversion functions between Value and other memgraph types.
|
||||
#pragma once
|
||||
|
||||
#include "communication/bolt/v1/value.hpp"
|
||||
#include "query/v2/bindings/typed_value.hpp"
|
||||
#include "storage/v3/property_value.hpp"
|
||||
#include "storage/v3/result.hpp"
|
||||
#include "storage/v3/view.hpp"
|
||||
|
||||
namespace memgraph::storage::v3 {
|
||||
class EdgeAccessor;
|
||||
class Storage;
|
||||
class VertexAccessor;
|
||||
} // namespace memgraph::storage::v3
|
||||
|
||||
namespace memgraph::glue::v2 {
|
||||
|
||||
/// @param storage::v3::VertexAccessor for converting to
|
||||
/// communication::bolt::Vertex.
|
||||
/// @param storage::v3::Shard for getting label and property names.
|
||||
/// @param storage::v3::View for deciding which vertex attributes are visible.
|
||||
///
|
||||
/// @throw std::bad_alloc
|
||||
storage::v3::Result<communication::bolt::Vertex> ToBoltVertex(const storage::v3::VertexAccessor &vertex,
|
||||
const storage::v3::Shard &db, storage::v3::View view);
|
||||
|
||||
/// @param storage::v3::EdgeAccessor for converting to communication::bolt::Edge.
|
||||
/// @param storage::v3::Shard for getting edge type and property names.
|
||||
/// @param storage::v3::View for deciding which edge attributes are visible.
|
||||
///
|
||||
/// @throw std::bad_alloc
|
||||
storage::v3::Result<communication::bolt::Edge> ToBoltEdge(const storage::v3::EdgeAccessor &edge,
|
||||
const storage::v3::Shard &db, storage::v3::View view);
|
||||
|
||||
/// @param query::v2::Path for converting to communication::bolt::Path.
|
||||
/// @param storage::v3::Shard for ToBoltVertex and ToBoltEdge.
|
||||
/// @param storage::v3::View for ToBoltVertex and ToBoltEdge.
|
||||
///
|
||||
/// @throw std::bad_alloc
|
||||
storage::v3::Result<communication::bolt::Path> ToBoltPath(const query::v2::Path &path, const storage::v3::Shard &db,
|
||||
storage::v3::View view);
|
||||
|
||||
/// @param query::v2::TypedValue for converting to communication::bolt::Value.
|
||||
/// @param storage::v3::Shard for ToBoltVertex and ToBoltEdge.
|
||||
/// @param storage::v3::View for ToBoltVertex and ToBoltEdge.
|
||||
///
|
||||
/// @throw std::bad_alloc
|
||||
storage::v3::Result<communication::bolt::Value> ToBoltValue(const query::v2::TypedValue &value,
|
||||
const storage::v3::Shard &db, storage::v3::View view);
|
||||
|
||||
query::v2::TypedValue ToTypedValue(const communication::bolt::Value &value);
|
||||
|
||||
communication::bolt::Value ToBoltValue(const storage::v3::PropertyValue &value);
|
||||
|
||||
storage::v3::PropertyValue ToPropertyValue(const communication::bolt::Value &value);
|
||||
|
||||
} // namespace memgraph::glue::v2
|
||||
@@ -17,6 +17,7 @@
|
||||
namespace memgraph::integrations {
|
||||
|
||||
inline constexpr int64_t kDefaultCheckBatchLimit{1};
|
||||
inline constexpr int64_t kMinimumStartBatchLimit{1};
|
||||
inline constexpr std::chrono::milliseconds kDefaultCheckTimeout{30000};
|
||||
inline constexpr std::chrono::milliseconds kMinimumInterval{1};
|
||||
inline constexpr int64_t kMinimumSize{1};
|
||||
|
||||
@@ -74,6 +74,36 @@ utils::BasicResult<std::string, std::vector<Message>> GetBatch(RdKafka::KafkaCon
|
||||
|
||||
return std::move(batch);
|
||||
}
|
||||
|
||||
void CheckAndDestroyLastAssignmentIfNeeded(RdKafka::KafkaConsumer &consumer, const ConsumerInfo &info,
|
||||
std::vector<RdKafka::TopicPartition *> &last_assignment) {
|
||||
if (!last_assignment.empty()) {
|
||||
if (const auto err = consumer.assign(last_assignment); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerStartFailedException(info.consumer_name,
|
||||
fmt::format("Couldn't restore commited offsets: '{}'", RdKafka::err2str(err)));
|
||||
}
|
||||
RdKafka::TopicPartition::destroy(last_assignment);
|
||||
}
|
||||
}
|
||||
|
||||
void TryToConsumeBatch(RdKafka::KafkaConsumer &consumer, const ConsumerInfo &info,
|
||||
const ConsumerFunction &consumer_function, const std::vector<Message> &batch) {
|
||||
consumer_function(batch);
|
||||
std::vector<RdKafka::TopicPartition *> partitions;
|
||||
utils::OnScopeExit clear_partitions([&]() { RdKafka::TopicPartition::destroy(partitions); });
|
||||
|
||||
if (const auto err = consumer.assignment(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCommitFailedException(
|
||||
info.consumer_name, fmt::format("Couldn't get assignment to commit offsets: {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer.position(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCommitFailedException(info.consumer_name,
|
||||
fmt::format("Couldn't get offsets from librdkafka {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer.commitSync(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCommitFailedException(info.consumer_name, RdKafka::err2str(err));
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Message::Message(std::unique_ptr<RdKafka::Message> &&message) : message_{std::move(message)} {
|
||||
@@ -221,10 +251,21 @@ void Consumer::Start() {
|
||||
StartConsuming();
|
||||
}
|
||||
|
||||
void Consumer::StartIfStopped() {
|
||||
if (!is_running_) {
|
||||
StartConsuming();
|
||||
void Consumer::StartWithLimit(const uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
if (is_running_) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
if (limit_batches < kMinimumStartBatchLimit) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name, fmt::format("Batch limit has to be greater than or equal to {}", kMinimumStartBatchLimit));
|
||||
}
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name,
|
||||
fmt::format("Timeout has to be greater than or equal to {} milliseconds", kMinimumInterval.count()));
|
||||
}
|
||||
|
||||
StartConsumingWithLimit(limit_batches, timeout);
|
||||
}
|
||||
|
||||
void Consumer::Stop() {
|
||||
@@ -244,7 +285,7 @@ void Consumer::StopIfRunning() {
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const {
|
||||
// NOLINTNEXTLINE (modernize-use-nullptr)
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
@@ -344,13 +385,7 @@ void Consumer::StartConsuming() {
|
||||
|
||||
is_running_.store(true);
|
||||
|
||||
if (!last_assignment_.empty()) {
|
||||
if (const auto err = consumer_->assign(last_assignment_); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerStartFailedException(info_.consumer_name,
|
||||
fmt::format("Couldn't restore commited offsets: '{}'", RdKafka::err2str(err)));
|
||||
}
|
||||
RdKafka::TopicPartition::destroy(last_assignment_);
|
||||
}
|
||||
CheckAndDestroyLastAssignmentIfNeeded(*consumer_, info_, last_assignment_);
|
||||
|
||||
thread_ = std::thread([this] {
|
||||
static constexpr auto kMaxThreadNameSize = utils::GetMaxThreadNameSize();
|
||||
@@ -361,33 +396,18 @@ void Consumer::StartConsuming() {
|
||||
while (is_running_) {
|
||||
auto maybe_batch = GetBatch(*consumer_, info_, is_running_);
|
||||
if (maybe_batch.HasError()) {
|
||||
spdlog::warn("Error happened in consumer {} while fetching messages: {}!", info_.consumer_name,
|
||||
maybe_batch.GetError());
|
||||
break;
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
|
||||
if (batch.empty()) continue;
|
||||
if (batch.empty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
spdlog::info("Kafka consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
try {
|
||||
consumer_function_(batch);
|
||||
std::vector<RdKafka::TopicPartition *> partitions;
|
||||
utils::OnScopeExit clear_partitions([&]() { RdKafka::TopicPartition::destroy(partitions); });
|
||||
|
||||
if (const auto err = consumer_->assignment(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCheckFailedException(
|
||||
info_.consumer_name, fmt::format("Couldn't get assignment to commit offsets: {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer_->position(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCheckFailedException(
|
||||
info_.consumer_name, fmt::format("Couldn't get offsets from librdkafka {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer_->commitSync(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
spdlog::warn("Committing offset of consumer {} failed: {}", info_.consumer_name, RdKafka::err2str(err));
|
||||
break;
|
||||
}
|
||||
TryToConsumeBatch(*consumer_, info_, consumer_function_, batch);
|
||||
} catch (const std::exception &e) {
|
||||
spdlog::warn("Error happened in consumer {} while processing a batch: {}!", info_.consumer_name, e.what());
|
||||
break;
|
||||
@@ -398,6 +418,44 @@ void Consumer::StartConsuming() {
|
||||
});
|
||||
}
|
||||
|
||||
void Consumer::StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
MG_ASSERT(!is_running_, "Cannot start already running consumer!");
|
||||
|
||||
if (is_running_.exchange(true)) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
utils::OnScopeExit restore_is_running([this] { is_running_.store(false); });
|
||||
|
||||
CheckAndDestroyLastAssignmentIfNeeded(*consumer_, info_, last_assignment_);
|
||||
|
||||
const auto timeout_to_use = timeout.value_or(kDefaultCheckTimeout);
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
|
||||
for (uint64_t batch_count = 0; batch_count < limit_batches;) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - start >= timeout_to_use) {
|
||||
throw ConsumerStartFailedException(info_.consumer_name, "Timeout reached");
|
||||
}
|
||||
|
||||
const auto maybe_batch = GetBatch(*consumer_, info_, is_running_);
|
||||
if (maybe_batch.HasError()) {
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
|
||||
if (batch.empty()) {
|
||||
continue;
|
||||
}
|
||||
++batch_count;
|
||||
|
||||
spdlog::info("Kafka consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
TryToConsumeBatch(*consumer_, info_, consumer_function_, batch);
|
||||
|
||||
spdlog::info("Kafka consumer {} finished processing", info_.consumer_name);
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::StopConsuming() {
|
||||
is_running_.store(false);
|
||||
if (thread_.joinable()) thread_.join();
|
||||
|
||||
@@ -113,11 +113,19 @@ class Consumer final : public RdKafka::EventCb {
|
||||
/// This method will start a new thread which will poll all the topics for messages.
|
||||
///
|
||||
/// @throws ConsumerRunningException if the consumer is already running
|
||||
/// @throws ConsumerStartFailedException if the commited offsets cannot be restored
|
||||
void Start();
|
||||
|
||||
/// Starts consuming messages if it is not started already.
|
||||
/// Starts consuming messages.
|
||||
///
|
||||
void StartIfStopped();
|
||||
/// This method will start a new thread which will poll all the topics for messages.
|
||||
///
|
||||
/// @param limit_batches the consumer will only consume the given number of batches.
|
||||
/// @param timeout the maximum duration during which the command should run.
|
||||
///
|
||||
/// @throws ConsumerRunningException if the consumer is already running
|
||||
/// @throws ConsumerStartFailedException if the commited offsets cannot be restored
|
||||
void StartWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
|
||||
/// Stops consuming messages.
|
||||
///
|
||||
@@ -136,9 +144,9 @@ class Consumer final : public RdKafka::EventCb {
|
||||
/// used.
|
||||
/// @param check_consumer_function a function to feed the received messages in, only used during this dry-run.
|
||||
///
|
||||
/// @throws ConsumerRunningException if the consumer is alredy running.
|
||||
/// @throws ConsumerRunningException if the consumer is already running.
|
||||
/// @throws ConsumerCheckFailedException if check isn't successful.
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const;
|
||||
|
||||
/// Returns true if the consumer is actively consuming messages.
|
||||
@@ -157,6 +165,7 @@ class Consumer final : public RdKafka::EventCb {
|
||||
void event_cb(RdKafka::Event &event) override;
|
||||
|
||||
void StartConsuming();
|
||||
void StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
|
||||
void StopConsuming();
|
||||
|
||||
@@ -178,7 +187,6 @@ class Consumer final : public RdKafka::EventCb {
|
||||
ConsumerFunction consumer_function_;
|
||||
mutable std::atomic<bool> is_running_{false};
|
||||
mutable std::vector<RdKafka::TopicPartition *> last_assignment_; // Protected by is_running_
|
||||
std::optional<int64_t> limit_batches_{std::nullopt};
|
||||
std::unique_ptr<RdKafka::KafkaConsumer, std::function<void(RdKafka::KafkaConsumer *)>> consumer_;
|
||||
std::thread thread_;
|
||||
ConsumerRebalanceCb cb_;
|
||||
|
||||
@@ -64,4 +64,16 @@ class TopicNotFoundException : public KafkaStreamException {
|
||||
TopicNotFoundException(const std::string_view consumer_name, const std::string_view topic_name)
|
||||
: KafkaStreamException("Kafka consumer {} cannot find topic {}", consumer_name, topic_name) {}
|
||||
};
|
||||
|
||||
class ConsumerCommitFailedException : public KafkaStreamException {
|
||||
public:
|
||||
ConsumerCommitFailedException(const std::string_view consumer_name, const std::string_view error)
|
||||
: KafkaStreamException("Committing offset of consumer {} failed: {}", consumer_name, error) {}
|
||||
};
|
||||
|
||||
class ConsumerReadMessagesFailedException : public KafkaStreamException {
|
||||
public:
|
||||
ConsumerReadMessagesFailedException(const std::string_view consumer_name, const std::string_view error)
|
||||
: KafkaStreamException("Error happened in consumer {} while fetching messages: {}", consumer_name, error) {}
|
||||
};
|
||||
} // namespace memgraph::integrations::kafka
|
||||
|
||||
@@ -11,13 +11,14 @@
|
||||
|
||||
#include "integrations/pulsar/consumer.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <pulsar/Client.h>
|
||||
#include <pulsar/InitialPosition.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <thread>
|
||||
|
||||
#include "integrations/constants.hpp"
|
||||
#include "integrations/pulsar/exceptions.hpp"
|
||||
#include "utils/concepts.hpp"
|
||||
@@ -33,6 +34,10 @@ namespace {
|
||||
template <typename T>
|
||||
concept PulsarConsumer = utils::SameAsAnyOf<T, pulsar_client::Consumer, pulsar_client::Reader>;
|
||||
|
||||
template <typename TFunc>
|
||||
concept PulsarMessageGetter =
|
||||
std::same_as<const pulsar_client::Message &, std::invoke_result_t<TFunc, const Message &>>;
|
||||
|
||||
pulsar_client::Result ConsumeMessage(pulsar_client::Consumer &consumer, pulsar_client::Message &message,
|
||||
int remaining_timeout_in_ms) {
|
||||
return consumer.receive(message, remaining_timeout_in_ms);
|
||||
@@ -97,6 +102,26 @@ pulsar_client::Client CreateClient(const std::string &service_url) {
|
||||
conf.setLogger(new SpdlogLoggerFactory);
|
||||
return {service_url, conf};
|
||||
}
|
||||
|
||||
template <PulsarConsumer TConsumer, PulsarMessageGetter TPulsarMessageGetter>
|
||||
void TryToConsumeBatch(TConsumer &consumer, const ConsumerInfo &info, const ConsumerFunction &consumer_function,
|
||||
pulsar_client::MessageId &last_message_id, const std::vector<Message> &batch,
|
||||
const TPulsarMessageGetter &message_getter) {
|
||||
consumer_function(batch);
|
||||
|
||||
auto has_message_failed = [&consumer, &info, &last_message_id, &message_getter](const auto &message) {
|
||||
if (const auto result = consumer.acknowledge(message_getter(message)); result != pulsar_client::ResultOk) {
|
||||
spdlog::warn("Acknowledging a message of consumer {} failed: {}", info.consumer_name, result);
|
||||
return true;
|
||||
}
|
||||
last_message_id = message_getter(message).getMessageId();
|
||||
return false;
|
||||
};
|
||||
|
||||
if (std::ranges::any_of(batch, has_message_failed)) {
|
||||
throw ConsumerAcknowledgeMessagesFailedException(info.consumer_name);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Message::Message(pulsar_client::Message &&message) : message_{std::move(message)} {}
|
||||
@@ -137,6 +162,24 @@ void Consumer::Start() {
|
||||
StartConsuming();
|
||||
}
|
||||
|
||||
void Consumer::StartWithLimit(const uint64_t limit_batches,
|
||||
const std::optional<std::chrono::milliseconds> timeout) const {
|
||||
if (is_running_) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
if (limit_batches < kMinimumStartBatchLimit) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name, fmt::format("Batch limit has to be greater than or equal to {}", kMinimumStartBatchLimit));
|
||||
}
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
throw ConsumerStartFailedException(
|
||||
info_.consumer_name,
|
||||
fmt::format("Timeout has to be greater than or equal to {} milliseconds", kMinimumInterval.count()));
|
||||
}
|
||||
|
||||
StartConsumingWithLimit(limit_batches, timeout);
|
||||
}
|
||||
|
||||
void Consumer::Stop() {
|
||||
if (!is_running_) {
|
||||
throw ConsumerStoppedException(info_.consumer_name);
|
||||
@@ -154,7 +197,7 @@ void Consumer::StopIfRunning() {
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const {
|
||||
// NOLINTNEXTLINE (modernize-use-nullptr)
|
||||
if (timeout.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
@@ -240,9 +283,7 @@ void Consumer::StartConsuming() {
|
||||
auto maybe_batch = GetBatch(consumer_, info_, is_running_, last_message_id_);
|
||||
|
||||
if (maybe_batch.HasError()) {
|
||||
spdlog::warn("Error happened in consumer {} while fetching messages: {}!", info_.consumer_name,
|
||||
maybe_batch.GetError());
|
||||
break;
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
@@ -254,18 +295,8 @@ void Consumer::StartConsuming() {
|
||||
spdlog::info("Pulsar consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
try {
|
||||
consumer_function_(batch);
|
||||
|
||||
if (std::any_of(batch.begin(), batch.end(), [&](const auto &message) {
|
||||
if (const auto result = consumer_.acknowledge(message.message_); result != pulsar_client::ResultOk) {
|
||||
spdlog::warn("Acknowledging a message of consumer {} failed: {}", info_.consumer_name, result);
|
||||
return true;
|
||||
}
|
||||
last_message_id_ = message.message_.getMessageId();
|
||||
return false;
|
||||
})) {
|
||||
break;
|
||||
}
|
||||
TryToConsumeBatch(consumer_, info_, consumer_function_, last_message_id_, batch,
|
||||
[&](const Message &message) -> const pulsar_client::Message & { return message.message_; });
|
||||
} catch (const std::exception &e) {
|
||||
spdlog::warn("Error happened in consumer {} while processing a batch: {}!", info_.consumer_name, e.what());
|
||||
break;
|
||||
@@ -277,6 +308,43 @@ void Consumer::StartConsuming() {
|
||||
});
|
||||
}
|
||||
|
||||
void Consumer::StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const {
|
||||
if (is_running_.exchange(true)) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
utils::OnScopeExit restore_is_running([this] { is_running_.store(false); });
|
||||
|
||||
const auto timeout_to_use = timeout.value_or(kDefaultCheckTimeout);
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
|
||||
for (uint64_t batch_count = 0; batch_count < limit_batches;) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - start >= timeout_to_use) {
|
||||
throw ConsumerCheckFailedException(info_.consumer_name, "Timeout reached");
|
||||
}
|
||||
|
||||
const auto maybe_batch = GetBatch(consumer_, info_, is_running_, last_message_id_);
|
||||
|
||||
if (maybe_batch.HasError()) {
|
||||
throw ConsumerReadMessagesFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
}
|
||||
|
||||
const auto &batch = maybe_batch.GetValue();
|
||||
|
||||
if (batch.empty()) {
|
||||
continue;
|
||||
}
|
||||
++batch_count;
|
||||
|
||||
spdlog::info("Pulsar consumer {} is processing a batch", info_.consumer_name);
|
||||
|
||||
TryToConsumeBatch(consumer_, info_, consumer_function_, last_message_id_, batch,
|
||||
[](const Message &message) -> const pulsar_client::Message & { return message.message_; });
|
||||
|
||||
spdlog::info("Pulsar consumer {} finished processing", info_.consumer_name);
|
||||
}
|
||||
}
|
||||
|
||||
void Consumer::StopConsuming() {
|
||||
is_running_.store(false);
|
||||
if (thread_.joinable()) {
|
||||
|
||||
@@ -58,25 +58,27 @@ class Consumer final {
|
||||
|
||||
bool IsRunning() const;
|
||||
void Start();
|
||||
void StartWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
void Stop();
|
||||
void StopIfRunning();
|
||||
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> limit_batches,
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<uint64_t> limit_batches,
|
||||
const ConsumerFunction &check_consumer_function) const;
|
||||
|
||||
const ConsumerInfo &Info() const;
|
||||
|
||||
private:
|
||||
void StartConsuming();
|
||||
void StartConsumingWithLimit(uint64_t limit_batches, std::optional<std::chrono::milliseconds> timeout) const;
|
||||
void StopConsuming();
|
||||
|
||||
ConsumerInfo info_;
|
||||
mutable pulsar_client::Client client_;
|
||||
pulsar_client::Consumer consumer_;
|
||||
mutable pulsar_client::Consumer consumer_;
|
||||
ConsumerFunction consumer_function_;
|
||||
|
||||
mutable std::atomic<bool> is_running_{false};
|
||||
pulsar_client::MessageId last_message_id_{pulsar_client::MessageId::earliest()};
|
||||
mutable pulsar_client::MessageId last_message_id_{pulsar_client::MessageId::earliest()}; // Protected by is_running_
|
||||
std::thread thread_;
|
||||
};
|
||||
} // namespace memgraph::integrations::pulsar
|
||||
|
||||
@@ -55,4 +55,16 @@ class TopicNotFoundException : public PulsarStreamException {
|
||||
TopicNotFoundException(const std::string &consumer_name, const std::string &topic_name)
|
||||
: PulsarStreamException("Pulsar consumer {} cannot find topic {}", consumer_name, topic_name) {}
|
||||
};
|
||||
|
||||
class ConsumerReadMessagesFailedException : public PulsarStreamException {
|
||||
public:
|
||||
ConsumerReadMessagesFailedException(const std::string_view consumer_name, const std::string_view error)
|
||||
: PulsarStreamException("Error happened in consumer {} while fetching messages: {}", consumer_name, error) {}
|
||||
};
|
||||
|
||||
class ConsumerAcknowledgeMessagesFailedException : public PulsarStreamException {
|
||||
public:
|
||||
explicit ConsumerAcknowledgeMessagesFailedException(const std::string_view consumer_name)
|
||||
: PulsarStreamException("Acknowledging a message of consumer {} has failed!", consumer_name) {}
|
||||
};
|
||||
} // namespace memgraph::integrations::pulsar
|
||||
|
||||
77
src/io/address.hpp
Normal file
77
src/io/address.hpp
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <compare>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/uuid/uuid.hpp>
|
||||
#include <boost/uuid/uuid_generators.hpp>
|
||||
#include <boost/uuid/uuid_io.hpp>
|
||||
|
||||
namespace memgraph::io {
|
||||
struct Address {
|
||||
// It's important for all participants to have a
|
||||
// unique identifier - IP and port alone are not
|
||||
// enough, and may change over the lifecycle of
|
||||
// the nodes. Particularly storage nodes may change
|
||||
// their IP addresses over time, and the system
|
||||
// should gracefully update its information
|
||||
// about them.
|
||||
boost::uuids::uuid unique_id;
|
||||
boost::asio::ip::address last_known_ip;
|
||||
uint16_t last_known_port;
|
||||
|
||||
static Address TestAddress(uint16_t port) {
|
||||
return Address{
|
||||
.unique_id = boost::uuids::uuid{boost::uuids::random_generator()()},
|
||||
.last_known_port = port,
|
||||
};
|
||||
}
|
||||
|
||||
static Address UniqueLocalAddress() {
|
||||
return Address{
|
||||
.unique_id = boost::uuids::uuid{boost::uuids::random_generator()()},
|
||||
};
|
||||
}
|
||||
|
||||
/// Returns a new ID with the same IP and port but a unique UUID.
|
||||
Address ForkUniqueAddress() {
|
||||
return Address{
|
||||
.unique_id = boost::uuids::uuid{boost::uuids::random_generator()()},
|
||||
.last_known_ip = last_known_ip,
|
||||
.last_known_port = last_known_port,
|
||||
};
|
||||
}
|
||||
|
||||
friend bool operator==(const Address &lhs, const Address &rhs) = default;
|
||||
|
||||
/// unique_id is most dominant for ordering, then last_known_ip, then last_known_port
|
||||
friend bool operator<(const Address &lhs, const Address &rhs) {
|
||||
if (lhs.unique_id != rhs.unique_id) {
|
||||
return lhs.unique_id < rhs.unique_id;
|
||||
}
|
||||
|
||||
if (lhs.last_known_ip != rhs.last_known_ip) {
|
||||
return lhs.last_known_ip < rhs.last_known_ip;
|
||||
}
|
||||
|
||||
return lhs.last_known_port < rhs.last_known_port;
|
||||
}
|
||||
|
||||
std::string ToString() const {
|
||||
return fmt::format("Address {{ unique_id: {}, last_known_ip: {}, last_known_port: {} }}",
|
||||
boost::uuids::to_string(unique_id), last_known_ip.to_string(), last_known_port);
|
||||
}
|
||||
};
|
||||
}; // namespace memgraph::io
|
||||
26
src/io/errors.hpp
Normal file
26
src/io/errors.hpp
Normal file
@@ -0,0 +1,26 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace memgraph::io {
|
||||
// Signifies that a retriable operation was unable to
|
||||
// complete after a configured number of retries.
|
||||
struct RetriesExhausted {};
|
||||
|
||||
// Signifies that a request was unable to receive a response
|
||||
// within some configured timeout duration. It is important
|
||||
// to remember that in distributed systems, a timeout does
|
||||
// not signify that a request was not received or processed.
|
||||
// It may be the case that the request was fully processed
|
||||
// but that the response was not received.
|
||||
struct TimedOut {};
|
||||
}; // namespace memgraph::io
|
||||
256
src/io/future.hpp
Normal file
256
src/io/future.hpp
Normal file
@@ -0,0 +1,256 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
|
||||
#include "io/errors.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
|
||||
namespace memgraph::io {
|
||||
|
||||
// Shared is in an anonymous namespace, and the only way to
|
||||
// construct a Promise or Future is to pass a Shared in. This
|
||||
// ensures that Promises and Futures can only be constructed
|
||||
// in this translation unit.
|
||||
namespace details {
|
||||
template <typename T>
|
||||
class Shared {
|
||||
mutable std::condition_variable cv_;
|
||||
mutable std::mutex mu_;
|
||||
std::optional<T> item_;
|
||||
bool consumed_ = false;
|
||||
bool waiting_ = false;
|
||||
std::function<bool()> simulator_notifier_ = nullptr;
|
||||
|
||||
public:
|
||||
explicit Shared(std::function<bool()> simulator_notifier) : simulator_notifier_(simulator_notifier) {}
|
||||
Shared() = default;
|
||||
Shared(Shared &&) = delete;
|
||||
Shared &operator=(Shared &&) = delete;
|
||||
Shared(const Shared &) = delete;
|
||||
Shared &operator=(const Shared &) = delete;
|
||||
~Shared() = default;
|
||||
|
||||
/// Takes the item out of our optional item_ and returns it.
|
||||
T Take() {
|
||||
MG_ASSERT(item_, "Take called without item_ being present");
|
||||
MG_ASSERT(!consumed_, "Take called on already-consumed Future");
|
||||
|
||||
T ret = std::move(item_).value();
|
||||
item_.reset();
|
||||
|
||||
consumed_ = true;
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
T Wait() {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
waiting_ = true;
|
||||
|
||||
while (!item_) {
|
||||
bool simulator_progressed = false;
|
||||
if (simulator_notifier_) [[unlikely]] {
|
||||
// We can't hold our own lock while notifying
|
||||
// the simulator because notifying the simulator
|
||||
// involves acquiring the simulator's mutex
|
||||
// to guarantee that our notification linearizes
|
||||
// with the simulator's condition variable.
|
||||
// However, the simulator may acquire our
|
||||
// mutex to check if we are being awaited,
|
||||
// while determining system quiescence,
|
||||
// so we have to get out of its way to avoid
|
||||
// a cyclical deadlock.
|
||||
lock.unlock();
|
||||
simulator_progressed = std::invoke(simulator_notifier_);
|
||||
lock.lock();
|
||||
if (item_) {
|
||||
// item may have been filled while we
|
||||
// had dropped our mutex while notifying
|
||||
// the simulator of our waiting_ status.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!simulator_progressed) [[likely]] {
|
||||
cv_.wait(lock);
|
||||
}
|
||||
MG_ASSERT(!consumed_, "Future consumed twice!");
|
||||
}
|
||||
|
||||
waiting_ = false;
|
||||
|
||||
return Take();
|
||||
}
|
||||
|
||||
bool IsReady() const {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
return item_.has_value();
|
||||
}
|
||||
|
||||
std::optional<T> TryGet() {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
|
||||
if (item_) {
|
||||
return Take();
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void Fill(T item) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
|
||||
MG_ASSERT(!consumed_, "Promise filled after it was already consumed!");
|
||||
MG_ASSERT(!item_, "Promise filled twice!");
|
||||
|
||||
item_ = item;
|
||||
} // lock released before condition variable notification
|
||||
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
bool IsAwaited() const {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
return waiting_;
|
||||
}
|
||||
};
|
||||
} // namespace details
|
||||
|
||||
template <typename T>
|
||||
class Future {
|
||||
bool consumed_or_moved_ = false;
|
||||
std::shared_ptr<details::Shared<T>> shared_;
|
||||
|
||||
public:
|
||||
explicit Future(std::shared_ptr<details::Shared<T>> shared) : shared_(shared) {}
|
||||
|
||||
Future() = delete;
|
||||
Future(Future &&old) noexcept {
|
||||
MG_ASSERT(!old.consumed_or_moved_, "Future moved from after already being moved from or consumed.");
|
||||
shared_ = std::move(old.shared_);
|
||||
consumed_or_moved_ = old.consumed_or_moved_;
|
||||
old.consumed_or_moved_ = true;
|
||||
}
|
||||
|
||||
Future(const Future &) = delete;
|
||||
Future &operator=(const Future &) = delete;
|
||||
~Future() = default;
|
||||
|
||||
/// Returns true if the Future is ready to
|
||||
/// be consumed using TryGet or Wait (prefer Wait
|
||||
/// if you know it's ready, because it doesn't
|
||||
/// return an optional.
|
||||
bool IsReady() {
|
||||
MG_ASSERT(!consumed_or_moved_, "Called IsReady after Future already consumed!");
|
||||
return shared_->IsReady();
|
||||
}
|
||||
|
||||
/// Non-blocking method that returns the inner
|
||||
/// item if it's already ready, or std::nullopt
|
||||
/// if it is not ready yet.
|
||||
std::optional<T> TryGet() {
|
||||
MG_ASSERT(!consumed_or_moved_, "Called TryGet after Future already consumed!");
|
||||
std::optional<T> ret = shared_->TryGet();
|
||||
if (ret) {
|
||||
consumed_or_moved_ = true;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Block on the corresponding promise to be filled,
|
||||
/// returning the inner item when ready.
|
||||
T Wait() && {
|
||||
MG_ASSERT(!consumed_or_moved_, "Future should only be consumed with Wait once!");
|
||||
T ret = shared_->Wait();
|
||||
consumed_or_moved_ = true;
|
||||
return ret;
|
||||
}
|
||||
|
||||
/// Marks this Future as canceled.
|
||||
void Cancel() {
|
||||
MG_ASSERT(!consumed_or_moved_, "Future::Cancel called on a future that was already moved or consumed!");
|
||||
consumed_or_moved_ = true;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class Promise {
|
||||
std::shared_ptr<details::Shared<T>> shared_;
|
||||
bool filled_or_moved_ = false;
|
||||
|
||||
public:
|
||||
explicit Promise(std::shared_ptr<details::Shared<T>> shared) : shared_(shared) {}
|
||||
|
||||
Promise() = delete;
|
||||
Promise(Promise &&old) noexcept {
|
||||
MG_ASSERT(!old.filled_or_moved_, "Promise moved from after already being moved from or filled.");
|
||||
shared_ = std::move(old.shared_);
|
||||
old.filled_or_moved_ = true;
|
||||
}
|
||||
|
||||
Promise &operator=(Promise &&old) noexcept {
|
||||
MG_ASSERT(!old.filled_or_moved_, "Promise moved from after already being moved from or filled.");
|
||||
shared_ = std::move(old.shared_);
|
||||
old.filled_or_moved_ = true;
|
||||
}
|
||||
Promise(const Promise &) = delete;
|
||||
Promise &operator=(const Promise &) = delete;
|
||||
|
||||
~Promise() { MG_ASSERT(filled_or_moved_, "Promise destroyed before its associated Future was filled!"); }
|
||||
|
||||
// Fill the expected item into the Future.
|
||||
void Fill(T item) {
|
||||
MG_ASSERT(!filled_or_moved_, "Promise::Fill called on a promise that is already filled or moved!");
|
||||
shared_->Fill(item);
|
||||
filled_or_moved_ = true;
|
||||
}
|
||||
|
||||
bool IsAwaited() { return shared_->IsAwaited(); }
|
||||
|
||||
/// Moves this Promise into a unique_ptr.
|
||||
std::unique_ptr<Promise<T>> ToUnique() && {
|
||||
std::unique_ptr<Promise<T>> up = std::make_unique<Promise<T>>(std::move(shared_));
|
||||
|
||||
filled_or_moved_ = true;
|
||||
|
||||
return up;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
std::pair<Future<T>, Promise<T>> FuturePromisePair() {
|
||||
std::shared_ptr<details::Shared<T>> shared = std::make_shared<details::Shared<T>>();
|
||||
|
||||
Future<T> future = Future<T>(shared);
|
||||
Promise<T> promise = Promise<T>(shared);
|
||||
|
||||
return std::make_pair(std::move(future), std::move(promise));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
std::pair<Future<T>, Promise<T>> FuturePromisePairWithNotifier(std::function<bool()> simulator_notifier) {
|
||||
std::shared_ptr<details::Shared<T>> shared = std::make_shared<details::Shared<T>>(simulator_notifier);
|
||||
|
||||
Future<T> future = Future<T>(shared);
|
||||
Promise<T> promise = Promise<T>(shared);
|
||||
|
||||
return std::make_pair(std::move(future), std::move(promise));
|
||||
}
|
||||
|
||||
}; // namespace memgraph::io
|
||||
35
src/io/local_transport/local_system.hpp
Normal file
35
src/io/local_transport/local_system.hpp
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <random>
|
||||
|
||||
#include "io/address.hpp"
|
||||
#include "io/local_transport/local_transport.hpp"
|
||||
#include "io/local_transport/local_transport_handle.hpp"
|
||||
#include "io/transport.hpp"
|
||||
|
||||
namespace memgraph::io::local_transport {
|
||||
|
||||
class LocalSystem {
|
||||
std::shared_ptr<LocalTransportHandle> local_transport_handle_ = std::make_shared<LocalTransportHandle>();
|
||||
|
||||
public:
|
||||
Io<LocalTransport> Register(Address address) {
|
||||
LocalTransport local_transport(local_transport_handle_);
|
||||
return Io{local_transport, address};
|
||||
}
|
||||
|
||||
void ShutDown() { local_transport_handle_->ShutDown(); }
|
||||
};
|
||||
|
||||
} // namespace memgraph::io::local_transport
|
||||
64
src/io/local_transport/local_transport.hpp
Normal file
64
src/io/local_transport/local_transport.hpp
Normal file
@@ -0,0 +1,64 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <memory>
|
||||
#include <random>
|
||||
#include <utility>
|
||||
|
||||
#include "io/address.hpp"
|
||||
#include "io/local_transport/local_transport_handle.hpp"
|
||||
#include "io/time.hpp"
|
||||
#include "io/transport.hpp"
|
||||
|
||||
namespace memgraph::io::local_transport {
|
||||
|
||||
class LocalTransport {
|
||||
std::shared_ptr<LocalTransportHandle> local_transport_handle_;
|
||||
|
||||
public:
|
||||
explicit LocalTransport(std::shared_ptr<LocalTransportHandle> local_transport_handle)
|
||||
: local_transport_handle_(std::move(local_transport_handle)) {}
|
||||
|
||||
template <Message RequestT, Message ResponseT>
|
||||
ResponseFuture<ResponseT> Request(Address to_address, Address from_address, RequestId request_id, RequestT request,
|
||||
Duration timeout) {
|
||||
auto [future, promise] = memgraph::io::FuturePromisePair<ResponseResult<ResponseT>>();
|
||||
|
||||
local_transport_handle_->SubmitRequest(to_address, from_address, request_id, std::move(request), timeout,
|
||||
std::move(promise));
|
||||
|
||||
return std::move(future);
|
||||
}
|
||||
|
||||
template <Message... Ms>
|
||||
requires(sizeof...(Ms) > 0) RequestResult<Ms...> Receive(Address receiver_address, Duration timeout) {
|
||||
return local_transport_handle_->template Receive<Ms...>(receiver_address, timeout);
|
||||
}
|
||||
|
||||
template <Message M>
|
||||
void Send(Address to_address, Address from_address, RequestId request_id, M &&message) {
|
||||
return local_transport_handle_->template Send<M>(to_address, from_address, request_id, std::forward<M>(message));
|
||||
}
|
||||
|
||||
Time Now() const { return local_transport_handle_->Now(); }
|
||||
|
||||
bool ShouldShutDown() const { return local_transport_handle_->ShouldShutDown(); }
|
||||
|
||||
template <class D = std::poisson_distribution<>, class Return = uint64_t>
|
||||
Return Rand(D distrib) {
|
||||
std::random_device rng;
|
||||
return distrib(rng);
|
||||
}
|
||||
};
|
||||
}; // namespace memgraph::io::local_transport
|
||||
151
src/io/local_transport/local_transport_handle.hpp
Normal file
151
src/io/local_transport/local_transport_handle.hpp
Normal file
@@ -0,0 +1,151 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
|
||||
#include "io/errors.hpp"
|
||||
#include "io/message_conversion.hpp"
|
||||
#include "io/time.hpp"
|
||||
#include "io/transport.hpp"
|
||||
|
||||
namespace memgraph::io::local_transport {
|
||||
|
||||
class LocalTransportHandle {
|
||||
mutable std::mutex mu_{};
|
||||
mutable std::condition_variable cv_;
|
||||
bool should_shut_down_ = false;
|
||||
|
||||
// the responses to requests that are being waited on
|
||||
std::map<PromiseKey, DeadlineAndOpaquePromise> promises_;
|
||||
|
||||
// messages that are sent to servers that may later receive them
|
||||
std::vector<OpaqueMessage> can_receive_;
|
||||
|
||||
public:
|
||||
~LocalTransportHandle() {
|
||||
for (auto &&[pk, promise] : promises_) {
|
||||
std::move(promise.promise).TimeOut();
|
||||
}
|
||||
promises_.clear();
|
||||
}
|
||||
|
||||
void ShutDown() {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
should_shut_down_ = true;
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
bool ShouldShutDown() const {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
return should_shut_down_;
|
||||
}
|
||||
|
||||
static Time Now() {
|
||||
auto nano_time = std::chrono::system_clock::now();
|
||||
return std::chrono::time_point_cast<std::chrono::microseconds>(nano_time);
|
||||
}
|
||||
|
||||
template <Message... Ms>
|
||||
requires(sizeof...(Ms) > 0) RequestResult<Ms...> Receive(Address /* receiver_address */, Duration timeout) {
|
||||
std::unique_lock lock(mu_);
|
||||
|
||||
Time before = Now();
|
||||
|
||||
spdlog::info("can_receive_ size: {}", can_receive_.size());
|
||||
|
||||
while (can_receive_.empty()) {
|
||||
Time now = Now();
|
||||
|
||||
// protection against non-monotonic timesources
|
||||
auto maxed_now = std::max(now, before);
|
||||
auto elapsed = maxed_now - before;
|
||||
|
||||
if (timeout < elapsed) {
|
||||
return TimedOut{};
|
||||
}
|
||||
|
||||
Duration relative_timeout = timeout - elapsed;
|
||||
|
||||
std::cv_status cv_status_value = cv_.wait_for(lock, relative_timeout);
|
||||
|
||||
if (cv_status_value == std::cv_status::timeout) {
|
||||
return TimedOut{};
|
||||
}
|
||||
}
|
||||
|
||||
auto current_message = std::move(can_receive_.back());
|
||||
can_receive_.pop_back();
|
||||
|
||||
auto m_opt = std::move(current_message).Take<Ms...>();
|
||||
|
||||
return std::move(m_opt).value();
|
||||
}
|
||||
|
||||
template <Message M>
|
||||
void Send(Address to_address, Address from_address, RequestId request_id, M &&message) {
|
||||
std::any message_any(std::forward<M>(message));
|
||||
OpaqueMessage opaque_message{.to_address = to_address,
|
||||
.from_address = from_address,
|
||||
.request_id = request_id,
|
||||
.message = std::move(message_any)};
|
||||
|
||||
PromiseKey promise_key{
|
||||
.requester_address = to_address, .request_id = opaque_message.request_id, .replier_address = from_address};
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
|
||||
if (promises_.contains(promise_key)) {
|
||||
spdlog::info("using message to fill promise");
|
||||
// complete waiting promise if it's there
|
||||
DeadlineAndOpaquePromise dop = std::move(promises_.at(promise_key));
|
||||
promises_.erase(promise_key);
|
||||
|
||||
dop.promise.Fill(std::move(opaque_message));
|
||||
} else {
|
||||
spdlog::info("placing message in can_receive_");
|
||||
can_receive_.emplace_back(std::move(opaque_message));
|
||||
}
|
||||
} // lock dropped
|
||||
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
template <Message RequestT, Message ResponseT>
|
||||
void SubmitRequest(Address to_address, Address from_address, RequestId request_id, RequestT &&request,
|
||||
Duration timeout, ResponsePromise<ResponseT> promise) {
|
||||
const bool port_matches = to_address.last_known_port == from_address.last_known_port;
|
||||
const bool ip_matches = to_address.last_known_ip == from_address.last_known_ip;
|
||||
|
||||
MG_ASSERT(port_matches && ip_matches);
|
||||
const Time deadline = Now() + timeout;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
|
||||
PromiseKey promise_key{
|
||||
.requester_address = from_address, .request_id = request_id, .replier_address = to_address};
|
||||
OpaquePromise opaque_promise(std::move(promise).ToUnique());
|
||||
DeadlineAndOpaquePromise dop{.deadline = deadline, .promise = std::move(opaque_promise)};
|
||||
promises_.emplace(std::move(promise_key), std::move(dop));
|
||||
} // lock dropped
|
||||
|
||||
Send(to_address, from_address, request_id, std::forward<RequestT>(request));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace memgraph::io::local_transport
|
||||
232
src/io/message_conversion.hpp
Normal file
232
src/io/message_conversion.hpp
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "io/transport.hpp"
|
||||
|
||||
namespace memgraph::io {
|
||||
|
||||
using memgraph::io::Duration;
|
||||
using memgraph::io::Message;
|
||||
using memgraph::io::Time;
|
||||
|
||||
struct PromiseKey {
|
||||
Address requester_address;
|
||||
uint64_t request_id;
|
||||
// TODO(tyler) possibly remove replier_address from promise key
|
||||
// once we want to support DSR.
|
||||
Address replier_address;
|
||||
|
||||
public:
|
||||
friend bool operator<(const PromiseKey &lhs, const PromiseKey &rhs) {
|
||||
if (lhs.requester_address != rhs.requester_address) {
|
||||
return lhs.requester_address < rhs.requester_address;
|
||||
}
|
||||
|
||||
if (lhs.request_id != rhs.request_id) {
|
||||
return lhs.request_id < rhs.request_id;
|
||||
}
|
||||
|
||||
return lhs.replier_address < rhs.replier_address;
|
||||
}
|
||||
};
|
||||
|
||||
struct OpaqueMessage {
|
||||
Address to_address;
|
||||
Address from_address;
|
||||
uint64_t request_id;
|
||||
std::any message;
|
||||
|
||||
/// Recursively tries to match a specific type from the outer
|
||||
/// variant's parameter pack against the type of the std::any,
|
||||
/// and if it matches, make it concrete and return it. Otherwise,
|
||||
/// move on and compare the any with the next type from the
|
||||
/// parameter pack.
|
||||
///
|
||||
/// Return is the full std::variant<Ts...> type that holds the
|
||||
/// full parameter pack without interfering with recursive
|
||||
/// narrowing expansion.
|
||||
template <typename Return, Message Head, Message... Rest>
|
||||
std::optional<Return> Unpack(std::any &&a) {
|
||||
if (typeid(Head) == a.type()) {
|
||||
Head concrete = std::any_cast<Head>(std::move(a));
|
||||
return concrete;
|
||||
}
|
||||
|
||||
if constexpr (sizeof...(Rest) > 0) {
|
||||
return Unpack<Return, Rest...>(std::move(a));
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
/// High level "user-facing" conversion function that lets
|
||||
/// people interested in conversion only supply a single
|
||||
/// parameter pack for the types that they want to compare
|
||||
/// with the any and potentially include in the returned
|
||||
/// variant.
|
||||
template <Message... Ms>
|
||||
requires(sizeof...(Ms) > 0) std::optional<std::variant<Ms...>> VariantFromAny(std::any &&a) {
|
||||
return Unpack<std::variant<Ms...>, Ms...>(std::move(a));
|
||||
}
|
||||
|
||||
template <Message... Ms>
|
||||
requires(sizeof...(Ms) > 0) std::optional<RequestEnvelope<Ms...>> Take() && {
|
||||
std::optional<std::variant<Ms...>> m_opt = VariantFromAny<Ms...>(std::move(message));
|
||||
|
||||
if (m_opt) {
|
||||
return RequestEnvelope<Ms...>{
|
||||
.message = std::move(*m_opt),
|
||||
.request_id = request_id,
|
||||
.to_address = to_address,
|
||||
.from_address = from_address,
|
||||
};
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
class OpaquePromiseTraitBase {
|
||||
public:
|
||||
virtual const std::type_info *TypeInfo() const = 0;
|
||||
virtual bool IsAwaited(void *ptr) const = 0;
|
||||
virtual void Fill(void *ptr, OpaqueMessage &&) const = 0;
|
||||
virtual void TimeOut(void *ptr) const = 0;
|
||||
|
||||
virtual ~OpaquePromiseTraitBase() = default;
|
||||
OpaquePromiseTraitBase() = default;
|
||||
OpaquePromiseTraitBase(const OpaquePromiseTraitBase &) = delete;
|
||||
OpaquePromiseTraitBase &operator=(const OpaquePromiseTraitBase &) = delete;
|
||||
OpaquePromiseTraitBase(OpaquePromiseTraitBase &&old) = delete;
|
||||
OpaquePromiseTraitBase &operator=(OpaquePromiseTraitBase &&) = delete;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class OpaquePromiseTrait : public OpaquePromiseTraitBase {
|
||||
public:
|
||||
const std::type_info *TypeInfo() const override { return &typeid(T); };
|
||||
|
||||
bool IsAwaited(void *ptr) const override { return static_cast<ResponsePromise<T> *>(ptr)->IsAwaited(); };
|
||||
|
||||
void Fill(void *ptr, OpaqueMessage &&opaque_message) const override {
|
||||
T message = std::any_cast<T>(std::move(opaque_message.message));
|
||||
auto response_envelope = ResponseEnvelope<T>{.message = std::move(message),
|
||||
.request_id = opaque_message.request_id,
|
||||
.to_address = opaque_message.to_address,
|
||||
.from_address = opaque_message.from_address};
|
||||
auto promise = static_cast<ResponsePromise<T> *>(ptr);
|
||||
auto unique_promise = std::unique_ptr<ResponsePromise<T>>(promise);
|
||||
unique_promise->Fill(std::move(response_envelope));
|
||||
};
|
||||
|
||||
void TimeOut(void *ptr) const override {
|
||||
auto promise = static_cast<ResponsePromise<T> *>(ptr);
|
||||
auto unique_promise = std::unique_ptr<ResponsePromise<T>>(promise);
|
||||
ResponseResult<T> result = TimedOut{};
|
||||
unique_promise->Fill(std::move(result));
|
||||
}
|
||||
};
|
||||
|
||||
class OpaquePromise {
|
||||
void *ptr_;
|
||||
std::unique_ptr<OpaquePromiseTraitBase> trait_;
|
||||
|
||||
public:
|
||||
OpaquePromise(OpaquePromise &&old) noexcept : ptr_(old.ptr_), trait_(std::move(old.trait_)) {
|
||||
MG_ASSERT(old.ptr_ != nullptr);
|
||||
old.ptr_ = nullptr;
|
||||
}
|
||||
|
||||
OpaquePromise &operator=(OpaquePromise &&old) noexcept {
|
||||
MG_ASSERT(ptr_ == nullptr);
|
||||
MG_ASSERT(old.ptr_ != nullptr);
|
||||
MG_ASSERT(this != &old);
|
||||
ptr_ = old.ptr_;
|
||||
trait_ = std::move(old.trait_);
|
||||
old.ptr_ = nullptr;
|
||||
return *this;
|
||||
}
|
||||
|
||||
OpaquePromise(const OpaquePromise &) = delete;
|
||||
OpaquePromise &operator=(const OpaquePromise &) = delete;
|
||||
|
||||
template <typename T>
|
||||
std::unique_ptr<ResponsePromise<T>> Take() && {
|
||||
MG_ASSERT(typeid(T) == *trait_->TypeInfo());
|
||||
MG_ASSERT(ptr_ != nullptr);
|
||||
|
||||
auto ptr = static_cast<ResponsePromise<T> *>(ptr_);
|
||||
|
||||
ptr_ = nullptr;
|
||||
|
||||
return std::unique_ptr<T>(ptr);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
explicit OpaquePromise(std::unique_ptr<ResponsePromise<T>> promise)
|
||||
: ptr_(static_cast<void *>(promise.release())), trait_(std::make_unique<OpaquePromiseTrait<T>>()) {}
|
||||
|
||||
bool IsAwaited() {
|
||||
MG_ASSERT(ptr_ != nullptr);
|
||||
return trait_->IsAwaited(ptr_);
|
||||
}
|
||||
|
||||
void TimeOut() {
|
||||
MG_ASSERT(ptr_ != nullptr);
|
||||
trait_->TimeOut(ptr_);
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
|
||||
void Fill(OpaqueMessage &&opaque_message) {
|
||||
MG_ASSERT(ptr_ != nullptr);
|
||||
trait_->Fill(ptr_, std::move(opaque_message));
|
||||
ptr_ = nullptr;
|
||||
}
|
||||
|
||||
~OpaquePromise() {
|
||||
MG_ASSERT(ptr_ == nullptr, "OpaquePromise destroyed without being explicitly timed out or filled");
|
||||
}
|
||||
};
|
||||
|
||||
struct DeadlineAndOpaquePromise {
|
||||
Time deadline;
|
||||
OpaquePromise promise;
|
||||
};
|
||||
|
||||
template <class From>
|
||||
std::type_info const &type_info_for_variant(From const &from) {
|
||||
return std::visit([](auto &&x) -> decltype(auto) { return typeid(x); }, from);
|
||||
}
|
||||
|
||||
template <typename From, typename Return, typename Head, typename... Rest>
|
||||
std::optional<Return> ConvertVariantInner(From &&a) {
|
||||
if (typeid(Head) == type_info_for_variant(a)) {
|
||||
Head concrete = std::get<Head>(std::forward<From>(a));
|
||||
return concrete;
|
||||
}
|
||||
|
||||
if constexpr (sizeof...(Rest) > 0) {
|
||||
return ConvertVariantInner<From, Return, Rest...>(std::forward<From>(a));
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
/// This function converts a variant to another variant holding a subset OR superset of
|
||||
/// possible types.
|
||||
template <class From, class... Ms>
|
||||
requires(sizeof...(Ms) > 0) std::optional<std::variant<Ms...>> ConvertVariant(From &&from) {
|
||||
return ConvertVariantInner<From, std::variant<Ms...>, Ms...>(std::forward<From>(from));
|
||||
}
|
||||
|
||||
} // namespace memgraph::io
|
||||
46
src/io/messages.hpp
Normal file
46
src/io/messages.hpp
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <variant>
|
||||
|
||||
#include <coordinator/coordinator.hpp>
|
||||
#include <io/rsm/raft.hpp>
|
||||
#include "query/v2/requests.hpp"
|
||||
#include "utils/concepts.hpp"
|
||||
|
||||
namespace memgraph::io::messages {
|
||||
|
||||
using memgraph::coordinator::CoordinatorReadRequests;
|
||||
using memgraph::coordinator::CoordinatorWriteRequests;
|
||||
using memgraph::coordinator::CoordinatorWriteResponses;
|
||||
using StorageReadRequest = msgs::ReadRequests;
|
||||
using StorageWriteRequest = msgs::WriteRequests;
|
||||
|
||||
using memgraph::io::rsm::AppendRequest;
|
||||
using memgraph::io::rsm::AppendResponse;
|
||||
using memgraph::io::rsm::ReadRequest;
|
||||
using memgraph::io::rsm::VoteRequest;
|
||||
using memgraph::io::rsm::VoteResponse;
|
||||
using memgraph::io::rsm::WriteRequest;
|
||||
using memgraph::io::rsm::WriteResponse;
|
||||
|
||||
using CoordinatorMessages =
|
||||
std::variant<ReadRequest<CoordinatorReadRequests>, AppendRequest<CoordinatorWriteRequests>, AppendResponse,
|
||||
WriteRequest<CoordinatorWriteRequests>, VoteRequest, VoteResponse>;
|
||||
|
||||
using ShardMessages = std::variant<ReadRequest<StorageReadRequest>, AppendRequest<StorageWriteRequest>, AppendResponse,
|
||||
WriteRequest<StorageWriteRequest>, VoteRequest, VoteResponse>;
|
||||
|
||||
using ShardManagerMessages = std::variant<WriteResponse<CoordinatorWriteResponses>>;
|
||||
|
||||
} // namespace memgraph::io::messages
|
||||
926
src/io/rsm/raft.hpp
Normal file
926
src/io/rsm/raft.hpp
Normal file
@@ -0,0 +1,926 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
// TODO(tyler) buffer out-of-order Append buffers on the Followers to reassemble more quickly
|
||||
// TODO(tyler) handle granular batch sizes based on simple flow control
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "io/simulator/simulator.hpp"
|
||||
#include "io/transport.hpp"
|
||||
#include "utils/concepts.hpp"
|
||||
|
||||
namespace memgraph::io::rsm {
|
||||
|
||||
/// Timeout and replication tunables
|
||||
using namespace std::chrono_literals;
|
||||
static constexpr auto kMinimumElectionTimeout = 100ms;
|
||||
static constexpr auto kMaximumElectionTimeout = 200ms;
|
||||
static constexpr auto kMinimumBroadcastTimeout = 40ms;
|
||||
static constexpr auto kMaximumBroadcastTimeout = 60ms;
|
||||
static constexpr auto kMinimumCronInterval = 1ms;
|
||||
static constexpr auto kMaximumCronInterval = 2ms;
|
||||
static constexpr auto kMinimumReceiveTimeout = 40ms;
|
||||
static constexpr auto kMaximumReceiveTimeout = 60ms;
|
||||
static_assert(kMinimumElectionTimeout > kMaximumBroadcastTimeout,
|
||||
"The broadcast timeout has to be smaller than the election timeout!");
|
||||
static_assert(kMinimumElectionTimeout < kMaximumElectionTimeout,
|
||||
"The minimum election timeout has to be smaller than the maximum election timeout!");
|
||||
static_assert(kMinimumBroadcastTimeout < kMaximumBroadcastTimeout,
|
||||
"The minimum broadcast timeout has to be smaller than the maximum broadcast timeout!");
|
||||
static_assert(kMinimumCronInterval < kMaximumCronInterval,
|
||||
"The minimum cron interval has to be smaller than the maximum cron interval!");
|
||||
static_assert(kMinimumReceiveTimeout < kMaximumReceiveTimeout,
|
||||
"The minimum receive timeout has to be smaller than the maximum receive timeout!");
|
||||
static constexpr size_t kMaximumAppendBatchSize = 1024;
|
||||
|
||||
using Term = uint64_t;
|
||||
using LogIndex = uint64_t;
|
||||
using LogSize = uint64_t;
|
||||
|
||||
template <typename WriteOperation>
|
||||
struct WriteRequest {
|
||||
WriteOperation operation;
|
||||
};
|
||||
|
||||
/// WriteResponse is returned to a client after
|
||||
/// their WriteRequest was entered in to the raft
|
||||
/// log and it reached consensus.
|
||||
///
|
||||
/// WriteReturn is the result of applying the WriteRequest to
|
||||
/// ReplicatedState, and if the ReplicatedState::write
|
||||
/// method is deterministic, all replicas will
|
||||
/// have the same ReplicatedState after applying
|
||||
/// the submitted WriteRequest.
|
||||
template <typename WriteReturn>
|
||||
struct WriteResponse {
|
||||
bool success;
|
||||
WriteReturn write_return;
|
||||
std::optional<Address> retry_leader;
|
||||
LogIndex raft_index;
|
||||
};
|
||||
|
||||
template <typename ReadOperation>
|
||||
struct ReadRequest {
|
||||
ReadOperation operation;
|
||||
};
|
||||
|
||||
template <typename ReadReturn>
|
||||
struct ReadResponse {
|
||||
bool success;
|
||||
ReadReturn read_return;
|
||||
std::optional<Address> retry_leader;
|
||||
};
|
||||
|
||||
/// AppendRequest is a raft-level message that the Leader
|
||||
/// periodically broadcasts to all Follower peers. This
|
||||
/// serves three main roles:
|
||||
/// 1. acts as a heartbeat from the Leader to the Follower
|
||||
/// 2. replicates new data that the Leader has received to the Follower
|
||||
/// 3. informs Follower peers when the commit index has increased,
|
||||
/// signalling that it is now safe to apply log items to the
|
||||
/// replicated state machine
|
||||
template <typename WriteRequest>
|
||||
struct AppendRequest {
|
||||
Term term = 0;
|
||||
LogIndex batch_start_log_index;
|
||||
Term last_log_term;
|
||||
std::vector<std::pair<Term, WriteRequest>> entries;
|
||||
LogSize leader_commit;
|
||||
};
|
||||
|
||||
struct AppendResponse {
|
||||
bool success;
|
||||
Term term;
|
||||
Term last_log_term;
|
||||
// a small optimization over the raft paper, tells
|
||||
// the leader the offset that we are interested in
|
||||
// to send log offsets from for us. This will only
|
||||
// be useful at the beginning of a leader's term.
|
||||
LogSize log_size;
|
||||
};
|
||||
|
||||
struct VoteRequest {
|
||||
Term term = 0;
|
||||
LogSize log_size;
|
||||
Term last_log_term;
|
||||
};
|
||||
|
||||
struct VoteResponse {
|
||||
Term term = 0;
|
||||
LogSize committed_log_size;
|
||||
bool vote_granted = false;
|
||||
};
|
||||
|
||||
template <typename WriteRequest>
|
||||
struct CommonState {
|
||||
Term term = 0;
|
||||
std::vector<std::pair<Term, WriteRequest>> log;
|
||||
LogSize committed_log_size = 0;
|
||||
LogSize applied_size = 0;
|
||||
};
|
||||
|
||||
struct FollowerTracker {
|
||||
LogIndex next_index = 0;
|
||||
LogSize confirmed_log_size = 0;
|
||||
};
|
||||
|
||||
struct PendingClientRequest {
|
||||
RequestId request_id;
|
||||
Address address;
|
||||
Time received_at;
|
||||
};
|
||||
|
||||
struct Leader {
|
||||
std::map<Address, FollowerTracker> followers;
|
||||
std::unordered_map<LogIndex, PendingClientRequest> pending_client_requests;
|
||||
Time last_broadcast = Time::min();
|
||||
|
||||
std::string static ToString() { return "\tLeader \t"; }
|
||||
};
|
||||
|
||||
struct Candidate {
|
||||
std::map<Address, LogSize> successful_votes;
|
||||
Time election_began = Time::min();
|
||||
std::set<Address> outstanding_votes;
|
||||
|
||||
std::string static ToString() { return "\tCandidate\t"; }
|
||||
};
|
||||
|
||||
struct Follower {
|
||||
Time last_received_append_entries_timestamp;
|
||||
Address leader_address;
|
||||
|
||||
std::string static ToString() { return "\tFollower \t"; }
|
||||
};
|
||||
|
||||
using Role = std::variant<Candidate, Leader, Follower>;
|
||||
|
||||
template <typename Role>
|
||||
concept AllRoles = memgraph::utils::SameAsAnyOf<Role, Leader, Follower, Candidate>;
|
||||
|
||||
template <typename Role>
|
||||
concept LeaderOrFollower = memgraph::utils::SameAsAnyOf<Role, Leader, Follower>;
|
||||
|
||||
template <typename Role>
|
||||
concept FollowerOrCandidate = memgraph::utils::SameAsAnyOf<Role, Follower, Candidate>;
|
||||
|
||||
/*
|
||||
all ReplicatedState classes should have an Apply method
|
||||
that returns our WriteResponseValue after consensus, and
|
||||
a Read method that returns our ReadResponseValue without
|
||||
requiring consensus.
|
||||
|
||||
ReadResponse Read(ReadOperation);
|
||||
WriteResponseValue ReplicatedState::Apply(WriteRequest);
|
||||
|
||||
For example:
|
||||
If the state is uint64_t, and WriteRequest is `struct PlusOne {};`,
|
||||
and WriteResponseValue is also uint64_t (the new value), then
|
||||
each call to state.Apply(PlusOne{}) will return the new value
|
||||
after incrementing it. 0, 1, 2, 3... and this will be sent back
|
||||
to the client that requested the mutation.
|
||||
|
||||
In practice, these mutations will usually be predicated on some
|
||||
previous value, so that they are idempotent, functioning similarly
|
||||
to a CAS operation.
|
||||
*/
|
||||
template <typename WriteOperation, typename ReadOperation, typename ReplicatedState, typename WriteResponseValue,
|
||||
typename ReadResponseValue>
|
||||
concept Rsm = requires(ReplicatedState state, WriteOperation w, ReadOperation r) {
|
||||
{ state.Read(r) } -> std::same_as<ReadResponseValue>;
|
||||
{ state.Apply(w) } -> std::same_as<WriteResponseValue>;
|
||||
};
|
||||
|
||||
/// Parameter Purpose
|
||||
/// --------------------------
|
||||
/// IoImpl the concrete Io provider - SimulatorTransport, ThriftTransport, etc...
|
||||
/// ReplicatedState the high-level data structure that is managed by the raft-backed replicated state machine
|
||||
/// WriteOperation the individual operation type that is applied to the ReplicatedState in identical order
|
||||
/// across each replica
|
||||
/// WriteResponseValue the return value of calling ReplicatedState::Apply(WriteOperation), which is executed in
|
||||
/// identical order across all replicas after an WriteOperation reaches consensus.
|
||||
/// ReadOperation the type of operations that do not require consensus before executing directly
|
||||
/// on a const ReplicatedState &
|
||||
/// ReadResponseValue the return value of calling ReplicatedState::Read(ReadOperation), which is executed directly
|
||||
/// without going through consensus first
|
||||
template <typename IoImpl, typename ReplicatedState, typename WriteOperation, typename WriteResponseValue,
|
||||
typename ReadOperation, typename ReadResponseValue>
|
||||
requires Rsm<WriteOperation, ReadOperation, ReplicatedState, WriteResponseValue, ReadResponseValue>
|
||||
class Raft {
|
||||
CommonState<WriteOperation> state_;
|
||||
Role role_ = Candidate{};
|
||||
Io<IoImpl> io_;
|
||||
std::vector<Address> peers_;
|
||||
ReplicatedState replicated_state_;
|
||||
Time next_cron_;
|
||||
|
||||
public:
|
||||
Raft(Io<IoImpl> &&io, std::vector<Address> peers, ReplicatedState &&replicated_state)
|
||||
: io_(std::forward<Io<IoImpl>>(io)),
|
||||
peers_(peers),
|
||||
replicated_state_(std::forward<ReplicatedState>(replicated_state)) {
|
||||
if (peers.empty()) {
|
||||
role_ = Leader{};
|
||||
}
|
||||
}
|
||||
|
||||
/// Periodic protocol maintenance. Returns the time that Cron should be called again
|
||||
/// in the future.
|
||||
Time Cron() {
|
||||
// dispatch periodic logic based on our role to a specific Cron method.
|
||||
std::optional<Role> new_role = std::visit([&](auto &role) { return Cron(role); }, role_);
|
||||
|
||||
if (new_role) {
|
||||
role_ = std::move(new_role).value();
|
||||
}
|
||||
const Duration random_cron_interval = RandomTimeout(kMinimumCronInterval, kMaximumCronInterval);
|
||||
|
||||
return io_.Now() + random_cron_interval;
|
||||
}
|
||||
|
||||
/// Returns the Address for our underlying Io implementation
|
||||
Address GetAddress() { return io_.GetAddress(); }
|
||||
|
||||
using ReceiveVariant = std::variant<ReadRequest<ReadOperation>, AppendRequest<WriteOperation>, AppendResponse,
|
||||
WriteRequest<WriteOperation>, VoteRequest, VoteResponse>;
|
||||
|
||||
void Handle(ReceiveVariant &&message_variant, RequestId request_id, Address from_address) {
|
||||
// dispatch the message to a handler based on our role,
|
||||
// which can be specified in the Handle first argument,
|
||||
// or it can be `auto` if it's a handler for several roles
|
||||
// or messages.
|
||||
std::optional<Role> new_role = std::visit(
|
||||
[&](auto &&msg, auto &role) mutable {
|
||||
return Handle(role, std::forward<decltype(msg)>(msg), request_id, from_address);
|
||||
},
|
||||
std::forward<ReceiveVariant>(message_variant), role_);
|
||||
|
||||
// TODO(tyler) (M3) maybe replace std::visit with get_if for explicit prioritized matching, [[likely]] etc...
|
||||
if (new_role) {
|
||||
role_ = std::move(new_role).value();
|
||||
}
|
||||
}
|
||||
|
||||
void Run() {
|
||||
while (!io_.ShouldShutDown()) {
|
||||
const auto now = io_.Now();
|
||||
if (now >= next_cron_) {
|
||||
next_cron_ = Cron();
|
||||
}
|
||||
|
||||
const Duration receive_timeout = RandomTimeout(kMinimumReceiveTimeout, kMaximumReceiveTimeout);
|
||||
|
||||
auto request_result =
|
||||
io_.template ReceiveWithTimeout<ReadRequest<ReadOperation>, AppendRequest<WriteOperation>, AppendResponse,
|
||||
WriteRequest<WriteOperation>, VoteRequest, VoteResponse>(receive_timeout);
|
||||
if (request_result.HasError()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto request = std::move(request_result.GetValue());
|
||||
|
||||
Handle(std::move(request.message), request.request_id, request.from_address);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
// Raft paper - 5.3
|
||||
// When the entry has been safely replicated, the leader applies the
|
||||
// entry to its state machine and returns the result of that
|
||||
// execution to the client.
|
||||
//
|
||||
// "Safely replicated" is defined as being known to be present
|
||||
// on at least a majority of all peers (inclusive of the Leader).
|
||||
void BumpCommitIndexAndReplyToClients(Leader &leader) {
|
||||
auto confirmed_log_sizes = std::vector<LogSize>{};
|
||||
|
||||
// We include our own log size in the calculation of the log
|
||||
// confirmed log size that is present on at least a majority of all peers.
|
||||
confirmed_log_sizes.push_back(state_.log.size());
|
||||
|
||||
for (const auto &[addr, f] : leader.followers) {
|
||||
confirmed_log_sizes.push_back(f.confirmed_log_size);
|
||||
Log("Follower at port ", addr.last_known_port, " has confirmed log size of: ", f.confirmed_log_size);
|
||||
}
|
||||
|
||||
// reverse sort from highest to lowest (using std::ranges::greater)
|
||||
std::ranges::sort(confirmed_log_sizes, std::ranges::greater());
|
||||
|
||||
// This is a particularly correctness-critical calculation because it
|
||||
// determines the committed log size that will be broadcast in
|
||||
// the next AppendRequest.
|
||||
//
|
||||
// If the following sizes are recorded for clusters of different numbers of peers,
|
||||
// these are the expected sizes that are considered to have reached consensus:
|
||||
//
|
||||
// state | expected value | (confirmed_log_sizes.size() / 2)
|
||||
// [1] 1 (1 / 2) => 0
|
||||
// [2, 1] 1 (2 / 2) => 1
|
||||
// [3, 2, 1] 2 (3 / 2) => 1
|
||||
// [4, 3, 2, 1] 2 (4 / 2) => 2
|
||||
// [5, 4, 3, 2, 1] 3 (5 / 2) => 2
|
||||
const size_t majority_index = confirmed_log_sizes.size() / 2;
|
||||
const LogSize new_committed_log_size = confirmed_log_sizes[majority_index];
|
||||
|
||||
// We never go backwards in history.
|
||||
MG_ASSERT(state_.committed_log_size <= new_committed_log_size,
|
||||
"as a Leader, we have previously set our committed_log_size to {}, but our Followers have a majority "
|
||||
"committed_log_size of {}",
|
||||
state_.committed_log_size, new_committed_log_size);
|
||||
|
||||
state_.committed_log_size = new_committed_log_size;
|
||||
|
||||
// For each size between the old size and the new one (inclusive),
|
||||
// Apply that log's WriteOperation to our replicated_state_,
|
||||
// and use the specific return value of the ReplicatedState::Apply
|
||||
// method (WriteResponseValue) to respond to the requester.
|
||||
for (; state_.applied_size < state_.committed_log_size; state_.applied_size++) {
|
||||
const LogIndex apply_index = state_.applied_size;
|
||||
const auto &write_request = state_.log[apply_index].second;
|
||||
const WriteResponseValue write_return = replicated_state_.Apply(write_request);
|
||||
|
||||
if (leader.pending_client_requests.contains(apply_index)) {
|
||||
const PendingClientRequest client_request = std::move(leader.pending_client_requests.at(apply_index));
|
||||
leader.pending_client_requests.erase(apply_index);
|
||||
|
||||
const WriteResponse<WriteResponseValue> resp{
|
||||
.success = true,
|
||||
.write_return = std::move(write_return),
|
||||
.raft_index = apply_index,
|
||||
};
|
||||
|
||||
io_.Send(client_request.address, client_request.request_id, std::move(resp));
|
||||
}
|
||||
}
|
||||
|
||||
Log("committed_log_size is now ", state_.committed_log_size);
|
||||
}
|
||||
|
||||
// Raft paper - 5.1
|
||||
// AppendEntries RPCs are initiated by leaders to replicate log entries and to provide a form of heartbeat
|
||||
void BroadcastAppendEntries(std::map<Address, FollowerTracker> &followers) {
|
||||
for (auto &[address, follower] : followers) {
|
||||
const LogIndex next_index = follower.next_index;
|
||||
|
||||
const auto missing = state_.log.size() - next_index;
|
||||
const auto batch_size = std::min(missing, kMaximumAppendBatchSize);
|
||||
const auto start_index = next_index;
|
||||
const auto end_index = start_index + batch_size;
|
||||
|
||||
// advance follower's next index
|
||||
follower.next_index += batch_size;
|
||||
|
||||
std::vector<std::pair<Term, WriteOperation>> entries;
|
||||
|
||||
entries.insert(entries.begin(), state_.log.begin() + start_index, state_.log.begin() + end_index);
|
||||
|
||||
const Term previous_term_from_index = PreviousTermFromIndex(start_index);
|
||||
|
||||
Log("sending ", entries.size(), " entries to Follower ", address.last_known_port,
|
||||
" which are above its next_index of ", next_index);
|
||||
|
||||
AppendRequest<WriteOperation> ar{
|
||||
.term = state_.term,
|
||||
.batch_start_log_index = start_index,
|
||||
.last_log_term = previous_term_from_index,
|
||||
.entries = std::move(entries),
|
||||
.leader_commit = state_.committed_log_size,
|
||||
};
|
||||
|
||||
// request_id not necessary to set because it's not a Future-backed Request.
|
||||
static constexpr RequestId request_id = 0;
|
||||
|
||||
io_.Send(address, request_id, std::move(ar));
|
||||
}
|
||||
}
|
||||
|
||||
// Raft paper - 5.2
|
||||
// Raft uses randomized election timeouts to ensure that split votes are rare and that they are resolved quickly
|
||||
Duration RandomTimeout(Duration min, Duration max) {
|
||||
std::uniform_int_distribution time_distrib(min.count(), max.count());
|
||||
|
||||
const auto rand_micros = io_.Rand(time_distrib);
|
||||
|
||||
return Duration{rand_micros};
|
||||
}
|
||||
|
||||
Duration RandomTimeout(int min_micros, int max_micros) {
|
||||
std::uniform_int_distribution time_distrib(min_micros, max_micros);
|
||||
|
||||
const int rand_micros = io_.Rand(time_distrib);
|
||||
|
||||
return std::chrono::microseconds{rand_micros};
|
||||
}
|
||||
|
||||
Term PreviousTermFromIndex(LogIndex index) const {
|
||||
if (index == 0 || state_.log.size() + 1 <= index) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto &[term, data] = state_.log.at(index - 1);
|
||||
return term;
|
||||
}
|
||||
|
||||
Term CommittedLogTerm() {
|
||||
MG_ASSERT(state_.log.size() >= state_.committed_log_size);
|
||||
if (state_.log.empty() || state_.committed_log_size == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto &[term, data] = state_.log.at(state_.committed_log_size - 1);
|
||||
return term;
|
||||
}
|
||||
|
||||
Term LastLogTerm() const {
|
||||
if (state_.log.empty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto &[term, data] = state_.log.back();
|
||||
return term;
|
||||
}
|
||||
|
||||
template <typename... Ts>
|
||||
void Log(Ts &&...args) {
|
||||
const Time now = io_.Now();
|
||||
const auto micros = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
|
||||
const Term term = state_.term;
|
||||
const std::string role_string = std::visit([&](const auto &role) { return role.ToString(); }, role_);
|
||||
|
||||
std::ostringstream out;
|
||||
|
||||
out << '\t' << static_cast<int>(micros) << "\t" << term << "\t" << io_.GetAddress().last_known_port;
|
||||
|
||||
out << role_string;
|
||||
|
||||
(out << ... << args);
|
||||
|
||||
spdlog::info(out.str());
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
/// Raft-related Cron methods
|
||||
///
|
||||
/// Cron + std::visit is how events are dispatched
|
||||
/// to certain code based on Raft role.
|
||||
///
|
||||
/// Cron(role) takes as the first argument a reference to its
|
||||
/// role, and as the second argument, the message that has
|
||||
/// been received.
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
// Raft paper - 5.2
|
||||
// Candidates keep sending Vote to peers until:
|
||||
// 1. receiving Append with a higher term (become Follower)
|
||||
// 2. receiving Vote with a higher term (become a Follower)
|
||||
// 3. receiving a quorum of responses to our last batch of Vote (become a Leader)
|
||||
std::optional<Role> Cron(Candidate &candidate) {
|
||||
const auto now = io_.Now();
|
||||
const Duration election_timeout = RandomTimeout(kMinimumElectionTimeout, kMaximumElectionTimeout);
|
||||
const auto election_timeout_us = std::chrono::duration_cast<std::chrono::milliseconds>(election_timeout).count();
|
||||
|
||||
if (now - candidate.election_began > election_timeout) {
|
||||
state_.term++;
|
||||
Log("becoming Candidate for term ", state_.term, " after leader timeout of ", election_timeout_us,
|
||||
"ms elapsed since last election attempt");
|
||||
|
||||
const VoteRequest request{
|
||||
.term = state_.term,
|
||||
.log_size = state_.log.size(),
|
||||
.last_log_term = LastLogTerm(),
|
||||
};
|
||||
|
||||
auto outstanding_votes = std::set<Address>();
|
||||
|
||||
for (const auto &peer : peers_) {
|
||||
// request_id not necessary to set because it's not a Future-backed Request.
|
||||
static constexpr auto request_id = 0;
|
||||
io_.template Send<VoteRequest>(peer, request_id, request);
|
||||
outstanding_votes.insert(peer);
|
||||
}
|
||||
|
||||
return Candidate{
|
||||
.successful_votes = std::map<Address, LogIndex>(),
|
||||
.election_began = now,
|
||||
.outstanding_votes = outstanding_votes,
|
||||
};
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Raft paper - 5.2
|
||||
// Followers become candidates if we haven't heard from the leader
|
||||
// after a randomized timeout.
|
||||
std::optional<Role> Cron(Follower &follower) {
|
||||
const auto now = io_.Now();
|
||||
const auto time_since_last_append_entries = now - follower.last_received_append_entries_timestamp;
|
||||
|
||||
// randomized follower timeout
|
||||
const Duration election_timeout = RandomTimeout(kMinimumElectionTimeout, kMaximumElectionTimeout);
|
||||
|
||||
if (time_since_last_append_entries > election_timeout) {
|
||||
// become a Candidate if we haven't heard from the Leader after this timeout
|
||||
return Candidate{};
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Leaders (re)send AppendRequest to followers.
|
||||
std::optional<Role> Cron(Leader &leader) {
|
||||
const Time now = io_.Now();
|
||||
const Duration broadcast_timeout = RandomTimeout(kMinimumBroadcastTimeout, kMaximumBroadcastTimeout);
|
||||
|
||||
if (now - leader.last_broadcast > broadcast_timeout) {
|
||||
BroadcastAppendEntries(leader.followers);
|
||||
leader.last_broadcast = now;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
/// Raft-related Handle methods
|
||||
///
|
||||
/// Handle + std::visit is how events are dispatched
|
||||
/// to certain code based on Raft role.
|
||||
///
|
||||
/// Handle(role, message, ...)
|
||||
/// takes as the first argument a reference
|
||||
/// to its role, and as the second argument, the
|
||||
/// message that has been received.
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
// all roles can receive Vote and possibly become a follower
|
||||
template <AllRoles ALL>
|
||||
std::optional<Role> Handle(ALL & /* variable */, VoteRequest &&req, RequestId request_id, Address from_address) {
|
||||
Log("received VoteRequest from ", from_address.last_known_port, " with term ", req.term);
|
||||
const bool last_log_term_dominates = req.last_log_term >= LastLogTerm();
|
||||
const bool term_dominates = req.term > state_.term;
|
||||
const bool log_size_dominates = req.log_size >= state_.log.size();
|
||||
const bool new_leader = last_log_term_dominates && term_dominates && log_size_dominates;
|
||||
|
||||
if (new_leader) {
|
||||
MG_ASSERT(req.term > state_.term);
|
||||
MG_ASSERT(std::max(req.term, state_.term) == req.term);
|
||||
}
|
||||
|
||||
const VoteResponse res{
|
||||
.term = std::max(req.term, state_.term),
|
||||
.committed_log_size = state_.committed_log_size,
|
||||
.vote_granted = new_leader,
|
||||
};
|
||||
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
if (new_leader) {
|
||||
// become a follower
|
||||
state_.term = req.term;
|
||||
return Follower{
|
||||
.last_received_append_entries_timestamp = io_.Now(),
|
||||
.leader_address = from_address,
|
||||
};
|
||||
}
|
||||
|
||||
if (term_dominates) {
|
||||
Log("received a vote from an inferior candidate. Becoming Candidate");
|
||||
state_.term = std::max(state_.term, req.term) + 1;
|
||||
return Candidate{};
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Role> Handle(Candidate &candidate, VoteResponse &&res, RequestId /* variable */, Address from_address) {
|
||||
Log("received VoteResponse");
|
||||
|
||||
if (!res.vote_granted || res.term != state_.term) {
|
||||
Log("received unsuccessful VoteResponse from term ", res.term, " when our candidacy term is ", state_.term);
|
||||
// we received a delayed VoteResponse from the past, which has to do with an election that is
|
||||
// no longer valid. We can simply drop this.
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
MG_ASSERT(candidate.outstanding_votes.contains(from_address),
|
||||
"Received unexpected VoteResponse from server not present in Candidate's outstanding_votes!");
|
||||
candidate.outstanding_votes.erase(from_address);
|
||||
|
||||
MG_ASSERT(!candidate.successful_votes.contains(from_address),
|
||||
"Received unexpected VoteResponse from server already in Candidate's successful_votes!");
|
||||
candidate.successful_votes.insert({from_address, res.committed_log_size});
|
||||
|
||||
if (candidate.successful_votes.size() >= candidate.outstanding_votes.size()) {
|
||||
std::map<Address, FollowerTracker> followers{};
|
||||
|
||||
for (const auto &[address, committed_log_size] : candidate.successful_votes) {
|
||||
FollowerTracker follower{
|
||||
.next_index = committed_log_size,
|
||||
.confirmed_log_size = committed_log_size,
|
||||
};
|
||||
followers.insert({address, follower});
|
||||
}
|
||||
for (const auto &address : candidate.outstanding_votes) {
|
||||
FollowerTracker follower{
|
||||
.next_index = state_.log.size(),
|
||||
.confirmed_log_size = 0,
|
||||
};
|
||||
followers.insert({address, follower});
|
||||
}
|
||||
|
||||
Log("becoming Leader at term ", state_.term);
|
||||
|
||||
BroadcastAppendEntries(followers);
|
||||
|
||||
return Leader{
|
||||
.followers = std::move(followers),
|
||||
.pending_client_requests = std::unordered_map<LogIndex, PendingClientRequest>(),
|
||||
};
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <LeaderOrFollower LOF>
|
||||
std::optional<Role> Handle(LOF & /* variable */, VoteResponse && /* variable */, RequestId /* variable */,
|
||||
Address /* variable */) {
|
||||
Log("non-Candidate received VoteResponse");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <AllRoles ALL>
|
||||
std::optional<Role> Handle(ALL &role, AppendRequest<WriteOperation> &&req, RequestId request_id,
|
||||
Address from_address) {
|
||||
// log size starts out as state_.committed_log_size and only if everything is successful do we
|
||||
// switch it to the log length.
|
||||
AppendResponse res{
|
||||
.success = false,
|
||||
.term = state_.term,
|
||||
.last_log_term = CommittedLogTerm(),
|
||||
.log_size = state_.log.size(),
|
||||
};
|
||||
|
||||
if constexpr (std::is_same<ALL, Leader>()) {
|
||||
MG_ASSERT(req.term != state_.term, "Multiple leaders are acting under the term ", req.term);
|
||||
}
|
||||
|
||||
const bool is_candidate = std::is_same<ALL, Candidate>();
|
||||
const bool is_failed_competitor = is_candidate && req.term == state_.term;
|
||||
const Time now = io_.Now();
|
||||
|
||||
// Raft paper - 5.2
|
||||
// While waiting for votes, a candidate may receive an
|
||||
// AppendEntries RPC from another server claiming to be leader. If
|
||||
// the leader’s term (included in its RPC) is at least as large as
|
||||
// the candidate’s current term, then the candidate recognizes the
|
||||
// leader as legitimate and returns to follower state.
|
||||
if (req.term > state_.term || is_failed_competitor) {
|
||||
// become follower of this leader, reply with our log status
|
||||
state_.term = req.term;
|
||||
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
Log("becoming Follower of Leader ", from_address.last_known_port, " at term ", req.term);
|
||||
return Follower{
|
||||
.last_received_append_entries_timestamp = now,
|
||||
.leader_address = from_address,
|
||||
};
|
||||
}
|
||||
|
||||
if (req.term < state_.term) {
|
||||
// nack this request from an old leader
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// at this point, we're dealing with our own leader
|
||||
if constexpr (std::is_same<ALL, Follower>()) {
|
||||
// small specialization for when we're already a Follower
|
||||
MG_ASSERT(role.leader_address == from_address, "Multiple Leaders are acting under the same term number!");
|
||||
role.last_received_append_entries_timestamp = now;
|
||||
} else {
|
||||
Log("Somehow entered Follower-specific logic as a non-Follower");
|
||||
MG_ASSERT(false, "Somehow entered Follower-specific logic as a non-Follower");
|
||||
}
|
||||
|
||||
// Handle steady-state conditions.
|
||||
if (req.batch_start_log_index != state_.log.size()) {
|
||||
Log("req.batch_start_log_index of ", req.batch_start_log_index, " does not match our log size of ",
|
||||
state_.log.size());
|
||||
} else if (req.last_log_term != LastLogTerm()) {
|
||||
Log("req.last_log_term differs from our leader term at that slot, expected: ", LastLogTerm(), " but got ",
|
||||
req.last_log_term);
|
||||
} else {
|
||||
// happy path - Apply log
|
||||
Log("applying batch of ", req.entries.size(), " entries to our log starting at index ",
|
||||
req.batch_start_log_index);
|
||||
|
||||
const auto resize_length = req.batch_start_log_index;
|
||||
|
||||
MG_ASSERT(resize_length >= state_.committed_log_size,
|
||||
"Applied history from Leader which goes back in time from our commit_index");
|
||||
|
||||
// possibly chop-off stuff that was replaced by
|
||||
// things with different terms (we got data that
|
||||
// hasn't reached consensus yet, which is normal)
|
||||
state_.log.resize(resize_length);
|
||||
|
||||
if (req.entries.size() > 0) {
|
||||
auto &[first_term, op] = req.entries.at(0);
|
||||
MG_ASSERT(LastLogTerm() <= first_term);
|
||||
}
|
||||
|
||||
state_.log.insert(state_.log.end(), std::make_move_iterator(req.entries.begin()),
|
||||
std::make_move_iterator(req.entries.end()));
|
||||
|
||||
MG_ASSERT(req.leader_commit >= state_.committed_log_size);
|
||||
state_.committed_log_size = std::min(req.leader_commit, state_.log.size());
|
||||
|
||||
for (; state_.applied_size < state_.committed_log_size; state_.applied_size++) {
|
||||
const auto &write_request = state_.log[state_.applied_size].second;
|
||||
replicated_state_.Apply(write_request);
|
||||
}
|
||||
|
||||
res.success = true;
|
||||
}
|
||||
|
||||
res.last_log_term = LastLogTerm();
|
||||
res.log_size = state_.log.size();
|
||||
|
||||
Log("returning log_size of ", res.log_size);
|
||||
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Role> Handle(Leader &leader, AppendResponse &&res, RequestId /* variable */, Address from_address) {
|
||||
if (res.term != state_.term) {
|
||||
Log("received AppendResponse related to a previous term when we (presumably) were the leader");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// TODO(tyler) when we have dynamic membership, this assert will become incorrect, but we should
|
||||
// keep it in-place until then because it has bug finding value.
|
||||
MG_ASSERT(leader.followers.contains(from_address), "received AppendResponse from unknown Follower");
|
||||
|
||||
// at this point, we know the term matches and we know this Follower
|
||||
|
||||
FollowerTracker &follower = leader.followers.at(from_address);
|
||||
|
||||
if (res.success) {
|
||||
Log("got successful AppendResponse from ", from_address.last_known_port, " with log_size of ", res.log_size);
|
||||
follower.next_index = std::max(follower.next_index, res.log_size);
|
||||
} else {
|
||||
Log("got unsuccessful AppendResponse from ", from_address.last_known_port, " with log_size of ", res.log_size);
|
||||
follower.next_index = res.log_size;
|
||||
}
|
||||
|
||||
follower.confirmed_log_size = std::max(follower.confirmed_log_size, res.log_size);
|
||||
|
||||
BumpCommitIndexAndReplyToClients(leader);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
template <FollowerOrCandidate FOC>
|
||||
std::optional<Role> Handle(FOC & /* variable */, AppendResponse && /* variable */, RequestId /* variable */,
|
||||
Address /* variable */) {
|
||||
// we used to be the leader, and are getting old delayed responses
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////
|
||||
/// RSM-related handle methods
|
||||
/////////////////////////////////////////////////////////////
|
||||
|
||||
// Leaders are able to immediately respond to the requester (with a ReadResponseValue) applied to the ReplicatedState
|
||||
std::optional<Role> Handle(Leader & /* variable */, ReadRequest<ReadOperation> &&req, RequestId request_id,
|
||||
Address from_address) {
|
||||
Log("handling ReadOperation");
|
||||
ReadOperation read_operation = req.operation;
|
||||
|
||||
ReadResponseValue read_return = replicated_state_.Read(read_operation);
|
||||
|
||||
const ReadResponse<ReadResponseValue> resp{
|
||||
.success = true,
|
||||
.read_return = std::move(read_return),
|
||||
.retry_leader = std::nullopt,
|
||||
};
|
||||
|
||||
io_.Send(from_address, request_id, resp);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Candidates should respond with a failure, similar to the Candidate + WriteRequest failure below
|
||||
std::optional<Role> Handle(Candidate & /* variable */, ReadRequest<ReadOperation> && /* variable */,
|
||||
RequestId request_id, Address from_address) {
|
||||
Log("received ReadOperation - not redirecting because no Leader is known");
|
||||
const ReadResponse<ReadResponseValue> res{
|
||||
.success = false,
|
||||
};
|
||||
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
Cron();
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Followers should respond with a redirection, similar to the Follower + WriteRequest response below
|
||||
std::optional<Role> Handle(Follower &follower, ReadRequest<ReadOperation> && /* variable */, RequestId request_id,
|
||||
Address from_address) {
|
||||
Log("redirecting client to known Leader with port ", follower.leader_address.last_known_port);
|
||||
|
||||
const ReadResponse<ReadResponseValue> res{
|
||||
.success = false,
|
||||
.retry_leader = follower.leader_address,
|
||||
};
|
||||
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// Raft paper - 8
|
||||
// When a client first starts up, it connects to a randomly chosen
|
||||
// server. If the client’s first choice is not the leader, that
|
||||
// server will reject the client’s request and supply information
|
||||
// about the most recent leader it has heard from.
|
||||
std::optional<Role> Handle(Follower &follower, WriteRequest<WriteOperation> && /* variable */, RequestId request_id,
|
||||
Address from_address) {
|
||||
Log("redirecting client to known Leader with port ", follower.leader_address.last_known_port);
|
||||
|
||||
const WriteResponse<WriteResponseValue> res{
|
||||
.success = false,
|
||||
.retry_leader = follower.leader_address,
|
||||
};
|
||||
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<Role> Handle(Candidate & /* variable */, WriteRequest<WriteOperation> && /* variable */,
|
||||
RequestId request_id, Address from_address) {
|
||||
Log("received WriteRequest - not redirecting because no Leader is known");
|
||||
|
||||
const WriteResponse<WriteResponseValue> res{
|
||||
.success = false,
|
||||
};
|
||||
|
||||
io_.Send(from_address, request_id, res);
|
||||
|
||||
Cron();
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// only leaders actually handle replication requests from clients
|
||||
std::optional<Role> Handle(Leader &leader, WriteRequest<WriteOperation> &&req, RequestId request_id,
|
||||
Address from_address) {
|
||||
Log("handling WriteRequest");
|
||||
|
||||
// we are the leader. add item to log and send Append to peers
|
||||
MG_ASSERT(state_.term >= LastLogTerm());
|
||||
state_.log.emplace_back(std::pair(state_.term, std::move(req.operation)));
|
||||
|
||||
LogIndex log_index = state_.log.size() - 1;
|
||||
|
||||
PendingClientRequest pcr{
|
||||
.request_id = request_id,
|
||||
.address = from_address,
|
||||
.received_at = io_.Now(),
|
||||
};
|
||||
|
||||
leader.pending_client_requests.emplace(log_index, pcr);
|
||||
|
||||
if (peers_.empty()) {
|
||||
BumpCommitIndexAndReplyToClients(leader);
|
||||
} else {
|
||||
BroadcastAppendEntries(leader.followers);
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
};
|
||||
|
||||
}; // namespace memgraph::io::rsm
|
||||
136
src/io/rsm/rsm_client.hpp
Normal file
136
src/io/rsm/rsm_client.hpp
Normal file
@@ -0,0 +1,136 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include "io/address.hpp"
|
||||
#include "io/rsm/raft.hpp"
|
||||
#include "utils/result.hpp"
|
||||
|
||||
namespace memgraph::io::rsm {
|
||||
|
||||
using memgraph::io::Address;
|
||||
using memgraph::io::Duration;
|
||||
using memgraph::io::ResponseEnvelope;
|
||||
using memgraph::io::ResponseFuture;
|
||||
using memgraph::io::ResponseResult;
|
||||
using memgraph::io::Time;
|
||||
using memgraph::io::TimedOut;
|
||||
using memgraph::io::rsm::ReadRequest;
|
||||
using memgraph::io::rsm::ReadResponse;
|
||||
using memgraph::io::rsm::WriteRequest;
|
||||
using memgraph::io::rsm::WriteResponse;
|
||||
using memgraph::utils::BasicResult;
|
||||
|
||||
template <typename IoImpl, typename WriteRequestT, typename WriteResponseT, typename ReadRequestT,
|
||||
typename ReadResponseT>
|
||||
class RsmClient {
|
||||
using ServerPool = std::vector<Address>;
|
||||
|
||||
Io<IoImpl> io_;
|
||||
Address leader_;
|
||||
ServerPool server_addrs_;
|
||||
|
||||
template <typename ResponseT>
|
||||
void PossiblyRedirectLeader(const ResponseT &response) {
|
||||
if (response.retry_leader) {
|
||||
MG_ASSERT(!response.success, "retry_leader should never be set for successful responses");
|
||||
leader_ = response.retry_leader.value();
|
||||
spdlog::debug("client redirected to leader server {}", leader_.ToString());
|
||||
} else if (!response.success) {
|
||||
std::uniform_int_distribution<size_t> addr_distrib(0, (server_addrs_.size() - 1));
|
||||
size_t addr_index = io_.Rand(addr_distrib);
|
||||
leader_ = server_addrs_[addr_index];
|
||||
|
||||
spdlog::debug(
|
||||
"client NOT redirected to leader server despite our success failing to be processed (it probably was sent to "
|
||||
"a RSM Candidate) trying a random one at index {} with address {}",
|
||||
addr_index, leader_.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
RsmClient(Io<IoImpl> io, Address leader, ServerPool server_addrs)
|
||||
: io_{io}, leader_{leader}, server_addrs_{server_addrs} {}
|
||||
|
||||
RsmClient() = delete;
|
||||
|
||||
BasicResult<TimedOut, WriteResponseT> SendWriteRequest(WriteRequestT req) {
|
||||
WriteRequest<WriteRequestT> client_req;
|
||||
client_req.operation = req;
|
||||
|
||||
const Duration overall_timeout = io_.GetDefaultTimeout();
|
||||
const Time before = io_.Now();
|
||||
|
||||
do {
|
||||
spdlog::debug("client sending WriteRequest to Leader {}", leader_.ToString());
|
||||
ResponseFuture<WriteResponse<WriteResponseT>> response_future =
|
||||
io_.template Request<WriteRequest<WriteRequestT>, WriteResponse<WriteResponseT>>(leader_, client_req);
|
||||
ResponseResult<WriteResponse<WriteResponseT>> response_result = std::move(response_future).Wait();
|
||||
|
||||
if (response_result.HasError()) {
|
||||
spdlog::debug("client timed out while trying to communicate with leader server {}", leader_.ToString());
|
||||
return response_result.GetError();
|
||||
}
|
||||
|
||||
ResponseEnvelope<WriteResponse<WriteResponseT>> &&response_envelope = std::move(response_result.GetValue());
|
||||
WriteResponse<WriteResponseT> &&write_response = std::move(response_envelope.message);
|
||||
|
||||
if (write_response.success) {
|
||||
return std::move(write_response.write_return);
|
||||
}
|
||||
|
||||
PossiblyRedirectLeader(write_response);
|
||||
} while (io_.Now() < before + overall_timeout);
|
||||
|
||||
return TimedOut{};
|
||||
}
|
||||
|
||||
BasicResult<TimedOut, ReadResponseT> SendReadRequest(ReadRequestT req) {
|
||||
ReadRequest<ReadRequestT> read_req;
|
||||
read_req.operation = req;
|
||||
|
||||
const Duration overall_timeout = io_.GetDefaultTimeout();
|
||||
const Time before = io_.Now();
|
||||
|
||||
do {
|
||||
spdlog::debug("client sending ReadRequest to Leader {}", leader_.ToString());
|
||||
|
||||
ResponseFuture<ReadResponse<ReadResponseT>> get_response_future =
|
||||
io_.template Request<ReadRequest<ReadRequestT>, ReadResponse<ReadResponseT>>(leader_, read_req);
|
||||
|
||||
// receive response
|
||||
ResponseResult<ReadResponse<ReadResponseT>> get_response_result = std::move(get_response_future).Wait();
|
||||
|
||||
if (get_response_result.HasError()) {
|
||||
spdlog::debug("client timed out while trying to communicate with leader server {}", leader_.ToString());
|
||||
return get_response_result.GetError();
|
||||
}
|
||||
|
||||
ResponseEnvelope<ReadResponse<ReadResponseT>> &&get_response_envelope = std::move(get_response_result.GetValue());
|
||||
ReadResponse<ReadResponseT> &&read_get_response = std::move(get_response_envelope.message);
|
||||
|
||||
if (read_get_response.success) {
|
||||
return std::move(read_get_response.read_return);
|
||||
}
|
||||
|
||||
PossiblyRedirectLeader(read_get_response);
|
||||
} while (io_.Now() < before + overall_timeout);
|
||||
|
||||
return TimedOut{};
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace memgraph::io::rsm
|
||||
155
src/io/rsm/shard_rsm.hpp
Normal file
155
src/io/rsm/shard_rsm.hpp
Normal file
@@ -0,0 +1,155 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
/// The ShardRsm is a simple in-memory raft-backed kv store that can be used for simple testing
|
||||
/// and implementation of some query engine logic before storage engines are fully implemented.
|
||||
///
|
||||
/// To implement multiple read and write commands, change the StorageRead* and StorageWrite* requests
|
||||
/// and responses to a std::variant of the different options, and route them to specific handlers in
|
||||
/// the ShardRsm's Read and Apply methods. Remember that Read is called immediately when the Raft
|
||||
/// leader receives the request, and does not replicate anything over Raft. Apply is called only
|
||||
/// AFTER the StorageWriteRequest is replicated to a majority of Raft peers, and the result of calling
|
||||
/// ShardRsm::Apply(StorageWriteRequest) is returned to the client that submitted the request.
|
||||
|
||||
#include <deque>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "coordinator/hybrid_logical_clock.hpp"
|
||||
#include "io/address.hpp"
|
||||
#include "io/rsm/raft.hpp"
|
||||
#include "storage/v3/id_types.hpp"
|
||||
#include "storage/v3/property_value.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
|
||||
namespace memgraph::io::rsm {
|
||||
|
||||
using memgraph::coordinator::Hlc;
|
||||
using memgraph::storage::v3::LabelId;
|
||||
using memgraph::storage::v3::PropertyValue;
|
||||
|
||||
using ShardRsmKey = std::vector<PropertyValue>;
|
||||
|
||||
struct StorageWriteRequest {
|
||||
LabelId label_id;
|
||||
Hlc transaction_id;
|
||||
ShardRsmKey key;
|
||||
std::optional<int> value;
|
||||
};
|
||||
|
||||
struct StorageWriteResponse {
|
||||
bool shard_rsm_success;
|
||||
std::optional<int> last_value;
|
||||
// Only has a value if the given shard does not contain the requested key
|
||||
std::optional<Hlc> latest_known_shard_map_version{std::nullopt};
|
||||
};
|
||||
|
||||
struct StorageReadRequest {
|
||||
LabelId label_id;
|
||||
Hlc transaction_id;
|
||||
ShardRsmKey key;
|
||||
};
|
||||
|
||||
struct StorageReadResponse {
|
||||
bool shard_rsm_success;
|
||||
std::optional<int> value;
|
||||
// Only has a value if the given shard does not contain the requested key
|
||||
std::optional<Hlc> latest_known_shard_map_version{std::nullopt};
|
||||
};
|
||||
|
||||
class ShardRsm {
|
||||
std::map<ShardRsmKey, int> state_;
|
||||
ShardRsmKey minimum_key_;
|
||||
std::optional<ShardRsmKey> maximum_key_{std::nullopt};
|
||||
Hlc shard_map_version_;
|
||||
|
||||
// The key is not located in this shard
|
||||
bool IsKeyInRange(const ShardRsmKey &key) const {
|
||||
if (maximum_key_) [[likely]] {
|
||||
return (key >= minimum_key_ && key <= maximum_key_);
|
||||
}
|
||||
return key >= minimum_key_;
|
||||
}
|
||||
|
||||
public:
|
||||
StorageReadResponse Read(StorageReadRequest request) const {
|
||||
StorageReadResponse ret;
|
||||
|
||||
if (!IsKeyInRange(request.key)) {
|
||||
ret.latest_known_shard_map_version = shard_map_version_;
|
||||
ret.shard_rsm_success = false;
|
||||
} else if (state_.contains(request.key)) {
|
||||
ret.value = state_.at(request.key);
|
||||
ret.shard_rsm_success = true;
|
||||
} else {
|
||||
ret.shard_rsm_success = false;
|
||||
ret.value = std::nullopt;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
StorageWriteResponse Apply(StorageWriteRequest request) {
|
||||
StorageWriteResponse ret;
|
||||
|
||||
// Key is outside the prohibited range
|
||||
if (!IsKeyInRange(request.key)) {
|
||||
ret.latest_known_shard_map_version = shard_map_version_;
|
||||
ret.shard_rsm_success = false;
|
||||
}
|
||||
// Key exist
|
||||
else if (state_.contains(request.key)) {
|
||||
auto &val = state_[request.key];
|
||||
|
||||
/*
|
||||
* Delete
|
||||
*/
|
||||
if (!request.value) {
|
||||
ret.shard_rsm_success = true;
|
||||
ret.last_value = val;
|
||||
state_.erase(state_.find(request.key));
|
||||
}
|
||||
|
||||
/*
|
||||
* Update
|
||||
*/
|
||||
// Does old_value match?
|
||||
if (request.value == val) {
|
||||
ret.last_value = val;
|
||||
ret.shard_rsm_success = true;
|
||||
|
||||
val = request.value.value();
|
||||
|
||||
} else {
|
||||
ret.last_value = val;
|
||||
ret.shard_rsm_success = false;
|
||||
}
|
||||
}
|
||||
/*
|
||||
* Create
|
||||
*/
|
||||
else {
|
||||
ret.last_value = std::nullopt;
|
||||
ret.shard_rsm_success = true;
|
||||
|
||||
state_.emplace(request.key, request.value.value());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace memgraph::io::rsm
|
||||
8
src/io/simulator/CMakeLists.txt
Normal file
8
src/io/simulator/CMakeLists.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
set(io_simulator_sources
|
||||
simulator_handle.cpp)
|
||||
|
||||
find_package(fmt REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_library(mg-io-simulator STATIC ${io_simulator_sources})
|
||||
target_link_libraries(mg-io-simulator stdc++fs Threads::Threads fmt::fmt mg-utils)
|
||||
51
src/io/simulator/simulator.hpp
Normal file
51
src/io/simulator/simulator.hpp
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <random>
|
||||
|
||||
#include "io/address.hpp"
|
||||
#include "io/simulator/simulator_config.hpp"
|
||||
#include "io/simulator/simulator_handle.hpp"
|
||||
#include "io/simulator/simulator_transport.hpp"
|
||||
|
||||
namespace memgraph::io::simulator {
|
||||
class Simulator {
|
||||
std::mt19937 rng_;
|
||||
std::shared_ptr<SimulatorHandle> simulator_handle_;
|
||||
uint16_t auto_port_ = 0;
|
||||
|
||||
public:
|
||||
explicit Simulator(SimulatorConfig config)
|
||||
: rng_(std::mt19937{config.rng_seed}), simulator_handle_{std::make_shared<SimulatorHandle>(config)} {}
|
||||
|
||||
void ShutDown() { simulator_handle_->ShutDown(); }
|
||||
|
||||
Io<SimulatorTransport> RegisterNew() {
|
||||
Address address = Address::TestAddress(auto_port_++);
|
||||
return Register(address);
|
||||
}
|
||||
|
||||
Io<SimulatorTransport> Register(Address address) {
|
||||
std::uniform_int_distribution<uint64_t> seed_distrib;
|
||||
uint64_t seed = seed_distrib(rng_);
|
||||
return Io{SimulatorTransport{simulator_handle_, address, seed}, address};
|
||||
}
|
||||
|
||||
void IncrementServerCountAndWaitForQuiescentState(Address address) {
|
||||
simulator_handle_->IncrementServerCountAndWaitForQuiescentState(address);
|
||||
}
|
||||
|
||||
SimulatorStats Stats() { return simulator_handle_->Stats(); }
|
||||
};
|
||||
}; // namespace memgraph::io::simulator
|
||||
30
src/io/simulator/simulator_config.hpp
Normal file
30
src/io/simulator/simulator_config.hpp
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
|
||||
#include "io/time.hpp"
|
||||
|
||||
namespace memgraph::io::simulator {
|
||||
|
||||
using memgraph::io::Time;
|
||||
|
||||
struct SimulatorConfig {
|
||||
uint8_t drop_percent = 0;
|
||||
bool perform_timeouts = false;
|
||||
bool scramble_messages = true;
|
||||
uint64_t rng_seed = 0;
|
||||
Time start_time = Time::min();
|
||||
Time abort_time = Time::max();
|
||||
};
|
||||
}; // namespace memgraph::io::simulator
|
||||
142
src/io/simulator/simulator_handle.cpp
Normal file
142
src/io/simulator/simulator_handle.cpp
Normal file
@@ -0,0 +1,142 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "io/simulator/simulator_handle.hpp"
|
||||
#include "io/address.hpp"
|
||||
#include "io/errors.hpp"
|
||||
#include "io/simulator/simulator_config.hpp"
|
||||
#include "io/simulator/simulator_stats.hpp"
|
||||
#include "io/time.hpp"
|
||||
#include "io/transport.hpp"
|
||||
|
||||
namespace memgraph::io::simulator {
|
||||
|
||||
using memgraph::io::Duration;
|
||||
using memgraph::io::Time;
|
||||
|
||||
void SimulatorHandle::ShutDown() {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
should_shut_down_ = true;
|
||||
cv_.notify_all();
|
||||
}
|
||||
|
||||
bool SimulatorHandle::ShouldShutDown() const {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
return should_shut_down_;
|
||||
}
|
||||
|
||||
void SimulatorHandle::IncrementServerCountAndWaitForQuiescentState(Address address) {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
server_addresses_.insert(address);
|
||||
|
||||
while (true) {
|
||||
const size_t blocked_servers = blocked_on_receive_.size();
|
||||
|
||||
const bool all_servers_blocked = blocked_servers == server_addresses_.size();
|
||||
|
||||
if (all_servers_blocked) {
|
||||
return;
|
||||
}
|
||||
|
||||
cv_.wait(lock);
|
||||
}
|
||||
}
|
||||
|
||||
bool SimulatorHandle::MaybeTickSimulator() {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
|
||||
const size_t blocked_servers = blocked_on_receive_.size();
|
||||
|
||||
if (blocked_servers < server_addresses_.size()) {
|
||||
// we only need to advance the simulator when all
|
||||
// servers have reached a quiescent state, blocked
|
||||
// on their own futures or receive methods.
|
||||
return false;
|
||||
}
|
||||
|
||||
stats_.simulator_ticks++;
|
||||
|
||||
cv_.notify_all();
|
||||
|
||||
TimeoutPromisesPastDeadline();
|
||||
|
||||
if (in_flight_.empty()) {
|
||||
// return early here because there are no messages to schedule
|
||||
|
||||
// We tick the clock forward when all servers are blocked but
|
||||
// there are no in-flight messages to schedule delivery of.
|
||||
std::poisson_distribution<> time_distrib(50);
|
||||
Duration clock_advance = std::chrono::microseconds{time_distrib(rng_)};
|
||||
cluster_wide_time_microseconds_ += clock_advance;
|
||||
|
||||
MG_ASSERT(cluster_wide_time_microseconds_ < config_.abort_time,
|
||||
"Cluster has executed beyond its configured abort_time, and something may be failing to make progress "
|
||||
"in an expected amount of time.");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config_.scramble_messages) {
|
||||
// scramble messages
|
||||
std::uniform_int_distribution<size_t> swap_distrib(0, in_flight_.size() - 1);
|
||||
const size_t swap_index = swap_distrib(rng_);
|
||||
std::swap(in_flight_[swap_index], in_flight_.back());
|
||||
}
|
||||
|
||||
auto [to_address, opaque_message] = std::move(in_flight_.back());
|
||||
in_flight_.pop_back();
|
||||
|
||||
std::uniform_int_distribution<int> drop_distrib(0, 99);
|
||||
const int drop_threshold = drop_distrib(rng_);
|
||||
const bool should_drop = drop_threshold < config_.drop_percent;
|
||||
|
||||
if (should_drop) {
|
||||
stats_.dropped_messages++;
|
||||
}
|
||||
|
||||
PromiseKey promise_key{.requester_address = to_address,
|
||||
.request_id = opaque_message.request_id,
|
||||
.replier_address = opaque_message.from_address};
|
||||
|
||||
if (promises_.contains(promise_key)) {
|
||||
// complete waiting promise if it's there
|
||||
DeadlineAndOpaquePromise dop = std::move(promises_.at(promise_key));
|
||||
promises_.erase(promise_key);
|
||||
|
||||
const bool normal_timeout = config_.perform_timeouts && (dop.deadline < cluster_wide_time_microseconds_);
|
||||
|
||||
if (should_drop || normal_timeout) {
|
||||
stats_.timed_out_requests++;
|
||||
dop.promise.TimeOut();
|
||||
} else {
|
||||
stats_.total_responses++;
|
||||
dop.promise.Fill(std::move(opaque_message));
|
||||
}
|
||||
} else if (should_drop) {
|
||||
// don't add it anywhere, let it drop
|
||||
} else {
|
||||
// add to can_receive_ if not
|
||||
const auto &[om_vec, inserted] = can_receive_.try_emplace(to_address, std::vector<OpaqueMessage>());
|
||||
om_vec->second.emplace_back(std::move(opaque_message));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Time SimulatorHandle::Now() const {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
return cluster_wide_time_microseconds_;
|
||||
}
|
||||
|
||||
SimulatorStats SimulatorHandle::Stats() {
|
||||
std::unique_lock<std::mutex> lock(mu_);
|
||||
return stats_;
|
||||
}
|
||||
} // namespace memgraph::io::simulator
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user