Compare commits
11 Commits
v2.8.0
...
replicate-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8719c20df | ||
|
|
de9aa1fd56 | ||
|
|
1c55f9c5bd | ||
|
|
c7fb5016d3 | ||
|
|
f9ce2a4be6 | ||
|
|
cf1a86ed13 | ||
|
|
7fb3f62703 | ||
|
|
cb4b71bdbd | ||
|
|
30ec570bb9 | ||
|
|
d917c3f0fd | ||
|
|
d842adbed3 |
@@ -1,6 +1,7 @@
|
||||
---
|
||||
BasedOnStyle: Google
|
||||
---
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
Standard: "c++20"
|
||||
UseTab: Never
|
||||
DerivePointerAlignment: false
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -10,5 +10,5 @@
|
||||
|
||||
|
||||
To keep docs changelog up to date, one more thing to do:
|
||||
- [ ] Write a release note here
|
||||
- [ ] Write a release note here, including added/changed clauses
|
||||
- [ ] Tag someone from docs team in the comments
|
||||
|
||||
17
.github/workflows/diff.yaml
vendored
17
.github/workflows/diff.yaml
vendored
@@ -196,22 +196,7 @@ jobs:
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd tests/integration
|
||||
for name in *; do
|
||||
if [ ! -d $name ]; then continue; fi
|
||||
pushd $name >/dev/null
|
||||
echo "Running: $name"
|
||||
if [ -x prepare.sh ]; then
|
||||
./prepare.sh
|
||||
fi
|
||||
if [ -x runner.py ]; then
|
||||
./runner.py
|
||||
elif [ -x runner.sh ]; then
|
||||
./runner.sh
|
||||
fi
|
||||
echo
|
||||
popd >/dev/null
|
||||
done
|
||||
tests/integration/run.sh
|
||||
|
||||
- name: Run cppcheck and clang-format
|
||||
run: |
|
||||
|
||||
17
.github/workflows/release_centos8.yaml
vendored
17
.github/workflows/release_centos8.yaml
vendored
@@ -146,22 +146,7 @@ jobs:
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd tests/integration
|
||||
for name in *; do
|
||||
if [ ! -d $name ]; then continue; fi
|
||||
pushd $name >/dev/null
|
||||
echo "Running: $name"
|
||||
if [ -x prepare.sh ]; then
|
||||
./prepare.sh
|
||||
fi
|
||||
if [ -x runner.py ]; then
|
||||
./runner.py
|
||||
elif [ -x runner.sh ]; then
|
||||
./runner.sh
|
||||
fi
|
||||
echo
|
||||
popd >/dev/null
|
||||
done
|
||||
tests/integration/run.sh
|
||||
|
||||
- name: Run cppcheck and clang-format
|
||||
run: |
|
||||
|
||||
17
.github/workflows/release_debian10.yaml
vendored
17
.github/workflows/release_debian10.yaml
vendored
@@ -146,22 +146,7 @@ jobs:
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd tests/integration
|
||||
for name in *; do
|
||||
if [ ! -d $name ]; then continue; fi
|
||||
pushd $name >/dev/null
|
||||
echo "Running: $name"
|
||||
if [ -x prepare.sh ]; then
|
||||
./prepare.sh
|
||||
fi
|
||||
if [ -x runner.py ]; then
|
||||
./runner.py
|
||||
elif [ -x runner.sh ]; then
|
||||
./runner.sh
|
||||
fi
|
||||
echo
|
||||
popd >/dev/null
|
||||
done
|
||||
tests/integration/run.sh
|
||||
|
||||
- name: Run cppcheck and clang-format
|
||||
run: |
|
||||
|
||||
17
.github/workflows/release_ubuntu2004.yaml
vendored
17
.github/workflows/release_ubuntu2004.yaml
vendored
@@ -146,22 +146,7 @@ jobs:
|
||||
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd tests/integration
|
||||
for name in *; do
|
||||
if [ ! -d $name ]; then continue; fi
|
||||
pushd $name >/dev/null
|
||||
echo "Running: $name"
|
||||
if [ -x prepare.sh ]; then
|
||||
./prepare.sh
|
||||
fi
|
||||
if [ -x runner.py ]; then
|
||||
./runner.py
|
||||
elif [ -x runner.sh ]; then
|
||||
./runner.sh
|
||||
fi
|
||||
echo
|
||||
popd >/dev/null
|
||||
done
|
||||
tests/integration/run.sh
|
||||
|
||||
- name: Run cppcheck and clang-format
|
||||
run: |
|
||||
|
||||
@@ -3,6 +3,7 @@ repos:
|
||||
rev: v4.4.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
args: [--allow-multiple-documents]
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- repo: https://github.com/psf/black
|
||||
|
||||
1
libs/.gitignore
vendored
1
libs/.gitignore
vendored
@@ -6,3 +6,4 @@
|
||||
!__main.cpp
|
||||
!pulsar.patch
|
||||
!antlr4.10.1.patch
|
||||
!rocksdb8.1.1.patch
|
||||
|
||||
13
libs/rocksdb8.1.1.patch
Normal file
13
libs/rocksdb8.1.1.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git a/CMakeLists.txt b/CMakeLists.txt
|
||||
index 598c728..816c705 100644
|
||||
--- a/CMakeLists.txt
|
||||
+++ b/CMakeLists.txt
|
||||
@@ -1242,7 +1242,7 @@ if(NOT WIN32 OR ROCKSDB_INSTALL_ON_WINDOWS)
|
||||
|
||||
if(ROCKSDB_BUILD_SHARED)
|
||||
install(
|
||||
- TARGETS ${ROCKSDB_SHARED_LIB}
|
||||
+ TARGETS ${ROCKSDB_SHARED_LIB} OPTIONAL
|
||||
EXPORT RocksDBTargets
|
||||
COMPONENT runtime
|
||||
ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}"
|
||||
@@ -192,10 +192,10 @@ cd json
|
||||
file_get_try_double "${primary_urls[nlohmann]}" "${secondary_urls[nlohmann]}"
|
||||
cd ..
|
||||
|
||||
rocksdb_tag="v6.14.6" # (2020-10-14)
|
||||
rocksdb_tag="v8.1.1" # (2023-04-21)
|
||||
repo_clone_try_double "${primary_urls[rocksdb]}" "${secondary_urls[rocksdb]}" "rocksdb" "$rocksdb_tag" true
|
||||
pushd rocksdb
|
||||
git apply ../rocksdb.patch
|
||||
git apply ../rocksdb8.1.1.patch
|
||||
popd
|
||||
|
||||
# mgclient
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -20,6 +20,8 @@ inline constexpr uint8_t kPreamble[4] = {0x60, 0x60, 0xB0, 0x17};
|
||||
enum class Signature : uint8_t {
|
||||
Noop = 0x00,
|
||||
Init = 0x01,
|
||||
LogOn = 0x6A,
|
||||
LogOff = 0x6B,
|
||||
AckFailure = 0x0E, // only v1
|
||||
Reset = 0x0F,
|
||||
Goodbye = 0x02,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -28,7 +28,7 @@ inline constexpr size_t kChunkWholeSize = kChunkHeaderSize + kChunkMaxDataSize;
|
||||
*/
|
||||
inline constexpr size_t kHandshakeSize = 20;
|
||||
|
||||
inline constexpr uint16_t kSupportedVersions[] = {0x0100, 0x0400, 0x0401, 0x0403};
|
||||
inline constexpr uint16_t kSupportedVersions[] = {0x0100, 0x0400, 0x0401, 0x0403, 0x0502};
|
||||
|
||||
inline constexpr int kPullAll = -1;
|
||||
inline constexpr int kPullLast = -1;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -33,7 +33,14 @@ namespace memgraph::communication::bolt {
|
||||
template <typename Buffer>
|
||||
class Decoder {
|
||||
public:
|
||||
explicit Decoder(Buffer &buffer) : buffer_(buffer) {}
|
||||
explicit Decoder(Buffer &buffer) : buffer_(buffer), major_v_(0) {}
|
||||
|
||||
/**
|
||||
* Lets the user update the version.
|
||||
* This is all single thread for now. TODO: Update if ever multithreaded.
|
||||
* @param major_v the major version of the Bolt protocol used.
|
||||
*/
|
||||
void UpdateVersion(int major_v) { major_v_ = major_v; }
|
||||
|
||||
/**
|
||||
* Reads a Value from the available data in the buffer.
|
||||
@@ -208,6 +215,10 @@ class Decoder {
|
||||
|
||||
protected:
|
||||
Buffer &buffer_;
|
||||
int major_v_; //!< Major version of the underlying Bolt protocol
|
||||
// TODO: when refactoring
|
||||
// Ideally the major_v would be a compile time constant. If the higher level (Bolt driver) ends up being separate
|
||||
// classes, this could be just a template and each version of the driver would use the appropriate decoder.
|
||||
|
||||
private:
|
||||
bool ReadNull(const Marker &marker, Value *data) {
|
||||
@@ -370,11 +381,7 @@ class Decoder {
|
||||
}
|
||||
ret.emplace(std::move(dv_key.ValueString()), std::move(dv_val));
|
||||
}
|
||||
if (ret.size() != size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return ret.size() == size;
|
||||
}
|
||||
|
||||
bool ReadVertex(Value *data) {
|
||||
@@ -407,6 +414,14 @@ class Decoder {
|
||||
}
|
||||
vertex.properties = std::move(dv.ValueMap());
|
||||
|
||||
if (major_v_ > 4) {
|
||||
// element_id introduced in v5.0
|
||||
if (!ReadValue(&dv, Value::Type::String)) {
|
||||
return false;
|
||||
}
|
||||
vertex.element_id = std::move(dv.ValueString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -445,6 +460,23 @@ class Decoder {
|
||||
}
|
||||
edge.properties = std::move(dv.ValueMap());
|
||||
|
||||
if (major_v_ > 4) {
|
||||
// element_id introduced in v5.0
|
||||
if (!ReadValue(&dv, Value::Type::String)) {
|
||||
return false;
|
||||
}
|
||||
edge.element_id = std::move(dv.ValueString());
|
||||
// from_element_id introduced in v5.0
|
||||
if (!ReadValue(&dv, Value::Type::String)) {
|
||||
return false;
|
||||
}
|
||||
edge.from_element_id = std::move(dv.ValueString());
|
||||
// to_element_id introduced in v5.0
|
||||
if (!ReadValue(&dv, Value::Type::String)) {
|
||||
return false;
|
||||
}
|
||||
edge.to_element_id = std::move(dv.ValueString());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -471,6 +503,14 @@ class Decoder {
|
||||
}
|
||||
edge.properties = std::move(dv.ValueMap());
|
||||
|
||||
if (major_v_ > 4) {
|
||||
// element_id introduced in v5.0
|
||||
if (!ReadValue(&dv, Value::Type::String)) {
|
||||
return false;
|
||||
}
|
||||
edge.element_id = std::move(dv.ValueString());
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -36,7 +36,14 @@ namespace memgraph::communication::bolt {
|
||||
template <typename Buffer>
|
||||
class BaseEncoder {
|
||||
public:
|
||||
explicit BaseEncoder(Buffer &buffer) : buffer_(buffer) {}
|
||||
explicit BaseEncoder(Buffer &buffer) : buffer_(buffer), major_v_(0) {}
|
||||
|
||||
/**
|
||||
* Lets the user update the version.
|
||||
* This is all single thread for now. TODO: Update if ever multithreaded.
|
||||
* @param major_v the major version of the Bolt protocol used.
|
||||
*/
|
||||
void UpdateVersion(int major_v) { major_v_ = major_v; }
|
||||
|
||||
void WriteRAW(const uint8_t *data, uint64_t len) { buffer_.Write(data, len); }
|
||||
|
||||
@@ -116,7 +123,8 @@ class BaseEncoder {
|
||||
}
|
||||
|
||||
void WriteVertex(const Vertex &vertex) {
|
||||
WriteRAW(utils::UnderlyingCast(Marker::TinyStruct) + 3);
|
||||
int struct_n = 3 + 1 * int(major_v_ > 4); // element_id introduced from v5
|
||||
WriteRAW(utils::UnderlyingCast(Marker::TinyStruct) + struct_n);
|
||||
WriteRAW(utils::UnderlyingCast(Signature::Node));
|
||||
WriteInt(vertex.id.AsInt());
|
||||
|
||||
@@ -132,10 +140,16 @@ class BaseEncoder {
|
||||
WriteString(prop.first);
|
||||
WriteValue(prop.second);
|
||||
}
|
||||
|
||||
if (major_v_ > 4) {
|
||||
// element_id introduced in v5.0
|
||||
WriteString(vertex.element_id);
|
||||
}
|
||||
}
|
||||
|
||||
void WriteEdge(const Edge &edge, bool unbound = false) {
|
||||
WriteRAW(utils::UnderlyingCast(Marker::TinyStruct) + (unbound ? 3 : 5));
|
||||
int struct_n = (unbound ? 3 + 1 * int(major_v_ > 4) : 5 + 3 * int(major_v_ > 4)); // element_id introduced from v5
|
||||
WriteRAW(utils::UnderlyingCast(Marker::TinyStruct) + struct_n);
|
||||
WriteRAW(utils::UnderlyingCast(unbound ? Signature::UnboundRelationship : Signature::Relationship));
|
||||
|
||||
WriteInt(edge.id.AsInt());
|
||||
@@ -152,10 +166,22 @@ class BaseEncoder {
|
||||
WriteString(prop.first);
|
||||
WriteValue(prop.second);
|
||||
}
|
||||
|
||||
if (major_v_ > 4) {
|
||||
// element_id introduced in v5.0
|
||||
WriteString(edge.element_id);
|
||||
if (!unbound) {
|
||||
// from_element_id introduced in v5.0
|
||||
WriteString(edge.from_element_id);
|
||||
// to_element_id introduced in v5.0
|
||||
WriteString(edge.to_element_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void WriteEdge(const UnboundedEdge &edge) {
|
||||
WriteRAW(utils::UnderlyingCast(Marker::TinyStruct) + 3);
|
||||
const int struct_n = 3 + 1 * int(major_v_ > 4); // element_id introduced from v5
|
||||
WriteRAW(utils::UnderlyingCast(Marker::TinyStruct) + struct_n);
|
||||
WriteRAW(utils::UnderlyingCast(Signature::UnboundRelationship));
|
||||
|
||||
WriteInt(edge.id.AsInt());
|
||||
@@ -168,6 +194,11 @@ class BaseEncoder {
|
||||
WriteString(prop.first);
|
||||
WriteValue(prop.second);
|
||||
}
|
||||
|
||||
if (major_v_ > 4) {
|
||||
// element_id introduced in v5.0
|
||||
WriteString(edge.element_id);
|
||||
}
|
||||
}
|
||||
|
||||
void WritePath(const Path &path) {
|
||||
@@ -264,6 +295,7 @@ class BaseEncoder {
|
||||
|
||||
protected:
|
||||
Buffer &buffer_;
|
||||
int major_v_; //!< Major version of the underlying Bolt protocol (TODO: Think about reimplementing the versioning)
|
||||
|
||||
private:
|
||||
template <class T>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -41,6 +41,8 @@ class ClientEncoder : private BaseEncoder<Buffer> {
|
||||
public:
|
||||
ClientEncoder(Buffer &buffer) : BaseEncoder<Buffer>(buffer) {}
|
||||
|
||||
using BaseEncoder<Buffer>::UpdateVersion;
|
||||
|
||||
/**
|
||||
* Writes a Init message.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -34,6 +34,8 @@ class Encoder : private BaseEncoder<Buffer> {
|
||||
public:
|
||||
Encoder(Buffer &buffer) : BaseEncoder<Buffer>(buffer) {}
|
||||
|
||||
using BaseEncoder<Buffer>::UpdateVersion;
|
||||
|
||||
/**
|
||||
* Sends a Record message.
|
||||
*
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -63,7 +63,8 @@ class Session {
|
||||
* if an explicit transaction was started.
|
||||
*/
|
||||
virtual std::pair<std::vector<std::string>, std::optional<int>> Interpret(
|
||||
const std::string &query, const std::map<std::string, Value> ¶ms) = 0;
|
||||
const std::string &query, const std::map<std::string, Value> ¶ms,
|
||||
const std::map<std::string, memgraph::communication::bolt::Value> &metadata) = 0;
|
||||
|
||||
/**
|
||||
* Put results of the processed query in the `encoder`.
|
||||
@@ -85,7 +86,7 @@ class Session {
|
||||
*/
|
||||
virtual std::map<std::string, Value> Discard(std::optional<int> n, std::optional<int> qid) = 0;
|
||||
|
||||
virtual void BeginTransaction() = 0;
|
||||
virtual void BeginTransaction(const std::map<std::string, memgraph::communication::bolt::Value> &) = 0;
|
||||
virtual void CommitTransaction() = 0;
|
||||
virtual void RollbackTransaction() = 0;
|
||||
|
||||
@@ -120,6 +121,9 @@ class Session {
|
||||
return;
|
||||
}
|
||||
handshake_done_ = true;
|
||||
// Update the decoder's Bolt version (v5 has changed the undelying structure)
|
||||
decoder_.UpdateVersion(version_.major);
|
||||
encoder_.UpdateVersion(version_.major);
|
||||
}
|
||||
|
||||
ChunkState chunk_state;
|
||||
|
||||
@@ -91,6 +91,37 @@ State RunHandlerV4(Signature signature, TSession &session, State state, Marker m
|
||||
}
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State RunHandlerV5(Signature signature, TSession &session, State state, Marker marker) {
|
||||
switch (signature) {
|
||||
case Signature::Run:
|
||||
return HandleRunV5<TSession>(session, state, marker);
|
||||
case Signature::Pull:
|
||||
return HandlePullV5<TSession>(session, state, marker);
|
||||
case Signature::Discard:
|
||||
return HandleDiscardV5<TSession>(session, state, marker);
|
||||
case Signature::Reset:
|
||||
return HandleReset<TSession>(session, marker);
|
||||
case Signature::Begin:
|
||||
return HandleBegin<TSession>(session, state, marker);
|
||||
case Signature::Commit:
|
||||
return HandleCommit<TSession>(session, state, marker);
|
||||
case Signature::Goodbye:
|
||||
return HandleGoodbye<TSession>();
|
||||
case Signature::Rollback:
|
||||
return HandleRollback<TSession>(session, state, marker);
|
||||
case Signature::Noop:
|
||||
return HandleNoop<TSession>(state);
|
||||
case Signature::Route:
|
||||
return HandleRoute<TSession>(session, marker);
|
||||
case Signature::LogOff:
|
||||
return HandleLogOff<TSession>();
|
||||
default:
|
||||
spdlog::trace("Unrecognized signature received (0x{:02X})!", utils::UnderlyingCast(signature));
|
||||
return State::Close;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executor state run function
|
||||
* This function executes an initialized Bolt session.
|
||||
@@ -120,6 +151,8 @@ State StateExecutingRun(TSession &session, State state) {
|
||||
}
|
||||
return RunHandlerV4<TSession>(signature, session, state, marker);
|
||||
}
|
||||
case 5:
|
||||
return RunHandlerV5<TSession>(signature, session, state, marker);
|
||||
default:
|
||||
spdlog::trace("Unsupported bolt version:{}.{})!", session.version_.major, session.version_.minor);
|
||||
return State::Close;
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "communication/bolt/v1/state.hpp"
|
||||
#include "communication/bolt/v1/value.hpp"
|
||||
#include "communication/exceptions.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/message.hpp"
|
||||
|
||||
@@ -71,6 +73,23 @@ inline std::pair<std::string, std::string> ExceptionToErrorMessage(const std::ex
|
||||
"should be in database logs."};
|
||||
}
|
||||
|
||||
namespace helpers {
|
||||
|
||||
/** Extracts metadata from the extras field.
|
||||
* NOTE: In order to avoid a copy, the metadata in moved.
|
||||
* TODO: Update if extra field is used for anything else.
|
||||
*/
|
||||
inline std::map<std::string, Value> ConsumeMetadata(Value &extra) {
|
||||
std::map<std::string, Value> md;
|
||||
auto &md_tv = extra.ValueMap()["tx_metadata"];
|
||||
if (md_tv.IsMap()) {
|
||||
md = std::move(md_tv.ValueMap());
|
||||
}
|
||||
return md;
|
||||
}
|
||||
|
||||
} // namespace helpers
|
||||
|
||||
namespace details {
|
||||
|
||||
template <bool is_pull, typename TSession>
|
||||
@@ -209,7 +228,7 @@ State HandleRunV1(TSession &session, const State state, const Marker marker) {
|
||||
|
||||
try {
|
||||
// Interpret can throw.
|
||||
const auto [header, qid] = session.Interpret(query.ValueString(), params.ValueMap());
|
||||
const auto [header, qid] = session.Interpret(query.ValueString(), params.ValueMap(), {});
|
||||
// Convert std::string to Value
|
||||
std::vector<Value> vec;
|
||||
std::map<std::string, Value> data;
|
||||
@@ -250,6 +269,7 @@ State HandleRunV4(TSession &session, const State state, const Marker marker) {
|
||||
// Even though this part seems unnecessary it is needed to move the buffer
|
||||
if (!session.decoder_.ReadValue(&extra, Value::Type::Map)) {
|
||||
spdlog::trace("Couldn't read extra field!");
|
||||
return State::Close;
|
||||
}
|
||||
|
||||
if (state != State::Idle) {
|
||||
@@ -266,7 +286,8 @@ State HandleRunV4(TSession &session, const State state, const Marker marker) {
|
||||
|
||||
try {
|
||||
// Interpret can throw.
|
||||
const auto [header, qid] = session.Interpret(query.ValueString(), params.ValueMap());
|
||||
const auto [header, qid] =
|
||||
session.Interpret(query.ValueString(), params.ValueMap(), helpers::ConsumeMetadata(extra));
|
||||
// Convert std::string to Value
|
||||
std::vector<Value> vec;
|
||||
std::map<std::string, Value> data;
|
||||
@@ -288,6 +309,12 @@ State HandleRunV4(TSession &session, const State state, const Marker marker) {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State HandleRunV5(TSession &session, const State state, const Marker marker) {
|
||||
// Using V4 on purpose
|
||||
return HandleRunV4<TSession>(session, state, marker);
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State HandlePullV1(TSession &session, const State state, const Marker marker) {
|
||||
return details::HandlePullDiscardV1<true>(session, state, marker);
|
||||
@@ -298,6 +325,12 @@ State HandlePullV4(TSession &session, const State state, const Marker marker) {
|
||||
return details::HandlePullDiscardV4<true>(session, state, marker);
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State HandlePullV5(TSession &session, const State state, const Marker marker) {
|
||||
// Using V4 on purpose
|
||||
return HandlePullV4<TSession>(session, state, marker);
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State HandleDiscardV1(TSession &session, const State state, const Marker marker) {
|
||||
return details::HandlePullDiscardV1<false>(session, state, marker);
|
||||
@@ -308,6 +341,12 @@ State HandleDiscardV4(TSession &session, const State state, const Marker marker)
|
||||
return details::HandlePullDiscardV4<false>(session, state, marker);
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State HandleDiscardV5(TSession &session, const State state, const Marker marker) {
|
||||
// Using V4 on purpose
|
||||
return HandleDiscardV4<TSession>(session, state, marker);
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State HandleReset(TSession &session, const Marker marker) {
|
||||
// IMPORTANT: This implementation of the Bolt RESET command isn't fully
|
||||
@@ -360,7 +399,7 @@ State HandleBegin(TSession &session, const State state, const Marker marker) {
|
||||
}
|
||||
|
||||
try {
|
||||
session.BeginTransaction();
|
||||
session.BeginTransaction(helpers::ConsumeMetadata(extra));
|
||||
} catch (const std::exception &e) {
|
||||
return HandleFailure(session, e);
|
||||
}
|
||||
@@ -465,4 +504,10 @@ State HandleRoute(TSession &session, const Marker marker) {
|
||||
}
|
||||
return State::Error;
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State HandleLogOff() {
|
||||
// Not arguments sent, the user just needs to reauthenticate
|
||||
return State::Init;
|
||||
}
|
||||
} // namespace memgraph::communication::bolt
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -27,17 +27,25 @@ namespace details {
|
||||
template <typename TSession>
|
||||
std::optional<State> AuthenticateUser(TSession &session, Value &metadata) {
|
||||
// Get authentication data.
|
||||
// From neo4j driver v4.4, fields that have a default value are not sent.
|
||||
// In order to have back-compatibility, the missing fields will be added.
|
||||
|
||||
auto &data = metadata.ValueMap();
|
||||
if (!data.count("scheme")) {
|
||||
spdlog::warn("The client didn't supply authentication information!");
|
||||
return State::Close;
|
||||
if (data.empty()) { // Special case auth=None
|
||||
spdlog::warn("The client didn't supply the authentication scheme! Trying with \"none\"...");
|
||||
data["scheme"] = "none";
|
||||
}
|
||||
|
||||
std::string username;
|
||||
std::string password;
|
||||
if (data["scheme"].ValueString() == "basic") {
|
||||
if (!data.count("principal") || !data.count("credentials")) {
|
||||
spdlog::warn("The client didn't supply authentication information!");
|
||||
return State::Close;
|
||||
if (!data.count("principal")) { // Special case principal = ""
|
||||
spdlog::warn("The client didn't supply the principal field! Trying with \"\"...");
|
||||
data["principal"] = "";
|
||||
}
|
||||
if (!data.count("credentials")) { // Special case credentials = ""
|
||||
spdlog::warn("The client didn't supply the credentials field! Trying with \"\"...");
|
||||
data["credentials"] = "";
|
||||
}
|
||||
username = data["principal"].ValueString();
|
||||
password = data["credentials"].ValueString();
|
||||
@@ -106,6 +114,30 @@ std::optional<Value> GetMetadataV4(TSession &session, const Marker marker) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto &data = metadata.ValueMap();
|
||||
if (!data.count("user_agent")) {
|
||||
spdlog::warn("The client didn't supply the user agent!");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
spdlog::info("Client connected '{}'", data.at("user_agent").ValueString());
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
std::optional<Value> GetInitDataV5(TSession &session, const Marker marker) {
|
||||
if (marker != Marker::TinyStruct1) [[unlikely]] {
|
||||
spdlog::trace("Expected TinyStruct1 marker, but received 0x{:02X}!", utils::UnderlyingCast(marker));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Value metadata;
|
||||
if (!session.decoder_.ReadValue(&metadata, Value::Type::Map)) {
|
||||
spdlog::trace("Couldn't read metadata!");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const auto &data = metadata.ValueMap();
|
||||
if (!data.count("user_agent")) {
|
||||
spdlog::warn("The client didn't supply the user agent!");
|
||||
@@ -117,6 +149,22 @@ std::optional<Value> GetMetadataV4(TSession &session, const Marker marker) {
|
||||
return metadata;
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
std::optional<Value> GetAuthDataV5(TSession &session, const Marker marker) {
|
||||
if (marker != Marker::TinyStruct1) [[unlikely]] {
|
||||
spdlog::trace("Expected TinyStruct1 marker, but received 0x{:02X}!", utils::UnderlyingCast(marker));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
Value metadata;
|
||||
if (!session.decoder_.ReadValue(&metadata, Value::Type::Map)) {
|
||||
spdlog::trace("Couldn't read metadata!");
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State SendSuccessMessage(TSession &session) {
|
||||
// Neo4j's Java driver 4.1.1+ requires connection_id.
|
||||
@@ -180,6 +228,57 @@ State StateInitRunV4(TSession &session, Marker marker, Signature signature) {
|
||||
|
||||
return SendSuccessMessage(session);
|
||||
}
|
||||
|
||||
template <typename TSession>
|
||||
State StateInitRunV5(TSession &session, Marker marker, Signature signature) {
|
||||
if (signature == Signature::Noop) [[unlikely]] {
|
||||
SPDLOG_DEBUG("Received NOOP message");
|
||||
return State::Init;
|
||||
}
|
||||
|
||||
if (signature == Signature::Init) {
|
||||
auto maybeMetadata = GetInitDataV5(session, marker);
|
||||
|
||||
if (!maybeMetadata) {
|
||||
return State::Close;
|
||||
}
|
||||
|
||||
if (SendSuccessMessage(session) == State::Close) {
|
||||
return State::Close;
|
||||
}
|
||||
// Stay in Init
|
||||
return State::Init;
|
||||
|
||||
} else if (signature == Signature::LogOn) {
|
||||
if (marker != Marker::TinyStruct1) [[unlikely]] {
|
||||
spdlog::trace("Expected TinyStruct1 marker, but received 0x{:02X}!", utils::UnderlyingCast(marker));
|
||||
spdlog::trace(
|
||||
"The client sent malformed data, but we are continuing "
|
||||
"because the official Neo4j Java driver sends malformed "
|
||||
"data. D'oh!");
|
||||
return State::Close;
|
||||
}
|
||||
|
||||
auto maybeMetadata = GetAuthDataV5(session, marker);
|
||||
if (!maybeMetadata) {
|
||||
return State::Close;
|
||||
}
|
||||
auto result = AuthenticateUser(session, *maybeMetadata);
|
||||
if (result) {
|
||||
spdlog::trace("Failed to authenticate, closing connection...");
|
||||
return State::Close;
|
||||
}
|
||||
|
||||
if (SendSuccessMessage(session) == State::Close) {
|
||||
return State::Close;
|
||||
}
|
||||
return State::Idle;
|
||||
|
||||
} else [[unlikely]] {
|
||||
spdlog::trace("Expected Init signature, but received 0x{:02X}!", utils::UnderlyingCast(signature));
|
||||
return State::Close;
|
||||
}
|
||||
}
|
||||
} // namespace details
|
||||
|
||||
/**
|
||||
@@ -208,6 +307,9 @@ State StateInitRun(TSession &session) {
|
||||
}
|
||||
return details::StateInitRunV4<TSession>(session, marker, signature);
|
||||
}
|
||||
case 5: {
|
||||
return details::StateInitRunV5<TSession>(session, marker, signature);
|
||||
}
|
||||
}
|
||||
spdlog::trace("Unsupported bolt version:{}.{})!", session.version_.major, session.version_.minor);
|
||||
return State::Close;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -342,6 +342,9 @@ std::ostream &operator<<(std::ostream &os, const Vertex &vertex) {
|
||||
[&](auto &stream, const auto &pair) { stream << pair.first << ": " << pair.second; });
|
||||
os << "}";
|
||||
}
|
||||
if (!vertex.element_id.empty()) {
|
||||
os << " element_id: " << vertex.element_id;
|
||||
}
|
||||
return os << ")";
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -57,6 +57,7 @@ struct Vertex {
|
||||
Id id;
|
||||
std::vector<std::string> labels;
|
||||
std::map<std::string, Value> properties;
|
||||
std::string element_id;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -69,6 +70,9 @@ struct Edge {
|
||||
Id to;
|
||||
std::string type;
|
||||
std::map<std::string, Value> properties;
|
||||
std::string element_id;
|
||||
std::string from_element_id;
|
||||
std::string to_element_id;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -79,6 +83,7 @@ struct UnboundedEdge {
|
||||
Id id;
|
||||
std::string type;
|
||||
std::map<std::string, Value> properties;
|
||||
std::string element_id;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -150,7 +150,9 @@ storage::Result<communication::bolt::Vertex> ToBoltVertex(const storage::VertexA
|
||||
for (const auto &prop : *maybe_properties) {
|
||||
properties[db.PropertyToName(prop.first)] = ToBoltValue(prop.second);
|
||||
}
|
||||
return communication::bolt::Vertex{id, labels, properties};
|
||||
// Introduced in Bolt v5 (for now just send the ID)
|
||||
const auto element_id = std::to_string(id.AsInt());
|
||||
return communication::bolt::Vertex{id, labels, properties, element_id};
|
||||
}
|
||||
|
||||
storage::Result<communication::bolt::Edge> ToBoltEdge(const storage::EdgeAccessor &edge, const storage::Storage &db,
|
||||
@@ -165,7 +167,11 @@ storage::Result<communication::bolt::Edge> ToBoltEdge(const storage::EdgeAccesso
|
||||
for (const auto &prop : *maybe_properties) {
|
||||
properties[db.PropertyToName(prop.first)] = ToBoltValue(prop.second);
|
||||
}
|
||||
return communication::bolt::Edge{id, from, to, type, properties};
|
||||
// Introduced in Bolt v5 (for now just send the ID)
|
||||
const auto element_id = std::to_string(id.AsInt());
|
||||
const auto from_element_id = std::to_string(from.AsInt());
|
||||
const auto to_element_id = std::to_string(to.AsInt());
|
||||
return communication::bolt::Edge{id, from, to, type, properties, element_id, from_element_id, to_element_id};
|
||||
}
|
||||
|
||||
storage::Result<communication::bolt::Path> ToBoltPath(const query::Path &path, const storage::Storage &db,
|
||||
|
||||
@@ -533,16 +533,29 @@ class BoltSession final : public memgraph::communication::bolt::Session<memgraph
|
||||
using memgraph::communication::bolt::Session<memgraph::communication::v2::InputStream,
|
||||
memgraph::communication::v2::OutputStream>::TEncoder;
|
||||
|
||||
void BeginTransaction() override { interpreter_.BeginTransaction(); }
|
||||
void BeginTransaction(const std::map<std::string, memgraph::communication::bolt::Value> &metadata) override {
|
||||
std::map<std::string, memgraph::storage::PropertyValue> metadata_pv;
|
||||
for (const auto &[key, bolt_value] : metadata) {
|
||||
metadata_pv.emplace(key, memgraph::glue::ToPropertyValue(bolt_value));
|
||||
}
|
||||
interpreter_.BeginTransaction(metadata_pv);
|
||||
}
|
||||
|
||||
void CommitTransaction() override { interpreter_.CommitTransaction(); }
|
||||
|
||||
void RollbackTransaction() override { interpreter_.RollbackTransaction(); }
|
||||
|
||||
std::pair<std::vector<std::string>, std::optional<int>> Interpret(
|
||||
const std::string &query, const std::map<std::string, memgraph::communication::bolt::Value> ¶ms) override {
|
||||
const std::string &query, const std::map<std::string, memgraph::communication::bolt::Value> ¶ms,
|
||||
const std::map<std::string, memgraph::communication::bolt::Value> &metadata) override {
|
||||
std::map<std::string, memgraph::storage::PropertyValue> params_pv;
|
||||
for (const auto &kv : params) params_pv.emplace(kv.first, memgraph::glue::ToPropertyValue(kv.second));
|
||||
std::map<std::string, memgraph::storage::PropertyValue> metadata_pv;
|
||||
for (const auto &[key, bolt_param] : params) {
|
||||
params_pv.emplace(key, memgraph::glue::ToPropertyValue(bolt_param));
|
||||
}
|
||||
for (const auto &[key, bolt_md] : metadata) {
|
||||
metadata_pv.emplace(key, memgraph::glue::ToPropertyValue(bolt_md));
|
||||
}
|
||||
const std::string *username{nullptr};
|
||||
if (user_) {
|
||||
username = &user_->username();
|
||||
@@ -554,7 +567,7 @@ class BoltSession final : public memgraph::communication::bolt::Session<memgraph
|
||||
}
|
||||
#endif
|
||||
try {
|
||||
auto result = interpreter_.Prepare(query, params_pv, username);
|
||||
auto result = interpreter_.Prepare(query, params_pv, username, metadata_pv);
|
||||
if (user_ && !memgraph::glue::AuthChecker::IsUserAuthorized(*user_, result.privileges)) {
|
||||
interpreter_.Abort();
|
||||
throw memgraph::communication::bolt::ClientError(
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "query/trigger.hpp"
|
||||
#include "utils/async_timer.hpp"
|
||||
|
||||
#include "query/frame_change.hpp"
|
||||
|
||||
namespace memgraph::query {
|
||||
|
||||
enum class TransactionStatus {
|
||||
@@ -82,6 +84,7 @@ struct ExecutionContext {
|
||||
plan::ProfilingStats *stats_root{nullptr};
|
||||
ExecutionStats execution_stats;
|
||||
TriggerContextCollector *trigger_context_collector{nullptr};
|
||||
FrameChangeCollector *frame_change_collector{nullptr};
|
||||
utils::AsyncTimer timer;
|
||||
#ifdef MG_ENTERPRISE
|
||||
std::unique_ptr<FineGrainedAuthChecker> auth_checker{nullptr};
|
||||
|
||||
122
src/query/frame_change.hpp
Normal file
122
src/query/frame_change.hpp
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
#include "query/typed_value.hpp"
|
||||
#include "utils/memory.hpp"
|
||||
#include "utils/pmr/unordered_map.hpp"
|
||||
#include "utils/pmr/vector.hpp"
|
||||
namespace memgraph::query {
|
||||
|
||||
// Key is hash output, value is vector of unique elements
|
||||
using CachedType = utils::pmr::unordered_map<size_t, std::vector<TypedValue>>;
|
||||
|
||||
struct CachedValue {
|
||||
// Cached value, this can be probably templateized
|
||||
CachedType cache_;
|
||||
|
||||
explicit CachedValue(utils::MemoryResource *mem) : cache_(mem) {}
|
||||
|
||||
CachedValue(CachedType &&cache, memgraph::utils::MemoryResource *memory) : cache_(std::move(cache), memory) {}
|
||||
|
||||
CachedValue(const CachedValue &other, memgraph::utils::MemoryResource *memory) : cache_(other.cache_, memory) {}
|
||||
|
||||
CachedValue(CachedValue &&other, memgraph::utils::MemoryResource *memory) : cache_(std::move(other.cache_), memory) {}
|
||||
|
||||
CachedValue(CachedValue &&other) noexcept = delete;
|
||||
|
||||
/// Copy construction without memgraph::utils::MemoryResource is not allowed.
|
||||
CachedValue(const CachedValue &) = delete;
|
||||
|
||||
CachedValue &operator=(const CachedValue &) = delete;
|
||||
CachedValue &operator=(CachedValue &&) = delete;
|
||||
|
||||
~CachedValue() = default;
|
||||
|
||||
memgraph::utils::MemoryResource *GetMemoryResource() const noexcept {
|
||||
return cache_.get_allocator().GetMemoryResource();
|
||||
}
|
||||
|
||||
// Func to check if cache_ contains value
|
||||
bool CacheValue(const TypedValue &value) {
|
||||
if (!value.IsList()) {
|
||||
return false;
|
||||
}
|
||||
const auto &list = value.ValueList();
|
||||
TypedValue::Hash hash{};
|
||||
for (const TypedValue &element : list) {
|
||||
const auto key = hash(element);
|
||||
auto &vector_values = cache_[key];
|
||||
if (!IsValueInVec(vector_values, element)) {
|
||||
vector_values.push_back(element);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Func to cache_value inside cache_
|
||||
bool ContainsValue(const TypedValue &value) const {
|
||||
TypedValue::Hash hash{};
|
||||
const auto key = hash(value);
|
||||
if (cache_.contains(key)) {
|
||||
return IsValueInVec(cache_.at(key), value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool IsValueInVec(const std::vector<TypedValue> &vec_values, const TypedValue &value) const {
|
||||
return std::any_of(vec_values.begin(), vec_values.end(), [&value](auto &vec_value) {
|
||||
const auto is_value_equal = vec_value == value;
|
||||
if (is_value_equal.IsNull()) return false;
|
||||
return is_value_equal.ValueBool();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Class tracks keys for which user can cache values which help with faster search or faster retrieval
|
||||
// in the future.
|
||||
class FrameChangeCollector {
|
||||
public:
|
||||
explicit FrameChangeCollector(utils::MemoryResource *mem) : tracked_values_(mem){};
|
||||
|
||||
// Add tracking key to cache later value
|
||||
CachedValue &AddTrackingKey(const std::string &key) {
|
||||
const auto &[it, _] = tracked_values_.emplace(key, tracked_values_.get_allocator().GetMemoryResource());
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Is key tracked
|
||||
bool IsKeyTracked(const std::string &key) const { return tracked_values_.contains(key); }
|
||||
|
||||
// Is value for given key cached
|
||||
bool IsKeyValueCached(const std::string &key) const {
|
||||
return tracked_values_.contains(key) && !tracked_values_.at(key).cache_.empty();
|
||||
}
|
||||
|
||||
// Reset value for tracking key
|
||||
bool ResetTrackingValue(const std::string &key) {
|
||||
if (tracked_values_.contains(key)) {
|
||||
tracked_values_.erase(key);
|
||||
AddTrackingKey(key);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Get value cached for tracking key, throws if key is not in tracked
|
||||
CachedValue &GetCachedValue(const std::string &key) { return tracked_values_.at(key); }
|
||||
|
||||
// Checks for keys tracked
|
||||
bool IsTrackingValues() const { return !tracked_values_.empty(); }
|
||||
|
||||
private:
|
||||
// Key is output of utils::GetFrameChangeId, value is utils::pmr::unordered_map
|
||||
memgraph::utils::pmr::unordered_map<std::string, CachedValue> tracked_values_;
|
||||
};
|
||||
} // namespace memgraph::query
|
||||
@@ -13,6 +13,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
@@ -28,7 +29,11 @@
|
||||
#include "query/frontend/semantic/symbol_table.hpp"
|
||||
#include "query/interpret/frame.hpp"
|
||||
#include "query/typed_value.hpp"
|
||||
#include "spdlog/spdlog.h"
|
||||
#include "utils/exceptions.hpp"
|
||||
#include "utils/frame_change_id.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
#include "utils/pmr/unordered_map.hpp"
|
||||
|
||||
namespace memgraph::query {
|
||||
|
||||
@@ -103,8 +108,13 @@ class ReferenceExpressionEvaluator : public ExpressionVisitor<TypedValue *> {
|
||||
class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
||||
public:
|
||||
ExpressionEvaluator(Frame *frame, const SymbolTable &symbol_table, const EvaluationContext &ctx, DbAccessor *dba,
|
||||
storage::View view)
|
||||
: frame_(frame), symbol_table_(&symbol_table), ctx_(&ctx), dba_(dba), view_(view) {}
|
||||
storage::View view, FrameChangeCollector *frame_change_collector = nullptr)
|
||||
: frame_(frame),
|
||||
symbol_table_(&symbol_table),
|
||||
ctx_(&ctx),
|
||||
dba_(dba),
|
||||
view_(view),
|
||||
frame_change_collector_(frame_change_collector) {}
|
||||
|
||||
using ExpressionVisitor<TypedValue>::Visit;
|
||||
|
||||
@@ -193,25 +203,78 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
||||
}
|
||||
|
||||
TypedValue Visit(InListOperator &in_list) override {
|
||||
TypedValue *_list_ptr = nullptr;
|
||||
TypedValue _list;
|
||||
auto literal = in_list.expression1_->Accept(*this);
|
||||
auto _list = in_list.expression2_->Accept(*this);
|
||||
if (_list.IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
|
||||
auto get_list_literal = [this, &in_list, &_list, &_list_ptr]() -> void {
|
||||
ReferenceExpressionEvaluator reference_expression_evaluator{frame_, symbol_table_, ctx_};
|
||||
_list_ptr = in_list.expression2_->Accept(reference_expression_evaluator);
|
||||
if (nullptr == _list_ptr) {
|
||||
_list = in_list.expression2_->Accept(*this);
|
||||
_list_ptr = &_list;
|
||||
}
|
||||
};
|
||||
|
||||
auto do_list_literal_checks = [this, &literal, &_list_ptr]() -> std::optional<TypedValue> {
|
||||
MG_ASSERT(_list_ptr, "List literal should have been defined");
|
||||
if (_list_ptr->IsNull()) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
// Exceptions have higher priority than returning nulls when list expression
|
||||
// is not null.
|
||||
if (_list_ptr->type() != TypedValue::Type::List) {
|
||||
throw QueryRuntimeException("IN expected a list, got {}.", _list_ptr->type());
|
||||
}
|
||||
const auto &list = _list_ptr->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);
|
||||
return {};
|
||||
};
|
||||
|
||||
const auto cached_id = memgraph::utils::GetFrameChangeId(in_list);
|
||||
|
||||
const auto do_cache{frame_change_collector_ != nullptr && cached_id &&
|
||||
frame_change_collector_->IsKeyTracked(*cached_id)};
|
||||
if (do_cache) {
|
||||
if (!frame_change_collector_->IsKeyValueCached(*cached_id)) {
|
||||
// Check only first time if everything is okay, later when we use
|
||||
// cache there is no need to check again as we did check first time
|
||||
get_list_literal();
|
||||
auto preoperational_checks = do_list_literal_checks();
|
||||
if (preoperational_checks) {
|
||||
return std::move(*preoperational_checks);
|
||||
}
|
||||
auto &cached_value = frame_change_collector_->GetCachedValue(*cached_id);
|
||||
cached_value.CacheValue(*_list_ptr);
|
||||
spdlog::trace("Value cached {}", *cached_id);
|
||||
}
|
||||
const auto &cached_value = frame_change_collector_->GetCachedValue(*cached_id);
|
||||
|
||||
if (cached_value.ContainsValue(literal)) {
|
||||
return TypedValue(true, ctx_->memory);
|
||||
}
|
||||
// has null
|
||||
if (cached_value.ContainsValue(TypedValue(ctx_->memory))) {
|
||||
return TypedValue(ctx_->memory);
|
||||
}
|
||||
return TypedValue(false, ctx_->memory);
|
||||
}
|
||||
// Exceptions have higher priority than returning nulls when list expression
|
||||
// is not null.
|
||||
if (_list.type() != TypedValue::Type::List) {
|
||||
throw QueryRuntimeException("IN expected a list, got {}.", _list.type());
|
||||
// When caching is not an option, we need to evaluate list literal every time
|
||||
// and do the checks
|
||||
get_list_literal();
|
||||
auto preoperational_checks = do_list_literal_checks();
|
||||
if (preoperational_checks) {
|
||||
return std::move(*preoperational_checks);
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
spdlog::trace("Not using cache on IN LIST operator");
|
||||
auto has_null = false;
|
||||
for (const auto &element : list) {
|
||||
auto result = literal == element;
|
||||
@@ -973,6 +1036,7 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
||||
DbAccessor *dba_;
|
||||
// which switching approach should be used when evaluating
|
||||
storage::View view_;
|
||||
FrameChangeCollector *frame_change_collector_;
|
||||
}; // namespace memgraph::query
|
||||
|
||||
/// A helper function for evaluating an expression that's an int.
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <concepts>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
@@ -44,13 +45,16 @@
|
||||
#include "query/frontend/semantic/required_privileges.hpp"
|
||||
#include "query/frontend/semantic/symbol_generator.hpp"
|
||||
#include "query/interpret/eval.hpp"
|
||||
#include "query/interpret/frame.hpp"
|
||||
#include "query/metadata.hpp"
|
||||
#include "query/plan/planner.hpp"
|
||||
#include "query/plan/profile.hpp"
|
||||
#include "query/plan/vertex_count_cache.hpp"
|
||||
#include "query/stream.hpp"
|
||||
#include "query/stream/common.hpp"
|
||||
#include "query/trigger.hpp"
|
||||
#include "query/typed_value.hpp"
|
||||
#include "spdlog/spdlog.h"
|
||||
#include "storage/v2/edge.hpp"
|
||||
#include "storage/v2/id_types.hpp"
|
||||
#include "storage/v2/isolation_level.hpp"
|
||||
@@ -112,6 +116,16 @@ void UpdateTypeCount(const plan::ReadWriteTypeChecker::RWType type) {
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
concept HasEmpty = requires(T t) {
|
||||
{ t.empty() } -> std::convertible_to<bool>;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
inline std::optional<T> GenOptional(const T &in) {
|
||||
return in.empty() ? std::nullopt : std::make_optional<T>(in);
|
||||
}
|
||||
|
||||
struct Callback {
|
||||
std::vector<std::string> header;
|
||||
using CallbackFunction = std::function<std::vector<std::vector<TypedValue>>()>;
|
||||
@@ -993,7 +1007,8 @@ struct PullPlan {
|
||||
DbAccessor *dba, InterpreterContext *interpreter_context, utils::MemoryResource *execution_memory,
|
||||
std::optional<std::string> username, std::atomic<TransactionStatus> *transaction_status,
|
||||
TriggerContextCollector *trigger_context_collector = nullptr,
|
||||
std::optional<size_t> memory_limit = {}, bool use_monotonic_memory = true);
|
||||
std::optional<size_t> memory_limit = {}, bool use_monotonic_memory = true,
|
||||
FrameChangeCollector *frame_change_collector_ = nullptr);
|
||||
|
||||
std::optional<plan::ProfilingStatsWithTotalTime> Pull(AnyStream *stream, std::optional<int> n,
|
||||
const std::vector<Symbol> &output_symbols,
|
||||
@@ -1031,7 +1046,7 @@ PullPlan::PullPlan(const std::shared_ptr<CachedPlan> plan, const Parameters &par
|
||||
DbAccessor *dba, InterpreterContext *interpreter_context, utils::MemoryResource *execution_memory,
|
||||
std::optional<std::string> username, std::atomic<TransactionStatus> *transaction_status,
|
||||
TriggerContextCollector *trigger_context_collector, const std::optional<size_t> memory_limit,
|
||||
bool use_monotonic_memory)
|
||||
bool use_monotonic_memory, FrameChangeCollector *frame_change_collector)
|
||||
: plan_(plan),
|
||||
cursor_(plan->plan().MakeCursor(execution_memory)),
|
||||
frame_(plan->symbol_table().max_position(), execution_memory),
|
||||
@@ -1062,6 +1077,7 @@ PullPlan::PullPlan(const std::shared_ptr<CachedPlan> plan, const Parameters &par
|
||||
ctx_.transaction_status = transaction_status;
|
||||
ctx_.is_profile_query = is_profile_query;
|
||||
ctx_.trigger_context_collector = trigger_context_collector;
|
||||
ctx_.frame_change_collector = frame_change_collector;
|
||||
}
|
||||
|
||||
std::optional<plan::ProfilingStatsWithTotalTime> PullPlan::Pull(AnyStream *stream, std::optional<int> n,
|
||||
@@ -1177,11 +1193,14 @@ Interpreter::Interpreter(InterpreterContext *interpreter_context) : interpreter_
|
||||
MG_ASSERT(interpreter_context_, "Interpreter context must not be NULL");
|
||||
}
|
||||
|
||||
PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper) {
|
||||
PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper,
|
||||
const std::map<std::string, storage::PropertyValue> &metadata) {
|
||||
std::function<void()> handler;
|
||||
|
||||
if (query_upper == "BEGIN") {
|
||||
handler = [this] {
|
||||
// TODO: Evaluate doing move(metadata). Currently the metadata is very small, but this will be important if it ever
|
||||
// becomes large.
|
||||
handler = [this, metadata] {
|
||||
if (in_explicit_transaction_) {
|
||||
throw ExplicitTransactionUsageException("Nested transactions are not supported.");
|
||||
}
|
||||
@@ -1190,6 +1209,7 @@ PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper)
|
||||
|
||||
in_explicit_transaction_ = true;
|
||||
expect_rollback_ = false;
|
||||
metadata_ = GenOptional(metadata);
|
||||
|
||||
db_accessor_ =
|
||||
std::make_unique<storage::Storage::Accessor>(interpreter_context_->db->Access(GetIsolationLevelOverride()));
|
||||
@@ -1220,6 +1240,7 @@ PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper)
|
||||
|
||||
expect_rollback_ = false;
|
||||
in_explicit_transaction_ = false;
|
||||
metadata_ = std::nullopt;
|
||||
};
|
||||
} else if (query_upper == "ROLLBACK") {
|
||||
handler = [this] {
|
||||
@@ -1232,6 +1253,7 @@ PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper)
|
||||
Abort();
|
||||
expect_rollback_ = false;
|
||||
in_explicit_transaction_ = false;
|
||||
metadata_ = std::nullopt;
|
||||
};
|
||||
} else {
|
||||
LOG_FATAL("Should not get here -- unknown transaction query!");
|
||||
@@ -1246,11 +1268,28 @@ PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper)
|
||||
RWType::NONE};
|
||||
}
|
||||
|
||||
inline static void TryCaching(const AstStorage &ast_storage, FrameChangeCollector *frame_change_collector) {
|
||||
if (!frame_change_collector) return;
|
||||
for (const auto &tree : ast_storage.storage_) {
|
||||
if (tree->GetTypeInfo() != memgraph::query::InListOperator::kType) {
|
||||
continue;
|
||||
}
|
||||
auto *in_list_operator = utils::Downcast<InListOperator>(tree.get());
|
||||
const auto cached_id = memgraph::utils::GetFrameChangeId(*in_list_operator);
|
||||
if (!cached_id || cached_id->empty()) {
|
||||
continue;
|
||||
}
|
||||
frame_change_collector->AddTrackingKey(*cached_id);
|
||||
spdlog::trace("Tracking {} operator, by id: {}", InListOperator::kType.name, *cached_id);
|
||||
}
|
||||
}
|
||||
|
||||
PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string, TypedValue> *summary,
|
||||
InterpreterContext *interpreter_context, DbAccessor *dba,
|
||||
utils::MemoryResource *execution_memory, std::vector<Notification> *notifications,
|
||||
const std::string *username, std::atomic<TransactionStatus> *transaction_status,
|
||||
TriggerContextCollector *trigger_context_collector = nullptr) {
|
||||
TriggerContextCollector *trigger_context_collector = nullptr,
|
||||
FrameChangeCollector *frame_change_collector = nullptr) {
|
||||
auto *cypher_query = utils::Downcast<CypherQuery>(parsed_query.query);
|
||||
|
||||
Frame frame(0);
|
||||
@@ -1281,6 +1320,7 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string,
|
||||
parsed_query.parameters,
|
||||
parsed_query.is_cacheable ? &interpreter_context->plan_cache : nullptr, dba);
|
||||
|
||||
TryCaching(plan->ast_storage(), frame_change_collector);
|
||||
summary->insert_or_assign("cost_estimate", plan->cost());
|
||||
auto rw_type_checker = plan::ReadWriteTypeChecker();
|
||||
rw_type_checker.InferRWType(const_cast<plan::LogicalOperator &>(plan->plan()));
|
||||
@@ -1297,9 +1337,10 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string,
|
||||
header.push_back(
|
||||
utils::FindOr(parsed_query.stripped_query.named_expressions(), symbol.token_position(), symbol.name()).first);
|
||||
}
|
||||
auto pull_plan = std::make_shared<PullPlan>(plan, parsed_query.parameters, false, dba, interpreter_context,
|
||||
execution_memory, StringPointerToOptional(username), transaction_status,
|
||||
trigger_context_collector, memory_limit, use_monotonic_memory);
|
||||
auto pull_plan = std::make_shared<PullPlan>(
|
||||
plan, parsed_query.parameters, false, dba, interpreter_context, execution_memory,
|
||||
StringPointerToOptional(username), transaction_status, trigger_context_collector, memory_limit,
|
||||
use_monotonic_memory, frame_change_collector->IsTrackingValues() ? frame_change_collector : nullptr);
|
||||
return PreparedQuery{std::move(header), std::move(parsed_query.required_privileges),
|
||||
[pull_plan = std::move(pull_plan), output_symbols = std::move(output_symbols), summary](
|
||||
AnyStream *stream, std::optional<int> n) -> std::optional<QueryHandlerResult> {
|
||||
@@ -1360,7 +1401,8 @@ PreparedQuery PrepareExplainQuery(ParsedQuery parsed_query, std::map<std::string
|
||||
PreparedQuery PrepareProfileQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
|
||||
std::map<std::string, TypedValue> *summary, InterpreterContext *interpreter_context,
|
||||
DbAccessor *dba, utils::MemoryResource *execution_memory, const std::string *username,
|
||||
std::atomic<TransactionStatus> *transaction_status) {
|
||||
std::atomic<TransactionStatus> *transaction_status,
|
||||
FrameChangeCollector *frame_change_collector) {
|
||||
const std::string kProfileQueryStart = "profile ";
|
||||
|
||||
MG_ASSERT(utils::StartsWith(utils::ToLowerCase(parsed_query.stripped_query.query()), kProfileQueryStart),
|
||||
@@ -1419,39 +1461,42 @@ PreparedQuery PrepareProfileQuery(ParsedQuery parsed_query, bool in_explicit_tra
|
||||
auto cypher_query_plan = CypherQueryToPlan(
|
||||
parsed_inner_query.stripped_query.hash(), std::move(parsed_inner_query.ast_storage), cypher_query,
|
||||
parsed_inner_query.parameters, parsed_inner_query.is_cacheable ? &interpreter_context->plan_cache : nullptr, dba);
|
||||
TryCaching(cypher_query_plan->ast_storage(), frame_change_collector);
|
||||
auto rw_type_checker = plan::ReadWriteTypeChecker();
|
||||
auto optional_username = StringPointerToOptional(username);
|
||||
|
||||
rw_type_checker.InferRWType(const_cast<plan::LogicalOperator &>(cypher_query_plan->plan()));
|
||||
|
||||
return PreparedQuery{{"OPERATOR", "ACTUAL HITS", "RELATIVE TIME", "ABSOLUTE TIME"},
|
||||
std::move(parsed_query.required_privileges),
|
||||
[plan = std::move(cypher_query_plan), parameters = std::move(parsed_inner_query.parameters),
|
||||
summary, dba, interpreter_context, execution_memory, memory_limit, optional_username,
|
||||
// We want to execute the query we are profiling lazily, so we delay
|
||||
// the construction of the corresponding context.
|
||||
stats_and_total_time = std::optional<plan::ProfilingStatsWithTotalTime>{},
|
||||
pull_plan = std::shared_ptr<PullPlanVector>(nullptr), transaction_status, use_monotonic_memory](
|
||||
AnyStream *stream, std::optional<int> n) mutable -> std::optional<QueryHandlerResult> {
|
||||
// No output symbols are given so that nothing is streamed.
|
||||
if (!stats_and_total_time) {
|
||||
stats_and_total_time = PullPlan(plan, parameters, true, dba, interpreter_context,
|
||||
execution_memory, optional_username, transaction_status,
|
||||
nullptr, memory_limit, use_monotonic_memory)
|
||||
.Pull(stream, {}, {}, summary);
|
||||
pull_plan = std::make_shared<PullPlanVector>(ProfilingStatsToTable(*stats_and_total_time));
|
||||
}
|
||||
return PreparedQuery{
|
||||
{"OPERATOR", "ACTUAL HITS", "RELATIVE TIME", "ABSOLUTE TIME"},
|
||||
std::move(parsed_query.required_privileges),
|
||||
[plan = std::move(cypher_query_plan), parameters = std::move(parsed_inner_query.parameters), summary, dba,
|
||||
interpreter_context, execution_memory, memory_limit, optional_username,
|
||||
// We want to execute the query we are profiling lazily, so we delay
|
||||
// the construction of the corresponding context.
|
||||
stats_and_total_time = std::optional<plan::ProfilingStatsWithTotalTime>{},
|
||||
pull_plan = std::shared_ptr<PullPlanVector>(nullptr), transaction_status, use_monotonic_memory,
|
||||
frame_change_collector](AnyStream *stream, std::optional<int> n) mutable -> std::optional<QueryHandlerResult> {
|
||||
// No output symbols are given so that nothing is streamed.
|
||||
if (!stats_and_total_time) {
|
||||
stats_and_total_time =
|
||||
PullPlan(plan, parameters, true, dba, interpreter_context, execution_memory, optional_username,
|
||||
transaction_status, nullptr, memory_limit, use_monotonic_memory,
|
||||
frame_change_collector->IsTrackingValues() ? frame_change_collector : nullptr)
|
||||
.Pull(stream, {}, {}, summary);
|
||||
pull_plan = std::make_shared<PullPlanVector>(ProfilingStatsToTable(*stats_and_total_time));
|
||||
}
|
||||
|
||||
MG_ASSERT(stats_and_total_time, "Failed to execute the query!");
|
||||
MG_ASSERT(stats_and_total_time, "Failed to execute the query!");
|
||||
|
||||
if (pull_plan->Pull(stream, n)) {
|
||||
summary->insert_or_assign("profile", ProfilingStatsToJson(*stats_and_total_time).dump());
|
||||
return QueryHandlerResult::ABORT;
|
||||
}
|
||||
if (pull_plan->Pull(stream, n)) {
|
||||
summary->insert_or_assign("profile", ProfilingStatsToJson(*stats_and_total_time).dump());
|
||||
return QueryHandlerResult::ABORT;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
},
|
||||
rw_type_checker.type};
|
||||
return std::nullopt;
|
||||
},
|
||||
rw_type_checker.type};
|
||||
}
|
||||
|
||||
PreparedQuery PrepareDumpQuery(ParsedQuery parsed_query, std::map<std::string, TypedValue> *summary, DbAccessor *dba,
|
||||
@@ -2212,6 +2257,14 @@ std::vector<std::vector<TypedValue>> TransactionQueueQueryHandler::ShowTransacti
|
||||
const auto &typed_queries = interpreter->GetQueries();
|
||||
results.push_back({TypedValue(interpreter->username_.value_or("")),
|
||||
TypedValue(std::to_string(transaction_id.value())), TypedValue(typed_queries)});
|
||||
// Handle user-defined metadata
|
||||
std::map<std::string, TypedValue> metadata_tv;
|
||||
if (interpreter->metadata_) {
|
||||
for (const auto &md : *(interpreter->metadata_)) {
|
||||
metadata_tv.emplace(md.first, TypedValue(md.second));
|
||||
}
|
||||
}
|
||||
results.back().push_back(TypedValue(metadata_tv));
|
||||
}
|
||||
}
|
||||
return results;
|
||||
@@ -2281,7 +2334,7 @@ Callback HandleTransactionQueueQuery(TransactionQueueQuery *transaction_query,
|
||||
Callback callback;
|
||||
switch (transaction_query->action_) {
|
||||
case TransactionQueueQuery::Action::SHOW_TRANSACTIONS: {
|
||||
callback.header = {"username", "transaction_id", "query"};
|
||||
callback.header = {"username", "transaction_id", "query", "metadata"};
|
||||
callback.fn = [handler = TransactionQueueQueryHandler(), interpreter_context, username,
|
||||
hasTransactionManagementPrivilege]() mutable {
|
||||
std::vector<std::vector<TypedValue>> results;
|
||||
@@ -2717,8 +2770,8 @@ std::optional<uint64_t> Interpreter::GetTransactionId() const {
|
||||
return {};
|
||||
}
|
||||
|
||||
void Interpreter::BeginTransaction() {
|
||||
const auto prepared_query = PrepareTransactionQuery("BEGIN");
|
||||
void Interpreter::BeginTransaction(const std::map<std::string, storage::PropertyValue> &metadata) {
|
||||
const auto prepared_query = PrepareTransactionQuery("BEGIN", metadata);
|
||||
prepared_query.query_handler(nullptr, {});
|
||||
}
|
||||
|
||||
@@ -2738,10 +2791,13 @@ void Interpreter::RollbackTransaction() {
|
||||
|
||||
Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
const std::map<std::string, storage::PropertyValue> ¶ms,
|
||||
const std::string *username) {
|
||||
const std::string *username,
|
||||
const std::map<std::string, storage::PropertyValue> &metadata) {
|
||||
if (!in_explicit_transaction_) {
|
||||
query_executions_.clear();
|
||||
transaction_queries_->clear();
|
||||
// Handle user-defined metadata in auto-transactions
|
||||
metadata_ = GenOptional(metadata);
|
||||
}
|
||||
|
||||
// This will be done in the handle transaction query. Our handler can save username and then send it to the kill and
|
||||
@@ -2761,7 +2817,7 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
std::optional<int> qid =
|
||||
in_explicit_transaction_ ? static_cast<int>(query_executions_.size() - 1) : std::optional<int>{};
|
||||
|
||||
query_execution->prepared_query.emplace(PrepareTransactionQuery(trimmed_query));
|
||||
query_execution->prepared_query.emplace(PrepareTransactionQuery(trimmed_query, metadata));
|
||||
return {query_execution->prepared_query->header, query_execution->prepared_query->privileges, qid};
|
||||
}
|
||||
|
||||
@@ -2843,18 +2899,21 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
utils::MemoryResource *memory_resource =
|
||||
std::visit([](auto &execution_memory) -> utils::MemoryResource * { return &execution_memory; },
|
||||
query_execution->execution_memory);
|
||||
frame_change_collector_.reset();
|
||||
frame_change_collector_.emplace(memory_resource);
|
||||
if (utils::Downcast<CypherQuery>(parsed_query.query)) {
|
||||
prepared_query =
|
||||
PrepareCypherQuery(std::move(parsed_query), &query_execution->summary, interpreter_context_,
|
||||
&*execution_db_accessor_, memory_resource, &query_execution->notifications, username,
|
||||
&transaction_status_, trigger_context_collector_ ? &*trigger_context_collector_ : nullptr);
|
||||
prepared_query = PrepareCypherQuery(
|
||||
std::move(parsed_query), &query_execution->summary, interpreter_context_, &*execution_db_accessor_,
|
||||
memory_resource, &query_execution->notifications, username, &transaction_status_,
|
||||
trigger_context_collector_ ? &*trigger_context_collector_ : nullptr, &*frame_change_collector_);
|
||||
} else if (utils::Downcast<ExplainQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareExplainQuery(std::move(parsed_query), &query_execution->summary, interpreter_context_,
|
||||
&*execution_db_accessor_, &query_execution->execution_memory_with_exception);
|
||||
} else if (utils::Downcast<ProfileQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareProfileQuery(
|
||||
std::move(parsed_query), in_explicit_transaction_, &query_execution->summary, interpreter_context_,
|
||||
&*execution_db_accessor_, &query_execution->execution_memory_with_exception, username, &transaction_status_);
|
||||
prepared_query = PrepareProfileQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary,
|
||||
interpreter_context_, &*execution_db_accessor_,
|
||||
&query_execution->execution_memory_with_exception, username,
|
||||
&transaction_status_, &*frame_change_collector_);
|
||||
} else if (utils::Downcast<DumpQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareDumpQuery(std::move(parsed_query), &query_execution->summary, &*execution_db_accessor_,
|
||||
memory_resource);
|
||||
@@ -2962,6 +3021,7 @@ void Interpreter::Abort() {
|
||||
|
||||
expect_rollback_ = false;
|
||||
in_explicit_transaction_ = false;
|
||||
metadata_ = std::nullopt;
|
||||
|
||||
memgraph::metrics::DecrementCounter(memgraph::metrics::ActiveTransactions);
|
||||
|
||||
@@ -2971,6 +3031,7 @@ void Interpreter::Abort() {
|
||||
execution_db_accessor_.reset();
|
||||
db_accessor_.reset();
|
||||
trigger_context_collector_.reset();
|
||||
frame_change_collector_.reset();
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -3073,6 +3134,10 @@ void Interpreter::Commit() {
|
||||
trigger_context_collector_.reset();
|
||||
}
|
||||
|
||||
if (frame_change_collector_) {
|
||||
frame_change_collector_.reset();
|
||||
}
|
||||
|
||||
if (trigger_context) {
|
||||
// Run the triggers
|
||||
for (const auto &trigger : interpreter_context_->trigger_store.BeforeCommitTriggers().access()) {
|
||||
|
||||
@@ -261,6 +261,7 @@ class Interpreter final {
|
||||
std::optional<std::string> username_;
|
||||
bool in_explicit_transaction_{false};
|
||||
bool expect_rollback_{false};
|
||||
std::optional<std::map<std::string, storage::PropertyValue>> metadata_{}; //!< User defined transaction metadata
|
||||
|
||||
/**
|
||||
* Prepare a query for execution.
|
||||
@@ -271,7 +272,8 @@ class Interpreter final {
|
||||
* @throw query::QueryException
|
||||
*/
|
||||
PrepareResult Prepare(const std::string &query, const std::map<std::string, storage::PropertyValue> ¶ms,
|
||||
const std::string *username);
|
||||
const std::string *username,
|
||||
const std::map<std::string, storage::PropertyValue> &metadata = {});
|
||||
|
||||
/**
|
||||
* Execute the last prepared query and stream *all* of the results into the
|
||||
@@ -315,7 +317,7 @@ class Interpreter final {
|
||||
std::map<std::string, TypedValue> Pull(TStream *result_stream, std::optional<int> n = {},
|
||||
std::optional<int> qid = {});
|
||||
|
||||
void BeginTransaction();
|
||||
void BeginTransaction(const std::map<std::string, storage::PropertyValue> &metadata = {});
|
||||
|
||||
/*
|
||||
Returns transaction id or empty if the db_accessor is not initialized.
|
||||
@@ -401,11 +403,13 @@ class Interpreter final {
|
||||
std::unique_ptr<storage::Storage::Accessor> db_accessor_;
|
||||
std::optional<DbAccessor> execution_db_accessor_;
|
||||
std::optional<TriggerContextCollector> trigger_context_collector_;
|
||||
std::optional<FrameChangeCollector> frame_change_collector_;
|
||||
|
||||
std::optional<storage::IsolationLevel> interpreter_isolation_level;
|
||||
std::optional<storage::IsolationLevel> next_transaction_isolation_level;
|
||||
|
||||
PreparedQuery PrepareTransactionQuery(std::string_view query_upper);
|
||||
PreparedQuery PrepareTransactionQuery(std::string_view query_upper,
|
||||
const std::map<std::string, storage::PropertyValue> &metadata = {});
|
||||
void Commit();
|
||||
void AdvanceCommand();
|
||||
void AbortCommand(std::unique_ptr<QueryExecution> *query_execution);
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
#include "utils/readable_size.hpp"
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/temporal.hpp"
|
||||
#include "utils/typeinfo.hpp"
|
||||
|
||||
// macro for the default implementation of LogicalOperator::Accept
|
||||
// that accepts the visitor and visits it's input_ operator
|
||||
@@ -2332,12 +2333,11 @@ bool Filter::FilterCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
// Like all filters, newly set values should not affect filtering of old
|
||||
// nodes and edges.
|
||||
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
|
||||
storage::View::OLD);
|
||||
storage::View::OLD, context.frame_change_collector);
|
||||
while (input_cursor_->Pull(frame, context)) {
|
||||
for (const auto &pattern_filter_cursor : pattern_filter_cursors_) {
|
||||
pattern_filter_cursor->Pull(frame, context);
|
||||
}
|
||||
|
||||
if (EvaluateFilter(evaluator, self_.expression_)) return true;
|
||||
}
|
||||
return false;
|
||||
@@ -2410,9 +2410,13 @@ bool Produce::ProduceCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
if (input_cursor_->Pull(frame, context)) {
|
||||
// Produce should always yield the latest results.
|
||||
ExpressionEvaluator evaluator(&frame, context.symbol_table, context.evaluation_context, context.db_accessor,
|
||||
storage::View::NEW);
|
||||
for (auto named_expr : self_.named_expressions_) named_expr->Accept(evaluator);
|
||||
|
||||
storage::View::NEW, context.frame_change_collector);
|
||||
for (auto *named_expr : self_.named_expressions_) {
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(named_expr->name_)) {
|
||||
context.frame_change_collector->ResetTrackingValue(named_expr->name_);
|
||||
}
|
||||
named_expr->Accept(evaluator);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -3233,7 +3237,12 @@ class AccumulateCursor : public Cursor {
|
||||
if (MustAbort(context)) throw HintedAbortError();
|
||||
if (cache_it_ == cache_.end()) return false;
|
||||
auto row_it = (cache_it_++)->begin();
|
||||
for (const Symbol &symbol : self_.symbols_) frame[symbol] = *row_it++;
|
||||
for (const Symbol &symbol : self_.symbols_) {
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(symbol.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(symbol.name());
|
||||
}
|
||||
frame[symbol] = *row_it++;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3315,10 +3324,20 @@ class AggregateCursor : public Cursor {
|
||||
if (aggregation_.empty()) {
|
||||
auto *pull_memory = context.evaluation_context.memory;
|
||||
// place default aggregation values on the frame
|
||||
for (const auto &elem : self_.aggregations_)
|
||||
for (const auto &elem : self_.aggregations_) {
|
||||
frame[elem.output_sym] = DefaultAggregationOpValue(elem, pull_memory);
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(elem.output_sym.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(elem.output_sym.name());
|
||||
}
|
||||
}
|
||||
|
||||
// place null as remember values on the frame
|
||||
for (const Symbol &remember_sym : self_.remember_) frame[remember_sym] = TypedValue(pull_memory);
|
||||
for (const Symbol &remember_sym : self_.remember_) {
|
||||
frame[remember_sym] = TypedValue(pull_memory);
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(remember_sym.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(remember_sym.name());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -3798,8 +3817,12 @@ class OrderByCursor : public Cursor {
|
||||
"Number of values does not match the number of output symbols "
|
||||
"in OrderBy");
|
||||
auto output_sym_it = self_.output_symbols_.begin();
|
||||
for (const TypedValue &output : cache_it_->remember) frame[*output_sym_it++] = output;
|
||||
|
||||
for (const TypedValue &output : cache_it_->remember) {
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(output_sym_it->name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(output_sym_it->name());
|
||||
}
|
||||
frame[*output_sym_it++] = output;
|
||||
}
|
||||
cache_it_++;
|
||||
return true;
|
||||
}
|
||||
@@ -4032,6 +4055,9 @@ class UnwindCursor : public Cursor {
|
||||
if (input_value_it_ == input_value_.end()) continue;
|
||||
|
||||
frame[self_.output_symbol_] = *input_value_it_++;
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(self_.output_symbol_.name_)) {
|
||||
context.frame_change_collector->ResetTrackingValue(self_.output_symbol_.name_);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -4161,11 +4187,17 @@ bool Union::UnionCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
// collect values from the left child
|
||||
for (const auto &output_symbol : self_.left_symbols_) {
|
||||
results[output_symbol.name()] = frame[output_symbol];
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(output_symbol.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(output_symbol.name());
|
||||
}
|
||||
}
|
||||
} else if (right_cursor_->Pull(frame, context)) {
|
||||
// collect values from the right child
|
||||
for (const auto &output_symbol : self_.right_symbols_) {
|
||||
results[output_symbol.name()] = frame[output_symbol];
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(output_symbol.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(output_symbol.name());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
@@ -4174,6 +4206,9 @@ bool Union::UnionCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
// put collected values on frame under union symbols
|
||||
for (const auto &symbol : self_.union_symbols_) {
|
||||
frame[symbol] = results[symbol.name()];
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(symbol.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(symbol.name());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -4238,9 +4273,12 @@ class CartesianCursor : public Cursor {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto restore_frame = [&frame](const auto &symbols, const auto &restore_from) {
|
||||
auto restore_frame = [&frame, &context](const auto &symbols, const auto &restore_from) {
|
||||
for (const auto &symbol : symbols) {
|
||||
frame[symbol] = restore_from[symbol.position()];
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(symbol.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(symbol.name());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -4318,6 +4356,10 @@ class OutputTableCursor : public Cursor {
|
||||
if (current_row_ < rows_.size()) {
|
||||
for (size_t i = 0; i < self_.output_symbols_.size(); ++i) {
|
||||
frame[self_.output_symbols_[i]] = rows_[current_row_][i];
|
||||
if (context.frame_change_collector &&
|
||||
context.frame_change_collector->IsKeyTracked(self_.output_symbols_[i].name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(self_.output_symbols_[i].name());
|
||||
}
|
||||
}
|
||||
current_row_++;
|
||||
return true;
|
||||
@@ -4361,6 +4403,10 @@ class OutputTableStreamCursor : public Cursor {
|
||||
MG_ASSERT(row->size() == self_->output_symbols_.size(), "Wrong number of columns in row!");
|
||||
for (size_t i = 0; i < self_->output_symbols_.size(); ++i) {
|
||||
frame[self_->output_symbols_[i]] = row->at(i);
|
||||
if (context.frame_change_collector &&
|
||||
context.frame_change_collector->IsKeyTracked(self_->output_symbols_[i].name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(self_->output_symbols_[i].name());
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -4564,6 +4610,10 @@ class CallProcedureCursor : public Cursor {
|
||||
field_name);
|
||||
}
|
||||
frame[self_->result_symbols_[i]] = std::move(result_it->second);
|
||||
if (context.frame_change_collector &&
|
||||
context.frame_change_collector->IsKeyTracked(self_->result_symbols_[i].name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(self_->result_symbols_[i].name());
|
||||
}
|
||||
}
|
||||
++result_row_it_;
|
||||
|
||||
@@ -4690,6 +4740,9 @@ class LoadCsvCursor : public Cursor {
|
||||
frame[self_->row_var_] =
|
||||
CsvRowToTypedMap(*row, csv::Reader::Header(reader_->GetHeader(), context.evaluation_context.memory));
|
||||
}
|
||||
if (context.frame_change_collector && context.frame_change_collector->IsKeyTracked(self_->row_var_.name())) {
|
||||
context.frame_change_collector->ResetTrackingValue(self_->row_var_.name());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
22
src/utils/frame_change_id.hpp
Normal file
22
src/utils/frame_change_id.hpp
Normal file
@@ -0,0 +1,22 @@
|
||||
#include <string>
|
||||
|
||||
#include "query/frontend/ast/ast.hpp"
|
||||
#include "spdlog/spdlog.h"
|
||||
|
||||
namespace memgraph::utils {
|
||||
|
||||
// Get ID by which FrameChangeCollector struct can cache in_list.expression2_
|
||||
inline std::optional<std::string> GetFrameChangeId(memgraph::query::InListOperator &in_list) {
|
||||
if (in_list.expression2_->GetTypeInfo() == memgraph::query::ListLiteral::kType) {
|
||||
std::stringstream ss;
|
||||
ss << static_cast<const void *>(in_list.expression2_);
|
||||
return ss.str();
|
||||
}
|
||||
if (in_list.expression2_->GetTypeInfo() == memgraph::query::Identifier::kType) {
|
||||
auto *identifier = utils::Downcast<memgraph::query::Identifier>(in_list.expression2_);
|
||||
return identifier->name_;
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
} // namespace memgraph::utils
|
||||
12
tests/drivers/csharp/v4_1/Metadata/Metadata.csproj
Normal file
12
tests/drivers/csharp/v4_1/Metadata/Metadata.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Neo4j.Driver.Simple" Version="4.1.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
75
tests/drivers/csharp/v4_1/Metadata/Program.cs
Normal file
75
tests/drivers/csharp/v4_1/Metadata/Program.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Neo4j.Driver;
|
||||
|
||||
public class Transactions {
|
||||
public static void Main(string[] args) {
|
||||
var driver =
|
||||
GraphDatabase.Driver("bolt://localhost:7687", AuthTokens.None,
|
||||
(builder) => builder.WithEncryptionLevel(EncryptionLevel.None));
|
||||
ClearDatabase(driver);
|
||||
// Explicit transaction query.
|
||||
using (var session = driver.Session()) {
|
||||
Console.WriteLine("Checking explicit transaction metadata...");
|
||||
var txMetadata = new Dictionary<string, object> {
|
||||
{ "ver", "transaction" }, { "str", "oho" }, { "num", 456 }
|
||||
};
|
||||
using (var tx = session.BeginTransaction(txConfig => txConfig.WithMetadata(txMetadata))) {
|
||||
tx.Run("MATCH (n) RETURN n LIMIT 1").Consume();
|
||||
// Check transaction info from another thread
|
||||
Thread show_tx = new Thread(() => ShowTx(ref driver));
|
||||
show_tx.Start();
|
||||
show_tx.Join();
|
||||
// End current transaction
|
||||
tx.Commit();
|
||||
}
|
||||
}
|
||||
// Implicit transaction query
|
||||
using (var session = driver.Session()) {
|
||||
Console.WriteLine("Checking implicit transaction metadata...");
|
||||
var txMetadata = new Dictionary<string, object> {
|
||||
{ "ver", "session" }, { "str", "aha" }, { "num", 123 }
|
||||
};
|
||||
CheckMD(session.Run("SHOW TRANSACTIONS", txConfig => txConfig.WithMetadata(txMetadata)));
|
||||
}
|
||||
|
||||
Console.WriteLine("All ok!");
|
||||
}
|
||||
|
||||
private static void ClearDatabase(IDriver driver) {
|
||||
using (var session = driver.Session()) session.Run("MATCH (n) DETACH DELETE n").Consume();
|
||||
}
|
||||
|
||||
public static void ShowTx(ref IDriver driver) {
|
||||
using (var session = driver.Session()) {
|
||||
CheckMD(session.Run("SHOW TRANSACTIONS"));
|
||||
}
|
||||
}
|
||||
|
||||
public static void CheckMD(IResult tx_md) {
|
||||
int n = 0;
|
||||
try {
|
||||
foreach (var res in tx_md) {
|
||||
var md = res["metadata"].As<Dictionary<string, object>>();
|
||||
if (md.Count != 0) {
|
||||
if (md["ver"].As<string>() == "transaction" && md["str"].As<string>() == "oho" &&
|
||||
md["num"].As<int>() == 456) {
|
||||
n = n + 1;
|
||||
} else if (md["ver"].As<string>() == "session" && md["str"].As<string>() == "aha" &&
|
||||
md["num"].As<int>() == 123) {
|
||||
n = n + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
n = 0;
|
||||
}
|
||||
if (n == 0) {
|
||||
Console.WriteLine("Metadata error!");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Neo4j.Driver.Simple" Version="5.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
35
tests/drivers/csharp/v5_8/DocsHowToQuery/Program.cs
Normal file
35
tests/drivers/csharp/v5_8/DocsHowToQuery/Program.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Neo4j.Driver;
|
||||
|
||||
public class Basic {
|
||||
public static void Main(string[] args) {
|
||||
using (var driver = GraphDatabase.Driver(
|
||||
"bolt://localhost:7687", AuthTokens.None,
|
||||
(ConfigBuilder builder) => builder.WithEncryptionLevel(
|
||||
EncryptionLevel.None))) using (var session = driver.Session()) {
|
||||
session.Run("MATCH (n) DETACH DELETE n;").Consume();
|
||||
Console.WriteLine("Database cleared.");
|
||||
|
||||
session.Run("CREATE (alice:Person {name: \"Alice\", age: 22});").Consume();
|
||||
Console.WriteLine("Record created.");
|
||||
|
||||
var node = (INode)session.Run("MATCH (n) RETURN n;").First()["n"];
|
||||
Console.WriteLine("Record matched.");
|
||||
|
||||
var label = string.Join("", node.Labels);
|
||||
var name = node["name"];
|
||||
var age = (long)node["age"];
|
||||
|
||||
if (!label.Equals("Person") || !name.Equals("Alice") || !age.Equals(22)) {
|
||||
Console.WriteLine("Data doesn't match!");
|
||||
System.Environment.Exit(1);
|
||||
}
|
||||
|
||||
Console.WriteLine("Label: " + label);
|
||||
Console.WriteLine("name: " + name);
|
||||
Console.WriteLine("age: " + age);
|
||||
}
|
||||
Console.WriteLine("All ok!");
|
||||
}
|
||||
}
|
||||
64
tests/drivers/csharp/v5_8/Transactions/Program.cs
Normal file
64
tests/drivers/csharp/v5_8/Transactions/Program.cs
Normal file
@@ -0,0 +1,64 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using Neo4j.Driver;
|
||||
|
||||
public class Transactions {
|
||||
public static void Main(string[] args) {
|
||||
using (var driver = GraphDatabase.Driver(
|
||||
"bolt://localhost:7687", AuthTokens.None,
|
||||
(builder) => builder.WithEncryptionLevel(EncryptionLevel.None))) {
|
||||
ClearDatabase(driver);
|
||||
// Wrong query.
|
||||
try {
|
||||
using (var session = driver.Session()) using (var tx = session.BeginTransaction()) {
|
||||
CreatePerson(tx, "mirko");
|
||||
// Incorrectly start CREATE
|
||||
tx.Run("CREATE (").Consume();
|
||||
CreatePerson(tx, "slavko");
|
||||
tx.Commit();
|
||||
}
|
||||
} catch (ClientException) {
|
||||
Console.WriteLine("Rolled back transaction");
|
||||
}
|
||||
Trace.Assert(CountNodes(driver) == 0, "Expected transaction was rolled back.");
|
||||
// Correct query.
|
||||
using (var session = driver.Session()) using (var tx = session.BeginTransaction()) {
|
||||
CreatePerson(tx, "mirka");
|
||||
CreatePerson(tx, "slavka");
|
||||
tx.Commit();
|
||||
}
|
||||
Trace.Assert(CountNodes(driver) == 2, "Expected 2 created nodes.");
|
||||
ClearDatabase(driver);
|
||||
using (var session = driver.Session()) {
|
||||
// Create a lot of nodes so that the next read takes a long time.
|
||||
session.Run("UNWIND range(1, 100000) AS i CREATE ()").Consume();
|
||||
try {
|
||||
Console.WriteLine("Running a long read...");
|
||||
session.Run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt").Consume();
|
||||
} catch (TransientException) {
|
||||
Console.WriteLine("Transaction timed out");
|
||||
}
|
||||
}
|
||||
}
|
||||
Console.WriteLine("All ok!");
|
||||
}
|
||||
|
||||
private static void CreatePerson(ITransaction tx, string name) {
|
||||
var parameters = new Dictionary<string, Object> { { "name", name } };
|
||||
var result = tx.Run("CREATE (person:Person {name: $name}) RETURN person", parameters);
|
||||
Console.WriteLine("Created: " + ((INode)result.First()["person"])["name"]);
|
||||
}
|
||||
|
||||
private static void ClearDatabase(IDriver driver) {
|
||||
using (var session = driver.Session()) session.Run("MATCH (n) DETACH DELETE n").Consume();
|
||||
}
|
||||
|
||||
private static int CountNodes(IDriver driver) {
|
||||
using (var session = driver.Session()) {
|
||||
var result = session.Run("MATCH (n) RETURN COUNT(*) AS cnt");
|
||||
return Convert.ToInt32(result.First()["cnt"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
12
tests/drivers/csharp/v5_8/Transactions/Transactions.csproj
Normal file
12
tests/drivers/csharp/v5_8/Transactions/Transactions.csproj
Normal file
@@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Neo4j.Driver.Simple" Version="5.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
22
tests/drivers/csharp/v5_8/run.sh
Executable file
22
tests/drivers/csharp/v5_8/run.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$DIR"
|
||||
|
||||
# check if dotnet-sdk-2.1 is installed
|
||||
for i in dotnet; do
|
||||
if ! which $i >/dev/null; then
|
||||
echo "Please install $i!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
for i in *; do
|
||||
if [ ! -d $i ]; then
|
||||
continue
|
||||
fi
|
||||
pushd $i
|
||||
dotnet publish -c release --self-contained --runtime linux-x64 --framework netcoreapp2.1 -o build/
|
||||
./build/$i
|
||||
popd
|
||||
done;
|
||||
90
tests/drivers/go/v4/metadata.go
Normal file
90
tests/drivers/go/v4/metadata.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package main
|
||||
|
||||
import "github.com/neo4j/neo4j-go-driver/neo4j"
|
||||
import "log"
|
||||
import "fmt"
|
||||
|
||||
func handle_error(err error) {
|
||||
log.Fatal("Error occured: %s", err)
|
||||
}
|
||||
|
||||
func check_md(result neo4j.Result, err error) {
|
||||
if err != nil {
|
||||
handle_error(err)
|
||||
}
|
||||
n := 0
|
||||
for result.Next() {
|
||||
md, ok := result.Record().Get("metadata")
|
||||
if !ok {
|
||||
log.Fatal("Failed to read metadata!")
|
||||
}
|
||||
md_map, ok := md.(map[string]interface{})
|
||||
if ok {
|
||||
ver_val, ver_ok := md_map["ver"]
|
||||
str_val, str_ok := md_map["str"]
|
||||
num_val, num_ok := md_map["num"]
|
||||
if (ver_ok && str_ok && num_ok) {
|
||||
if ((ver_val.(string) == "session" && str_val.(string) == "aha" && num_val.(int64) == 123) ||
|
||||
(ver_val.(string) == "transaction" && str_val.(string) == "oho" && num_val.(int64) == 456)) {
|
||||
n++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if n == 0 {
|
||||
log.Fatal("Wrong metadata values!")
|
||||
}
|
||||
_, err = result.Consume()
|
||||
if err != nil {
|
||||
handle_error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func check_tx(driver neo4j.Driver) {
|
||||
sessionConfig := neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}
|
||||
session, err := driver.NewSession(sessionConfig)
|
||||
if err != nil {
|
||||
log.Fatal("An error occurred while creating a session: %s", err)
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
|
||||
result, err := session.Run("SHOW TRANSACTIONS", nil)
|
||||
check_md(result, err)
|
||||
}
|
||||
|
||||
func main() {
|
||||
configForNeo4j40 := func(conf *neo4j.Config) { conf.Encrypted = false }
|
||||
|
||||
driver, err := neo4j.NewDriver("bolt://localhost:7687", neo4j.BasicAuth("", "", ""), configForNeo4j40)
|
||||
if err != nil {
|
||||
log.Fatal("An error occurred opening conn: %s", err)
|
||||
}
|
||||
|
||||
defer driver.Close()
|
||||
|
||||
sessionConfig := neo4j.SessionConfig{AccessMode: neo4j.AccessModeWrite}
|
||||
session, err := driver.NewSession(sessionConfig)
|
||||
if err != nil {
|
||||
log.Fatal("An error occurred while creating a session: %s", err)
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
|
||||
// Implicit transaction
|
||||
fmt.Println("Checking implicit transaction metadata...")
|
||||
result, err := session.Run("SHOW TRANSACTIONS", nil, neo4j.WithTxMetadata(map[string]interface{}{"ver":"session", "str":"aha", "num":123}))
|
||||
check_md(result, err)
|
||||
|
||||
// Explicit transaction
|
||||
fmt.Println("Checking explicit transaction metadata...")
|
||||
tx, err := session.BeginTransaction(neo4j.WithTxMetadata(map[string]interface{}{"ver":"transaction", "str":"oho", "num":456}))
|
||||
if err != nil {
|
||||
handle_error(err)
|
||||
}
|
||||
tx.Run("MATCH (n) RETURN n LIMIT 1", map[string]interface{}{})
|
||||
go check_tx(driver)
|
||||
tx.Commit()
|
||||
|
||||
fmt.Println("All ok!")
|
||||
}
|
||||
@@ -12,3 +12,4 @@ go get github.com/neo4j/neo4j-go-driver/neo4j
|
||||
|
||||
go run docs_how_to_query.go
|
||||
go run transactions.go
|
||||
go run metadata.go
|
||||
|
||||
89
tests/drivers/go/v5/docs_quick_start.go
Normal file
89
tests/drivers/go/v5/docs_quick_start.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"fmt"
|
||||
"github.com/neo4j/neo4j-go-driver/v5/neo4j"
|
||||
)
|
||||
|
||||
func handle_if_error(err error) {
|
||||
if err != nil {
|
||||
log.Fatal("Error occured: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
dbUri := "bolt://localhost:7687"
|
||||
driver, err := neo4j.NewDriver(dbUri, neo4j.BasicAuth("", "", ""))
|
||||
if err != nil {
|
||||
log.Fatal("An error occurred opening conn: %s", err)
|
||||
}
|
||||
defer driver.Close()
|
||||
|
||||
session := driver.NewSession(neo4j.SessionConfig{})
|
||||
defer session.Close()
|
||||
|
||||
_, err = session.WriteTransaction(clearDatabase)
|
||||
handle_if_error(err)
|
||||
fmt.Println("Database cleared.")
|
||||
|
||||
_, err = session.WriteTransaction(createItemFn)
|
||||
handle_if_error(err)
|
||||
fmt.Println("Record created.")
|
||||
|
||||
_,err = session.WriteTransaction(testAll)
|
||||
handle_if_error(err)
|
||||
fmt.Println("All ok!")
|
||||
}
|
||||
|
||||
func clearDatabase(tx neo4j.Transaction) (interface{}, error) {
|
||||
result, err := tx.Run(
|
||||
"MATCH (n) DETACH DELETE n;",
|
||||
map[string]interface{}{})
|
||||
handle_if_error(err)
|
||||
return result.Consume()
|
||||
}
|
||||
|
||||
func createItemFn(tx neo4j.Transaction) (interface{}, error) {
|
||||
result, err := tx.Run(
|
||||
`CREATE (alice:Person {name: "Alice", age: 22});`,
|
||||
map[string]interface{}{})
|
||||
handle_if_error(err)
|
||||
return result.Consume()
|
||||
}
|
||||
|
||||
func testAll(tx neo4j.Transaction) (interface{}, error) {
|
||||
result, err := tx.Run(
|
||||
"MATCH (n) RETURN n;",
|
||||
map[string]interface{}{})
|
||||
handle_if_error(err)
|
||||
|
||||
if !result.Next() {
|
||||
log.Fatal("Missing result.")
|
||||
}
|
||||
|
||||
node_record, found := result.Record().Get("n")
|
||||
if !found {
|
||||
return nil, fmt.Errorf("Wrong result returned.")
|
||||
}
|
||||
|
||||
node_value := node_record.(neo4j.Node)
|
||||
fmt.Println("Record matched.")
|
||||
|
||||
label := node_value.Labels[0]
|
||||
name, err := neo4j.GetProperty[string](node_value, "name")
|
||||
handle_if_error(err)
|
||||
age, err := neo4j.GetProperty[int64](node_value, "age")
|
||||
handle_if_error(err)
|
||||
|
||||
if label != "Person" && name != "Alice" && age != 22 {
|
||||
return nil, fmt.Errorf("Data doesn't match.")
|
||||
}
|
||||
|
||||
fmt.Println("Label", label)
|
||||
fmt.Println("name", name)
|
||||
fmt.Println("age", age)
|
||||
|
||||
return result.Consume()
|
||||
}
|
||||
|
||||
8
tests/drivers/go/v5/go.mod
Normal file
8
tests/drivers/go/v5/go.mod
Normal file
@@ -0,0 +1,8 @@
|
||||
module bolt-test
|
||||
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.9.0 // indirect
|
||||
golang.org/dl v0.0.0-20230502172222-5216546bad51 // indirect
|
||||
)
|
||||
10
tests/drivers/go/v5/go.sum
Normal file
10
tests/drivers/go/v5/go.sum
Normal file
@@ -0,0 +1,10 @@
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.5.0 h1:KxufacDV+IqkzbzvjIAIGkBsa2i0lEB8/MhCgOQxrQo=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.5.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.6.0 h1:+LxOHCyDWGjtD8qHhb20GUpvwCFcJm1wqSEyo2MiehE=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.6.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.8.1 h1:IysKg6KJIUgyItmnHRRrt2N8srbd6znMslRW3qQErTQ=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.8.1/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.9.0 h1:TYxT0RSiwnvVFia90V7TLnRXv8HkdQQ6rTUaPVoyZ+w=
|
||||
github.com/neo4j/neo4j-go-driver/v5 v5.9.0/go.mod h1:Vff8OwT7QpLm7L2yYr85XNWe9Rbqlbeb9asNXJTHO4k=
|
||||
golang.org/dl v0.0.0-20230502172222-5216546bad51 h1:Bmo/kmR2hzyhGt3jjtl1ghkCqa5LINbB9D3QTkiLJIY=
|
||||
golang.org/dl v0.0.0-20230502172222-5216546bad51/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
|
||||
20
tests/drivers/go/v5/run.sh
Executable file
20
tests/drivers/go/v5/run.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
GO_VERSION="1.18.9"
|
||||
GO_VERSION_DIR="/opt/go$GO_VERSION"
|
||||
if [ -f "$GO_VERSION_DIR/go/bin/go" ]; then
|
||||
export GOROOT="$GO_VERSION_DIR/go"
|
||||
export GOPATH="$HOME/go$GO_VERSION"
|
||||
export PATH="$GO_VERSION_DIR/go/bin:$PATH"
|
||||
fi
|
||||
|
||||
# check if go is installed
|
||||
for i in go; do
|
||||
if ! which $i >/dev/null; then
|
||||
echo "Please install $i!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
go get github.com/neo4j/neo4j-go-driver/v5
|
||||
go run docs_quick_start.go
|
||||
@@ -1,38 +1,39 @@
|
||||
import static org.neo4j.driver.v1.Values.parameters;
|
||||
|
||||
import java.util.*;
|
||||
import org.neo4j.driver.v1.*;
|
||||
import org.neo4j.driver.v1.types.*;
|
||||
import static org.neo4j.driver.v1.Values.parameters;
|
||||
import java.util.*;
|
||||
|
||||
public class Basic {
|
||||
public static void main(String[] args) {
|
||||
Config config = Config.build().withoutEncryption().toConfig();
|
||||
Driver driver = GraphDatabase.driver( "bolt://localhost:7687", AuthTokens.basic( "neo4j", "1234" ), config );
|
||||
public static void main(String[] args) {
|
||||
Config config = Config.build().withoutEncryption().toConfig();
|
||||
Driver driver =
|
||||
GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "1234"), config);
|
||||
|
||||
try ( Session session = driver.session() ) {
|
||||
StatementResult rs1 = session.run( "MATCH (n) DETACH DELETE n" );
|
||||
System.out.println( "Database cleared." );
|
||||
try (Session session = driver.session()) {
|
||||
StatementResult rs1 = session.run("MATCH (n) DETACH DELETE n");
|
||||
System.out.println("Database cleared.");
|
||||
|
||||
StatementResult rs2 = session.run( "CREATE (alice: Person {name: 'Alice', age: 22})" );
|
||||
System.out.println( "Record created." );
|
||||
StatementResult rs2 = session.run("CREATE (alice: Person {name: 'Alice', age: 22})");
|
||||
System.out.println("Record created.");
|
||||
|
||||
StatementResult rs3 = session.run( "MATCH (n) RETURN n" );
|
||||
System.out.println( "Record matched." );
|
||||
StatementResult rs3 = session.run("MATCH (n) RETURN n");
|
||||
System.out.println("Record matched.");
|
||||
|
||||
List<Record> records = rs3.list();
|
||||
Record record = records.get( 0 );
|
||||
Node node = record.get( "n" ).asNode();
|
||||
if ( !node.get("name").asString().equals( "Alice" ) || node.get("age").asInt() != 22 ) {
|
||||
System.out.println( "Data doesn't match!" );
|
||||
System.exit( 1 );
|
||||
}
|
||||
List<org.neo4j.driver.v1.Record> records = rs3.list();
|
||||
org.neo4j.driver.v1.Record record = records.get(0);
|
||||
Node node = record.get("n").asNode();
|
||||
if (!node.get("name").asString().equals("Alice") || node.get("age").asInt() != 22) {
|
||||
System.out.println("Data doesn't match!");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
System.out.println( "All ok!" );
|
||||
}
|
||||
catch ( Exception e ) {
|
||||
System.out.println( e );
|
||||
System.exit( 1 );
|
||||
}
|
||||
|
||||
driver.close();
|
||||
System.out.println("All ok!");
|
||||
} catch (Exception e) {
|
||||
System.out.println(e);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
|
||||
111
tests/drivers/java/v4_1/Metadata.java
Normal file
111
tests/drivers/java/v4_1/Metadata.java
Normal file
@@ -0,0 +1,111 @@
|
||||
import static org.neo4j.driver.Values.parameters;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Config;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.neo4j.driver.Record;
|
||||
import org.neo4j.driver.Result;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.neo4j.driver.TransactionConfig;
|
||||
import org.neo4j.driver.Value;
|
||||
import org.neo4j.driver.exceptions.ClientException;
|
||||
import org.neo4j.driver.exceptions.TransientException;
|
||||
|
||||
public class Metadata {
|
||||
public static String createPerson(Transaction tx, String name) {
|
||||
Result result =
|
||||
tx.run("CREATE (a:Person {name: $name}) RETURN a.name", parameters("name", name));
|
||||
return result.single().get(0).asString();
|
||||
}
|
||||
|
||||
public static void checkMd(Result result) {
|
||||
int n = 0;
|
||||
while (result.hasNext()) {
|
||||
Record r = result.next();
|
||||
Value md = r.get("metadata");
|
||||
if (md != null && Objects.equals(md.get("ver").asString(), "transaction")
|
||||
&& Objects.equals(md.get("str").asString(), "oho")
|
||||
&& Objects.equals(md.get("num").asInt(), 456)) {
|
||||
n = n + 1;
|
||||
} else if (md != null && Objects.equals(md.get("ver").asString(), "session")
|
||||
&& Objects.equals(md.get("str").asString(), "aha")
|
||||
&& Objects.equals(md.get("num").asInt(), 123)) {
|
||||
n = n + 1;
|
||||
}
|
||||
}
|
||||
if (n == 0) {
|
||||
System.out.println("Error while reading metadata!");
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Config config = Config.builder()
|
||||
.withoutEncryption()
|
||||
.withMaxTransactionRetryTime(0, TimeUnit.SECONDS)
|
||||
.build();
|
||||
Driver driver =
|
||||
GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "1234"), config);
|
||||
|
||||
try (Session session = driver.session()) {
|
||||
// Explicit transaction
|
||||
System.out.println("Checking explicit transaction metadata...");
|
||||
try {
|
||||
TransactionConfig tx_config =
|
||||
TransactionConfig.builder()
|
||||
.withTimeout(Duration.ofSeconds(2))
|
||||
.withMetadata(Map.ofEntries(Map.entry("ver", "transaction"),
|
||||
Map.entry("str", "oho"), Map.entry("num", 456)))
|
||||
.build();
|
||||
Transaction tx = session.beginTransaction(tx_config);
|
||||
tx.run("MATCH (n) RETURN n LIMIT 1");
|
||||
// Check the metadata from another thread
|
||||
try {
|
||||
Runnable checkTx = new Runnable() {
|
||||
public void run() {
|
||||
try (Session s = driver.session()) {
|
||||
checkMd(s.run("SHOW TRANSACTIONS"));
|
||||
} catch (ClientException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
Thread thread = new Thread(checkTx);
|
||||
thread.start();
|
||||
thread.join();
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
tx.commit();
|
||||
} catch (ClientException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
|
||||
// Implicit transaction
|
||||
System.out.println("Checking implicit transaction metadata...");
|
||||
try {
|
||||
TransactionConfig tx_config = TransactionConfig.builder()
|
||||
.withTimeout(Duration.ofSeconds(2))
|
||||
.withMetadata(Map.ofEntries(Map.entry("ver", "session"),
|
||||
Map.entry("str", "aha"), Map.entry("num", 123)))
|
||||
.build();
|
||||
checkMd(session.run("SHOW TRANSACTIONS", tx_config));
|
||||
} catch (ClientException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
|
||||
System.out.println("All ok!");
|
||||
|
||||
} catch (Exception e) {
|
||||
System.out.println(e);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
@@ -35,3 +35,6 @@ java -classpath .:$DRIVER:$REACTIVE_STREAM_DEP MaxQueryLength
|
||||
|
||||
javac -classpath .:$DRIVER:$REACTIVE_STREAM_DEP Transactions.java
|
||||
java -classpath .:$DRIVER:$REACTIVE_STREAM_DEP Transactions
|
||||
|
||||
javac -classpath .:$DRIVER:$REACTIVE_STREAM_DEP Metadata.java
|
||||
java -classpath .:$DRIVER:$REACTIVE_STREAM_DEP Metadata
|
||||
|
||||
1
tests/drivers/java/v5_8/.gitignore
vendored
Normal file
1
tests/drivers/java/v5_8/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
target/
|
||||
93
tests/drivers/java/v5_8/pom.xml
Normal file
93
tests/drivers/java/v5_8/pom.xml
Normal file
@@ -0,0 +1,93 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>memgraph</groupId>
|
||||
<artifactId>java-driver-tests</artifactId>
|
||||
<version>1.0</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.neo4j.driver</groupId>
|
||||
<artifactId>neo4j-java-driver</artifactId>
|
||||
<version>5.8.0</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>build-a</id>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>memgraph.DocsHowToQuery</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>DocsHowToQuery</finalName>
|
||||
</configuration>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>build-b</id>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>memgraph.MaxQueryLength</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>MaxQueryLength</finalName>
|
||||
</configuration>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>build-c</id>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>memgraph.Transactions</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
<appendAssemblyId>false</appendAssemblyId>
|
||||
<finalName>Transactions</finalName>
|
||||
</configuration>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
38
tests/drivers/java/v5_8/run.sh
Executable file
38
tests/drivers/java/v5_8/run.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$DIR"
|
||||
|
||||
if [ -d "/usr/lib/jvm/java-17-oracle" ]; then
|
||||
export JAVA_HOME="/usr/lib/jvm/java-17-oracle"
|
||||
fi
|
||||
if [ -d "/usr/lib/jvm/java-17-openjdk-amd64" ]; then
|
||||
export JAVA_HOME="/usr/lib/jvm/java-17-openjdk-amd64"
|
||||
fi
|
||||
if [ -d "/opt/apache-maven-3.9.2" ]; then
|
||||
export M2_HOME="/opt/apache-maven-3.9.2"
|
||||
fi
|
||||
export PATH="$JAVA_HOME/bin:$M2_HOME/bin:$PATH"
|
||||
|
||||
for i in java mvn; do
|
||||
if ! which $i >/dev/null; then
|
||||
echo "Please install $i!"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
JAVA_VER=$(java -version 2>&1 >/dev/null | grep 'version' | cut -d "\"" -f2 | cut -d "." -f1)
|
||||
if [ $JAVA_VER -ne 17 ]
|
||||
then
|
||||
echo "neo4j-java-driver v5.8 requires Java 17. Please install it!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# CentOS 7 doesn't have Java version that supports var keyword
|
||||
source ../../../../environment/util.sh
|
||||
|
||||
mvn clean package
|
||||
|
||||
java -jar target/DocsHowToQuery.jar
|
||||
java -jar target/MaxQueryLength.jar
|
||||
java -jar target/Transactions.jar
|
||||
@@ -0,0 +1,48 @@
|
||||
package memgraph;
|
||||
|
||||
import java.util.*;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.neo4j.driver.Query;
|
||||
import org.neo4j.driver.Config;
|
||||
|
||||
import static org.neo4j.driver.Values.parameters;
|
||||
|
||||
public class DocsHowToQuery {
|
||||
public static void main(String[] args) {
|
||||
var config = Config.builder().withoutEncryption().build();
|
||||
var driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("", ""), config);
|
||||
|
||||
try (var session = driver.session()) {
|
||||
session.run("MATCH (n) DETACH DELETE n;");
|
||||
System.out.println("Database cleared.");
|
||||
|
||||
session.run("CREATE (alice:Person {name: 'Alice', age: 22});");
|
||||
System.out.println("Record created.");
|
||||
|
||||
var node = session.run("MATCH (n) RETURN n;").list().get(0).get("n").asNode();
|
||||
System.out.println("Record matched.");
|
||||
|
||||
var label = node.labels().iterator().next();
|
||||
var name = node.get("name").asString();
|
||||
var age = node.get("age").asInt();
|
||||
|
||||
if (!label.equals("Person") || !name.equals("Alice") || age != 22) {
|
||||
System.out.println("Data doesn't match!");
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
System.out.println("Label: " + label);
|
||||
System.out.println("name: " + name);
|
||||
System.out.println("age: " + age);
|
||||
|
||||
System.out.println("All ok!");
|
||||
} catch (Exception e) {
|
||||
System.out.println(e);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Determines how long could be a query executed
|
||||
* from Java driver.
|
||||
*
|
||||
* Performs binary search until the maximum possible
|
||||
* query size has found.
|
||||
*/
|
||||
package memgraph;
|
||||
|
||||
import static org.neo4j.driver.Values.parameters;
|
||||
|
||||
import java.util.*;
|
||||
import org.neo4j.driver.*;
|
||||
import org.neo4j.driver.types.*;
|
||||
|
||||
public class MaxQueryLength {
|
||||
public static void main(String[] args) {
|
||||
// init driver
|
||||
Config config = Config.builder().withoutEncryption().build();
|
||||
Driver driver = GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("", ""), config);
|
||||
// init query
|
||||
int property_size = 0;
|
||||
int min_len = 1;
|
||||
int max_len = 100000;
|
||||
String query_template = "CREATE (n {name:\"%s\"})";
|
||||
int template_size = query_template.length() - 2; // because of %s
|
||||
|
||||
// binary search
|
||||
while (true) {
|
||||
property_size = (max_len + min_len) / 2;
|
||||
try (Session session = driver.session()) {
|
||||
String property_value = new String(new char[property_size]).replace('\0', 'a');
|
||||
String query = String.format(query_template, property_value);
|
||||
session.run(query).consume();
|
||||
if (min_len == max_len || property_size + 1 > max_len) {
|
||||
break;
|
||||
}
|
||||
min_len = property_size + 1;
|
||||
} catch (Exception e) {
|
||||
System.out.println(
|
||||
String.format("Query length: %d; Error: %s", property_size + template_size, e));
|
||||
max_len = property_size - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// final result
|
||||
System.out.println(String.format("\nThe max length of a query executed from "
|
||||
+ "Java driver is: %s\n",
|
||||
property_size + template_size));
|
||||
|
||||
// cleanup
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package memgraph;
|
||||
|
||||
import static org.neo4j.driver.Values.parameters;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import org.neo4j.driver.AuthTokens;
|
||||
import org.neo4j.driver.Config;
|
||||
import org.neo4j.driver.Driver;
|
||||
import org.neo4j.driver.GraphDatabase;
|
||||
import org.neo4j.driver.Result;
|
||||
import org.neo4j.driver.Session;
|
||||
import org.neo4j.driver.Transaction;
|
||||
import org.neo4j.driver.TransactionWork;
|
||||
import org.neo4j.driver.exceptions.ClientException;
|
||||
import org.neo4j.driver.exceptions.TransientException;
|
||||
|
||||
public class Transactions {
|
||||
public static String createPerson(Transaction tx, String name) {
|
||||
Result result =
|
||||
tx.run("CREATE (a:Person {name: $name}) RETURN a.name", parameters("name", name));
|
||||
return result.single().get(0).asString();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
Config config = Config.builder()
|
||||
.withoutEncryption()
|
||||
.withMaxTransactionRetryTime(0, TimeUnit.SECONDS)
|
||||
.build();
|
||||
Driver driver =
|
||||
GraphDatabase.driver("bolt://localhost:7687", AuthTokens.basic("neo4j", "1234"), config);
|
||||
|
||||
try (Session session = driver.session()) {
|
||||
try {
|
||||
session.writeTransaction(new TransactionWork<String>() {
|
||||
@Override
|
||||
public String execute(Transaction tx) {
|
||||
createPerson(tx, "mirko");
|
||||
Result result = tx.run("CREATE (");
|
||||
return result.single().get(0).asString();
|
||||
}
|
||||
});
|
||||
} catch (ClientException e) {
|
||||
System.out.println(e);
|
||||
}
|
||||
|
||||
session.writeTransaction(new TransactionWork<String>() {
|
||||
@Override
|
||||
public String execute(Transaction tx) {
|
||||
System.out.println(createPerson(tx, "mirko"));
|
||||
System.out.println(createPerson(tx, "slavko"));
|
||||
return "Done";
|
||||
}
|
||||
});
|
||||
|
||||
System.out.println("All ok!");
|
||||
|
||||
boolean timed_out = false;
|
||||
try {
|
||||
session.writeTransaction(new TransactionWork<String>() {
|
||||
@Override
|
||||
public String execute(Transaction tx) {
|
||||
Result result = tx.run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt");
|
||||
return result.single().get(0).asString();
|
||||
}
|
||||
});
|
||||
} catch (TransientException e) {
|
||||
timed_out = true;
|
||||
}
|
||||
|
||||
if (timed_out) {
|
||||
System.out.println("The query timed out as was expected.");
|
||||
} else {
|
||||
throw new Exception("The query should have timed out, but it didn't!");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.out.println(e);
|
||||
System.exit(1);
|
||||
}
|
||||
|
||||
driver.close();
|
||||
}
|
||||
}
|
||||
31
tests/drivers/node/v4_1/metadata.js
Normal file
31
tests/drivers/node/v4_1/metadata.js
Normal file
@@ -0,0 +1,31 @@
|
||||
var neo4j = require('neo4j-driver');
|
||||
var driver = neo4j.driver("bolt://localhost:7687",
|
||||
neo4j.auth.basic("", ""),
|
||||
{ encrypted: 'ENCRYPTION_OFF' });
|
||||
var session = driver.session();
|
||||
|
||||
function die() {
|
||||
session.close();
|
||||
driver.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function run_query(query, callback) {
|
||||
var run = session.run(query, {}, {metadata:{"ver":"session", "str":"aha", "num":123}});
|
||||
run.then(callback).catch(function (error) {
|
||||
console.log(error);
|
||||
die();
|
||||
});
|
||||
}
|
||||
|
||||
console.log("Checking implicit transaction metadata...");
|
||||
run_query("SHOW TRANSACTIONS;", function (result) {
|
||||
const md = result.records[0].get("metadata");
|
||||
if (md["ver"] != "session" || md["str"] != "aha" || md["num"] != 123){
|
||||
console.log("Error while reading metadata!");
|
||||
die();
|
||||
}
|
||||
console.log("All ok!");
|
||||
session.close();
|
||||
driver.close();
|
||||
});
|
||||
@@ -10,8 +10,9 @@ fi
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
# Driver generated with: `npm install neo4j-driver`
|
||||
npm install --no-package-lock --no-save neo4j-driver @babel/runtime
|
||||
npm install --no-package-lock --no-save neo4j-driver@4.1.1
|
||||
fi
|
||||
|
||||
node docs_how_to_query.js
|
||||
node max_query_length.js
|
||||
node metadata.js
|
||||
|
||||
42
tests/drivers/node/v5_8/docs_how_to_query.js
Normal file
42
tests/drivers/node/v5_8/docs_how_to_query.js
Normal file
@@ -0,0 +1,42 @@
|
||||
var neo4j = require('neo4j-driver');
|
||||
var driver = neo4j.driver("bolt://localhost:7687",
|
||||
neo4j.auth.basic("", ""),
|
||||
{ encrypted: 'ENCRYPTION_OFF' });
|
||||
var session = driver.session();
|
||||
|
||||
function die() {
|
||||
session.close();
|
||||
driver.close();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function run_query(query, callback) {
|
||||
var run = session.run(query, {});
|
||||
run.then(callback).catch(function (error) {
|
||||
console.log(error);
|
||||
die();
|
||||
});
|
||||
}
|
||||
|
||||
run_query("MATCH (n) DETACH DELETE n;", function (result) {
|
||||
console.log("Database cleared.");
|
||||
run_query("CREATE (alice:Person {name: 'Alice', age: 22});", function (result) {
|
||||
console.log("Record created.");
|
||||
run_query("MATCH (n) RETURN n", function (result) {
|
||||
console.log("Record matched.");
|
||||
const alice = result.records[0].get("n");
|
||||
const label = alice.labels[0];
|
||||
const name = alice.properties["name"];
|
||||
const age = alice.properties["age"];
|
||||
if(label != "Person" || name != "Alice" || age != 22){
|
||||
console.log("Data doesn't match!");
|
||||
die();
|
||||
}
|
||||
console.log("Label: " + label);
|
||||
console.log("name: " + name);
|
||||
console.log("age: " + age);
|
||||
console.log("All ok!");
|
||||
driver.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
51
tests/drivers/node/v5_8/max_query_length.js
Normal file
51
tests/drivers/node/v5_8/max_query_length.js
Normal file
@@ -0,0 +1,51 @@
|
||||
// Determines how long could be a query executed
|
||||
// from JavaScript driver.
|
||||
//
|
||||
// Performs binary search until the maximum possible
|
||||
// query size has found.
|
||||
|
||||
// init driver
|
||||
var neo4j = require('neo4j-driver');
|
||||
var driver = neo4j.driver("bolt://localhost:7687",
|
||||
neo4j.auth.basic("", ""),
|
||||
{ encrypted: 'ENCRYPTION_OFF' });
|
||||
|
||||
// init state
|
||||
var property_size = 0;
|
||||
var min_len = 1;
|
||||
var max_len = 1000000;
|
||||
|
||||
// hacking with JS and callbacks concept
|
||||
function serial_execution() {
|
||||
var next_size = [Math.floor((min_len + max_len) / 2)];
|
||||
setInterval(function() {
|
||||
if (next_size.length > 0) {
|
||||
property_size = next_size.pop();
|
||||
var query = "CREATE (n {name:\"" +
|
||||
(new Array(property_size)).join("a")+ "\"})";
|
||||
var session = driver.session();
|
||||
session.run(query, {}).then(function (result) {
|
||||
console.log("Success with the query length " + query.length);
|
||||
if (min_len == max_len || property_size + 1 > max_len) {
|
||||
console.log("\nThe max length of a query from JS driver is: " +
|
||||
query.length + "\n");
|
||||
session.close();
|
||||
driver.close();
|
||||
process.exit(0);
|
||||
}
|
||||
min_len = property_size + 1;
|
||||
next_size.push(Math.floor((min_len + max_len) / 2));
|
||||
}).catch(function (error) {
|
||||
console.log("Failure with the query length " + query.length);
|
||||
max_len = property_size - 1;
|
||||
next_size.push(Math.floor((min_len + max_len) / 2));
|
||||
}).then(function(){
|
||||
session.close();
|
||||
});
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// execution
|
||||
console.log("\nDetermine how long can be a query sent from JavaScript driver.");
|
||||
serial_execution(); // I don't like JavaScript
|
||||
17
tests/drivers/node/v5_8/run.sh
Executable file
17
tests/drivers/node/v5_8/run.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$DIR"
|
||||
|
||||
if ! which node >/dev/null; then
|
||||
echo "Please install nodejs!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d node_modules ]; then
|
||||
# Driver generated with: `npm install neo4j-driver`
|
||||
npm install --no-package-lock --no-save neo4j-driver@5.8.0
|
||||
fi
|
||||
|
||||
node docs_how_to_query.js
|
||||
node max_query_length.js
|
||||
65
tests/drivers/python/v4_1/metadata.py
Executable file
65
tests/drivers/python/v4_1/metadata.py
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2021 Memgraph Ltd.
|
||||
#
|
||||
# Use of this software is governed by the Business Source License
|
||||
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
# License, and you may not use this file except in compliance with the Business Source License.
|
||||
#
|
||||
# As of the Change Date specified in that file, in accordance with
|
||||
# the Business Source License, use of this software will be governed
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# licenses/APL.txt.
|
||||
|
||||
import threading
|
||||
|
||||
import neo4j
|
||||
|
||||
|
||||
def check_md(tx_md):
|
||||
n = 0
|
||||
for record in tx_md:
|
||||
md = record[3]
|
||||
if md["ver"] == "session" and md["str"] == "aha" and md["num"] == 123:
|
||||
n = n + 1
|
||||
elif md["ver"] == "transaction" and md["str"] == "oho" and md["num"] == 456:
|
||||
n = n + 1
|
||||
return n
|
||||
|
||||
|
||||
def session_run(driver):
|
||||
print("Checking implicit transaction metadata...")
|
||||
with driver.session() as session:
|
||||
query = neo4j.Query("SHOW TRANSACTIONS", timeout=2, metadata={"ver": "session", "str": "aha", "num": 123})
|
||||
result = session.run(query).values()
|
||||
assert check_md(result) == 1, "metadata info error!"
|
||||
|
||||
|
||||
def show_tx(driver, tx_md):
|
||||
with driver.session() as session:
|
||||
query = neo4j.Query("SHOW TRANSACTIONS", timeout=2, metadata={"ver": "session", "str": "aha", "num": 123})
|
||||
for t in session.run(query).values():
|
||||
tx_md.append(t)
|
||||
|
||||
|
||||
def transaction_run(driver):
|
||||
print("Checking explicit transaction metadata...")
|
||||
with driver.session() as session:
|
||||
tx = session.begin_transaction(timeout=2, metadata={"ver": "transaction", "str": "oho", "num": 456})
|
||||
tx.run("MATCH (n) RETURN n LIMIT 1").consume()
|
||||
tx_md = []
|
||||
th = threading.Thread(target=show_tx, args=(driver, tx_md))
|
||||
th.start()
|
||||
if th.is_alive():
|
||||
th.join()
|
||||
tx.commit()
|
||||
assert check_md(tx_md) == 2, "metadata info error!"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
driver = neo4j.GraphDatabase.driver("bolt://localhost:7687", auth=("user", "pass"), encrypted=False)
|
||||
session_run(driver)
|
||||
transaction_run(driver)
|
||||
driver.close()
|
||||
print("All ok!")
|
||||
@@ -31,3 +31,4 @@ source ve3/bin/activate
|
||||
python3 docs_how_to_query.py || exit 1
|
||||
python3 max_query_length.py || exit 1
|
||||
python3 transactions.py || exit 1
|
||||
python3 metadata.py || exit 1
|
||||
|
||||
46
tests/drivers/python/v5_8/docs_how_to_query.py
Normal file
46
tests/drivers/python/v5_8/docs_how_to_query.py
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2021 Memgraph Ltd.
|
||||
#
|
||||
# Use of this software is governed by the Business Source License
|
||||
# included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
# License, and you may not use this file except in compliance with the Business Source License.
|
||||
#
|
||||
# As of the Change Date specified in that file, in accordance with
|
||||
# the Business Source License, use of this software will be governed
|
||||
# by the Apache License, Version 2.0, included in the file
|
||||
# licenses/APL.txt.
|
||||
|
||||
import sys
|
||||
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=("", ""), encrypted=False)
|
||||
session = driver.session()
|
||||
|
||||
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||
print("Database cleared.")
|
||||
|
||||
session.run('CREATE (alice:Person {name: "Alice", age: 22})').consume()
|
||||
print("Record created.")
|
||||
|
||||
node = session.run("MATCH (n) RETURN n").single()["n"]
|
||||
print("Record matched.")
|
||||
|
||||
label = list(node.labels)[0]
|
||||
name = node["name"]
|
||||
age = node["age"]
|
||||
|
||||
if label != "Person" or name != "Alice" or age != 22:
|
||||
print("Data does not match")
|
||||
sys.exit(1)
|
||||
|
||||
print("Label: %s" % label)
|
||||
print("name: %s" % name)
|
||||
print("age: %s" % age)
|
||||
|
||||
session.close()
|
||||
driver.close()
|
||||
|
||||
print("All ok!")
|
||||
48
tests/drivers/python/v5_8/max_query_length.py
Normal file
48
tests/drivers/python/v5_8/max_query_length.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2021 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.
|
||||
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=None, encrypted=False)
|
||||
|
||||
query_template = 'CREATE (n {name:"%s"})'
|
||||
template_size = len(query_template) - 2 # because of %s
|
||||
min_len = 1
|
||||
max_len = 1000000
|
||||
|
||||
# binary search because we have to find the maximum size (in number of chars)
|
||||
# of a query that can be executed via driver
|
||||
while True:
|
||||
assert min_len > 0 and max_len > 0, (
|
||||
"The lengths have to be positive values! If this happens something"
|
||||
" is terrible wrong with min & max lengths OR the database"
|
||||
" isn't available."
|
||||
)
|
||||
property_size = (max_len + min_len) // 2
|
||||
try:
|
||||
driver.session().run(query_template % ("a" * property_size)).consume()
|
||||
if min_len == max_len or property_size + 1 > max_len:
|
||||
break
|
||||
min_len = property_size + 1
|
||||
except Exception as e:
|
||||
print("Query size %s is too big!" % (template_size + property_size))
|
||||
max_len = property_size - 1
|
||||
|
||||
assert property_size == max_len, "max_len probably has to be increased!"
|
||||
|
||||
print("\nThe max length of a query from Python driver is: %s\n" % (template_size + property_size))
|
||||
|
||||
# sessions are not closed bacause all sessions that are
|
||||
# executed with wrong query size might be broken
|
||||
driver.close()
|
||||
26
tests/drivers/python/v5_8/run.sh
Executable file
26
tests/drivers/python/v5_8/run.sh
Executable file
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
cd "$DIR"
|
||||
|
||||
# system check
|
||||
if ! which virtualenv >/dev/null; then
|
||||
echo "Please install virtualenv!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# setup virtual environment
|
||||
if [ ! -d "ve3" ]; then
|
||||
virtualenv -p python3 ve3 || exit 1
|
||||
source ve3/bin/activate
|
||||
python3 -m pip install neo4j==5.8.0 || exit 1
|
||||
deactivate
|
||||
fi
|
||||
|
||||
# activate virtualenv
|
||||
source ve3/bin/activate
|
||||
|
||||
# execute test
|
||||
python3 docs_how_to_query.py || exit 1
|
||||
python3 max_query_length.py || exit 1
|
||||
python3 transactions.py || exit 1
|
||||
69
tests/drivers/python/v5_8/transactions.py
Normal file
69
tests/drivers/python/v5_8/transactions.py
Normal file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright 2021 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.
|
||||
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
from neo4j.exceptions import ClientError, TransientError
|
||||
|
||||
|
||||
def tx_error(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
print(a[0])
|
||||
tx.run("CREATE (").consume()
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_good(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
print(a[0])
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_too_long(tx):
|
||||
tx.run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt")
|
||||
|
||||
|
||||
with GraphDatabase.driver("bolt://localhost:7687", auth=None, encrypted=False) as driver:
|
||||
|
||||
def add_person(f, name, name2):
|
||||
with driver.session() as session:
|
||||
session.write_transaction(f, name, name2)
|
||||
|
||||
# Wrong query.
|
||||
try:
|
||||
add_person(tx_error, "mirko", "slavko")
|
||||
except ClientError:
|
||||
pass
|
||||
# Correct query.
|
||||
add_person(tx_good, "mirka", "slavka")
|
||||
|
||||
# Setup for next query.
|
||||
with driver.session() as session:
|
||||
session.run("UNWIND range(1, 100000) AS x CREATE ()").consume()
|
||||
|
||||
# Query that will run for a very long time, transient error expected.
|
||||
timed_out = False
|
||||
try:
|
||||
with driver.session() as session:
|
||||
session.run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt").consume()
|
||||
except TransientError:
|
||||
timed_out = True
|
||||
|
||||
if timed_out:
|
||||
print("The query timed out as was expected.")
|
||||
else:
|
||||
raise Exception("The query should have timed out, but it didn't!")
|
||||
|
||||
print("All ok!")
|
||||
@@ -219,3 +219,47 @@ Feature: List operators
|
||||
| [[1], 2] |
|
||||
| [3] |
|
||||
| 4 |
|
||||
|
||||
Scenario: Unwind + InList test1
|
||||
When executing query:
|
||||
"""
|
||||
UNWIND [[1,2], [3,4]] as l
|
||||
RETURN 2 in l as x
|
||||
"""
|
||||
Then the result should be:
|
||||
| x |
|
||||
| true |
|
||||
| false |
|
||||
|
||||
Scenario: Unwind + InList test2
|
||||
When executing query:
|
||||
"""
|
||||
WITH [[1,2], [3,4]] as list
|
||||
UNWIND list as l
|
||||
RETURN 2 in l as x
|
||||
"""
|
||||
Then the result should be:
|
||||
| x |
|
||||
| true |
|
||||
| false |
|
||||
|
||||
Scenario: Unwind + InList test3
|
||||
Given an empty graph
|
||||
And having executed
|
||||
"""
|
||||
CREATE ({id: 1}), ({id: 2}), ({id: 3}), ({id: 4})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
WITH [1, 2, 3] as list
|
||||
MATCH (n) WHERE n.id in list
|
||||
WITH n
|
||||
WITH n, [1, 2] as list
|
||||
WHERE n.id in list
|
||||
RETURN n.id as id
|
||||
ORDER BY id;
|
||||
"""
|
||||
Then the result should be:
|
||||
| id |
|
||||
| 1 |
|
||||
| 2 |
|
||||
|
||||
1
tests/integration/audit/.gitignore
vendored
Normal file
1
tests/integration/audit/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
*.log
|
||||
47
tests/integration/run.sh
Executable file
47
tests/integration/run.sh
Executable file
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash -e
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
|
||||
|
||||
print_help() {
|
||||
echo -e "$0 => run all under tests/integration"
|
||||
echo -e "$0 folder_name => run single test under tests/integration"
|
||||
exit 1
|
||||
}
|
||||
|
||||
test_one() {
|
||||
cd "$DIR"
|
||||
integration_test_folder_name="$1"
|
||||
pushd "$integration_test_folder_name" >/dev/null
|
||||
echo "Running: $integration_test_folder_name"
|
||||
if [ -x prepare.sh ]; then
|
||||
./prepare.sh
|
||||
fi
|
||||
if [ -x runner.py ]; then
|
||||
./runner.py
|
||||
elif [ -x runner.sh ]; then
|
||||
./runner.sh
|
||||
fi
|
||||
echo
|
||||
popd >/dev/null
|
||||
}
|
||||
|
||||
test_all() {
|
||||
cd "$DIR"
|
||||
for name in *; do
|
||||
if [ ! -d "$name" ]; then continue; fi
|
||||
test_one "$name"
|
||||
done
|
||||
}
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
test_all
|
||||
else
|
||||
if [ "$#" -gt 1 ]; then
|
||||
print_help
|
||||
else
|
||||
if [ -d "$DIR/$1" ]; then
|
||||
test_one "$1"
|
||||
else
|
||||
print_help
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
// Copyright 2023 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
@@ -12,6 +12,7 @@
|
||||
#include <string>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "bolt_common.hpp"
|
||||
#include "communication/bolt/v1/session.hpp"
|
||||
@@ -27,6 +28,7 @@ using memgraph::communication::bolt::Value;
|
||||
static const char *kInvalidQuery = "invalid query";
|
||||
static const char *kQueryReturn42 = "RETURN 42";
|
||||
static const char *kQueryReturnMultiple = "UNWIND [1,2,3] as n RETURN n";
|
||||
static const char *kQueryShowTx = "SHOW TRANSACTIONS";
|
||||
static const char *kQueryEmpty = "no results";
|
||||
|
||||
class TestSessionData {};
|
||||
@@ -39,10 +41,18 @@ class TestSession : public Session<TestInputStream, TestOutputStream> {
|
||||
: Session<TestInputStream, TestOutputStream>(input_stream, output_stream) {}
|
||||
|
||||
std::pair<std::vector<std::string>, std::optional<int>> Interpret(
|
||||
const std::string &query, const std::map<std::string, Value> ¶ms) override {
|
||||
const std::string &query, const std::map<std::string, Value> ¶ms,
|
||||
const std::map<std::string, Value> &metadata) override {
|
||||
if (!metadata.empty()) md_ = metadata;
|
||||
if (query == kQueryReturn42 || query == kQueryEmpty || query == kQueryReturnMultiple) {
|
||||
query_ = query;
|
||||
return {{"result_name"}, {}};
|
||||
} else if (query == kQueryShowTx) {
|
||||
if (md_.at("str").ValueString() != "aha" || md_.at("num").ValueInt() != 123) {
|
||||
throw ClientError("Wrong metadata!");
|
||||
}
|
||||
query_ = query;
|
||||
return {{"username", "transaction_id", "query", "metadata"}, {}};
|
||||
} else {
|
||||
query_ = "";
|
||||
throw ClientError("client sent invalid query");
|
||||
@@ -71,6 +81,9 @@ class TestSession : public Session<TestInputStream, TestOutputStream> {
|
||||
}
|
||||
|
||||
return {std::pair("has_more", true)};
|
||||
} else if (query_ == kQueryShowTx) {
|
||||
encoder->MessageRecord({"", 1234567890, query_, md_});
|
||||
return {};
|
||||
} else {
|
||||
throw ClientError("client sent invalid query");
|
||||
}
|
||||
@@ -78,11 +91,11 @@ class TestSession : public Session<TestInputStream, TestOutputStream> {
|
||||
|
||||
std::map<std::string, Value> Discard(std::optional<int>, std::optional<int>) override { return {}; }
|
||||
|
||||
void BeginTransaction() override {}
|
||||
void CommitTransaction() override {}
|
||||
void RollbackTransaction() override {}
|
||||
void BeginTransaction(const std::map<std::string, Value> &metadata) override { md_ = metadata; }
|
||||
void CommitTransaction() override { md_.clear(); }
|
||||
void RollbackTransaction() override { md_.clear(); }
|
||||
|
||||
void Abort() override {}
|
||||
void Abort() override { md_.clear(); }
|
||||
|
||||
bool Authenticate(const std::string &username, const std::string &password) override { return true; }
|
||||
|
||||
@@ -90,6 +103,7 @@ class TestSession : public Session<TestInputStream, TestOutputStream> {
|
||||
|
||||
private:
|
||||
std::string query_;
|
||||
std::map<std::string, Value> md_;
|
||||
};
|
||||
|
||||
// TODO: This could be done in fixture.
|
||||
@@ -157,6 +171,10 @@ inline constexpr uint8_t handshake_req[] = {0x60, 0x60, 0xb0, 0x17, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
inline constexpr uint8_t handshake_resp[] = {0x00, 0x00, 0x03, 0x04};
|
||||
inline constexpr uint8_t route[]{0xb3, 0x66, 0xa0, 0x90, 0xc0};
|
||||
const std::string extra_w_metadata =
|
||||
"\xa2\x8b\x74\x78\x5f\x6d\x65\x74\x61\x64\x61\x74\x61\xa2\x83\x73\x74\x72\x83\x61\x68\x61\x83\x6e\x75\x6d\x7b\x8a"
|
||||
"\x74\x78\x5f\x74\x69\x6d\x65\x6f\x75\x74\xc9\x07\xd0";
|
||||
inline constexpr uint8_t commit[] = {0xb0, 0x12};
|
||||
} // namespace v4_3
|
||||
|
||||
// Write bolt chunk header (length)
|
||||
@@ -229,10 +247,11 @@ void ExecuteInit(TestInputStream &input_stream, TestSession &session, std::vecto
|
||||
}
|
||||
|
||||
// Write bolt encoded run request
|
||||
void WriteRunRequest(TestInputStream &input_stream, const char *str, const bool is_v4 = false) {
|
||||
void WriteRunRequest(TestInputStream &input_stream, const char *str, const bool is_v4 = false,
|
||||
const std::string &extra = "\xA0") {
|
||||
// write chunk header
|
||||
auto len = strlen(str);
|
||||
WriteChunkHeader(input_stream, (3 + is_v4) + 2 + len + 1);
|
||||
WriteChunkHeader(input_stream, (3 + is_v4 * extra.size()) + 2 + len + 1);
|
||||
|
||||
const auto *run_header = is_v4 ? v4::run_req_header : run_req_header;
|
||||
const auto run_header_size = is_v4 ? sizeof(v4::run_req_header) : sizeof(run_req_header);
|
||||
@@ -250,7 +269,7 @@ void WriteRunRequest(TestInputStream &input_stream, const char *str, const bool
|
||||
|
||||
if (is_v4) {
|
||||
// write empty map for extra field
|
||||
input_stream.Write("\xA0", 1); // TinyMap
|
||||
input_stream.Write(extra.data(), extra.size()); // TinyMap
|
||||
}
|
||||
|
||||
// write chunk tail
|
||||
@@ -353,15 +372,15 @@ TEST(BoltSession, HandshakeWithVersionOffset) {
|
||||
ASSERT_EQ(session.version_.minor, 3);
|
||||
ASSERT_EQ(session.version_.major, 4);
|
||||
}
|
||||
// With multiple offsets
|
||||
// With multiple offsets (added v5.2)
|
||||
{
|
||||
INIT_VARS;
|
||||
const uint8_t priority_request[] = {0x60, 0x60, 0xb0, 0x17, 0x00, 0x03, 0x03, 0x07, 0x00, 0x03,
|
||||
0x03, 0x06, 0x00, 0x03, 0x03, 0x05, 0x00, 0x03, 0x03, 0x04};
|
||||
const uint8_t priority_response[] = {0x00, 0x00, 0x03, 0x04};
|
||||
const uint8_t priority_response[] = {0x00, 0x00, 0x02, 0x05};
|
||||
ExecuteHandshake(input_stream, session, output, priority_request, priority_response);
|
||||
ASSERT_EQ(session.version_.minor, 3);
|
||||
ASSERT_EQ(session.version_.major, 4);
|
||||
ASSERT_EQ(session.version_.minor, 2);
|
||||
ASSERT_EQ(session.version_.major, 5);
|
||||
}
|
||||
// Offset overflows
|
||||
{
|
||||
@@ -1122,3 +1141,27 @@ TEST(BoltSession, ResetInIdle) {
|
||||
EXPECT_EQ(session.state_, State::Idle);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(BoltSession, PassMetadata) {
|
||||
// v4+
|
||||
{
|
||||
INIT_VARS;
|
||||
|
||||
ExecuteHandshake(input_stream, session, output, v4_3::handshake_req, v4_3::handshake_resp);
|
||||
ExecuteInit(input_stream, session, output, true);
|
||||
|
||||
WriteRunRequest(input_stream, kQueryShowTx, true, v4_3::extra_w_metadata);
|
||||
session.Execute();
|
||||
ASSERT_EQ(session.state_, State::Result);
|
||||
|
||||
ExecuteCommand(input_stream, session, v4::pullall_req, sizeof(v4::pullall_req));
|
||||
ASSERT_EQ(session.state_, State::Idle);
|
||||
PrintOutput(output);
|
||||
constexpr std::array<uint8_t, 5> md_num_123{0x83, 0x6E, 0x75, 0x6D, 0x7B};
|
||||
constexpr std::array<uint8_t, 8> md_str_aha{0x83, 0x73, 0x74, 0x72, 0x83, 0x61, 0x68, 0x61};
|
||||
auto find_num = std::search(begin(output), end(output), begin(md_num_123), end(md_num_123));
|
||||
EXPECT_NE(find_num, end(output));
|
||||
auto find_str = std::search(begin(output), end(output), begin(md_str_aha), end(md_str_aha));
|
||||
EXPECT_NE(find_str, end(output));
|
||||
}
|
||||
}
|
||||
|
||||
114
tools/debug/memgraph_syntetic_load.sh
Executable file
114
tools/debug/memgraph_syntetic_load.sh
Executable file
@@ -0,0 +1,114 @@
|
||||
!/bin/bash -e
|
||||
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )"
|
||||
|
||||
cd $DIR/../../build
|
||||
|
||||
|
||||
query_list=(
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CALL dummy_query.dummy_read() YIELD *;"
|
||||
"CALL dummy_query.dummy_write() YIELD *;"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
"CREATE (:Node)-[:CONNECTED]->(:Node);"
|
||||
|
||||
)
|
||||
|
||||
# Start the first memgraph instance
|
||||
echo "Starting memgraph..."
|
||||
./memgraph --log-level=TRACE --storage-recover-on-startup=true --storage-snapshot-interval-sec=600 --log-file=$DIR/memgraph.log --bolt-port 7687 --query-modules-directory=$DIR/query_modules &
|
||||
|
||||
sleep 5
|
||||
# Capture the process ID (PID)
|
||||
memgraph=$!
|
||||
echo $memgraph
|
||||
|
||||
docker run -d -p 7688:7687 --name memgraph_docker memgraph/memgraph:2.8.0 --log-level=TRACE --storage-recover-on-startup=true --storage-snapshot-interval-sec=600 &
|
||||
sleep 5
|
||||
|
||||
cd $DIR/query_modules
|
||||
tar -cf - dummy_query.py | docker cp -a - memgraph_docker:/usr/lib/memgraph/query_modules/
|
||||
docker exec -it -u 0 memgraph_docker bash -c "chown -R root:root /usr/lib/memgraph/query_modules"
|
||||
docker_ip=$(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' memgraph_docker)
|
||||
echo $docker_ip
|
||||
|
||||
#check docker query module ownership
|
||||
docker exec -it memgraph_docker ls -l /usr/lib/memgraph/query_modules
|
||||
|
||||
|
||||
# Function to execute query commands
|
||||
execute_query_native() {
|
||||
local query=$1
|
||||
|
||||
echo "$query" | mgconsole -host "127.0.0.1" -port 7687
|
||||
}
|
||||
|
||||
execute_query_docker(){
|
||||
local query=$1
|
||||
echo "$query" | mgconsole -host "127.0.0.1" -port 7688
|
||||
}
|
||||
|
||||
execute_query_docker "CALL mg.load_all();"
|
||||
execute_query_docker "CALL dummy_query.dummy_read() YIELD *;"
|
||||
|
||||
function cleanup {
|
||||
echo "Terminating child processes..."
|
||||
echo "Stopping memgraph..."
|
||||
kill -9 $memgraph
|
||||
sleep 5
|
||||
echo "Stopping memgraph_docker..."
|
||||
docker stop memgraph_docker
|
||||
echo "Removing memgraph_docker..."
|
||||
docker rm memgraph_docker
|
||||
echo "Script execution completed."
|
||||
exit
|
||||
}
|
||||
|
||||
|
||||
trap cleanup SIGINT
|
||||
|
||||
|
||||
|
||||
|
||||
previous_time=$(date +%s)
|
||||
previous_minute=$(date +%M)
|
||||
previous_hour=$(date +%H)
|
||||
|
||||
while true; do
|
||||
current_time=$(date +%s)
|
||||
current_minute=$(date +%M)
|
||||
current_hour=$(date +%H)
|
||||
|
||||
if ((current_time - previous_time >= 5)); then
|
||||
echo "Five seconds have passe running create query."
|
||||
for query in "${query_list[@]}"; do
|
||||
execute_query_native "$query" &
|
||||
execute_query_docker "$query" &
|
||||
done
|
||||
previous_time=$current_time
|
||||
fi
|
||||
|
||||
if [[ $current_minute != $previous_minute ]]; then
|
||||
echo "One minute has passed running match query."
|
||||
|
||||
execute_query_native "MATCH (n)-[r]->(m) RETURN DISTINCT COUNT(n);" &
|
||||
execute_query_docker "MATCH (n)-[r]->(m) RETURN DISTINCT COUNT(n);" &
|
||||
previous_minute=$current_minute
|
||||
fi
|
||||
|
||||
if [[ $current_hour != $previous_hour ]]; then
|
||||
echo "One hour has passed running delete query."
|
||||
execute_query_native "MATCH (n) DETACH DELETE n;" &
|
||||
execute_query_docker "MATCH (n) DETACH DELETE n;" &
|
||||
previous_hour=$current_hour
|
||||
fi
|
||||
|
||||
done
|
||||
27
tools/debug/query_modules/dummy_query.py
Normal file
27
tools/debug/query_modules/dummy_query.py
Normal file
@@ -0,0 +1,27 @@
|
||||
import random
|
||||
|
||||
import mgp
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def dummy_read(
|
||||
context: mgp.ProcCtx,
|
||||
) -> mgp.Record(total_vertices=int, total_edges=int):
|
||||
vertices = context.graph.vertices
|
||||
total_edges = 0
|
||||
for vertex in vertices:
|
||||
for edge in vertex.out_edges:
|
||||
total_edges += 1
|
||||
return mgp.Record(total_vertices=len(vertices), total_edges=total_edges)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def dummy_write(
|
||||
context: mgp.ProcCtx,
|
||||
) -> mgp.Record(vertex=mgp.Vertex):
|
||||
for i in range(0, 5):
|
||||
node = context.graph.create_vertex()
|
||||
node.add_label("Procedure_node")
|
||||
node.properties.set("id", random.randint(0, 100000))
|
||||
|
||||
return mgp.Record(vertex=node)
|
||||
Reference in New Issue
Block a user