Compare commits
38 Commits
T0785-MG-b
...
MG-unorder
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9104a8c9d3 | ||
|
|
5c4cbdbaf5 | ||
|
|
803f8c3aad | ||
|
|
95a2078013 | ||
|
|
f7f3f0a955 | ||
|
|
ab8ebdd02b | ||
|
|
521a085b79 | ||
|
|
5ab82c5cfe | ||
|
|
07566ea9d5 | ||
|
|
720762caa0 | ||
|
|
b5404a4fbf | ||
|
|
6f1e1d6120 | ||
|
|
9e0eec2176 | ||
|
|
113184a3a4 | ||
|
|
cd9ade06cb | ||
|
|
989f43b0b3 | ||
|
|
d95bfb3170 | ||
|
|
e01a11ca32 | ||
|
|
2f1c55e2b1 | ||
|
|
cfb9203b43 | ||
|
|
8b1d128c9d | ||
|
|
a30310ce80 | ||
|
|
f1b2ee7fc4 | ||
|
|
d72b23b682 | ||
|
|
74926ab058 | ||
|
|
8abc5ef025 | ||
|
|
a8b4c664a4 | ||
|
|
1eaea91312 | ||
|
|
3b13582868 | ||
|
|
99d17758fe | ||
|
|
05731ffb53 | ||
|
|
f4d8845718 | ||
|
|
4c5bf7c376 | ||
|
|
a8d9d5e754 | ||
|
|
ae7b5976af | ||
|
|
6c45cd8558 | ||
|
|
78ebaa0499 | ||
|
|
7eb0b4579d |
10
.github/workflows/package_all.yaml
vendored
10
.github/workflows/package_all.yaml
vendored
@@ -101,7 +101,7 @@ jobs:
|
||||
- name: "Build package"
|
||||
run: |
|
||||
cd release/package
|
||||
./run.sh package debian-11 --for-docker
|
||||
./run.sh package debian-10 --for-docker
|
||||
./run.sh docker
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
name: ubuntu-2004
|
||||
path: build/output/ubuntu-20.04/memgraph*.deb
|
||||
|
||||
debian-11-platform:
|
||||
debian-10-platform:
|
||||
runs-on: [self-hosted, DockerMgBuild]
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
@@ -153,9 +153,9 @@ jobs:
|
||||
fetch-depth: 0 # Required because of release/get_version.py
|
||||
- name: "Build package"
|
||||
run: |
|
||||
./release/package/run.sh package debian-11 --for-platform
|
||||
./release/package/run.sh package debian-10 --for-platform
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: debian-11-platform
|
||||
path: build/output/debian-11/memgraph*.deb
|
||||
name: debian-10-platform
|
||||
path: build/output/debian-10/memgraph*.deb
|
||||
|
||||
@@ -1405,76 +1405,38 @@ int mgp_must_abort(struct mgp_graph *graph);
|
||||
|
||||
/// @}
|
||||
|
||||
/// @name Stream Source message API
|
||||
/// API for accessing specific data contained in a mgp_message
|
||||
/// used for defining transformation procedures.
|
||||
/// Not all methods are available for all stream sources
|
||||
/// so make sure that your transformation procedure can be used
|
||||
/// for a specific source, i.e. only valid methods are used.
|
||||
/// @name Kafka message API
|
||||
/// Currently the API below is for kafka only but in the future
|
||||
/// mgp_message and mgp_messages might be generic to support
|
||||
/// other streaming systems.
|
||||
///@{
|
||||
|
||||
/// A single Stream source message
|
||||
/// A single Kafka message
|
||||
struct mgp_message;
|
||||
|
||||
/// A list of Stream source messages
|
||||
/// A list of Kafka messages
|
||||
struct mgp_messages;
|
||||
|
||||
/// Stream source type.
|
||||
enum mgp_source_type {
|
||||
KAFKA,
|
||||
PULSAR,
|
||||
};
|
||||
|
||||
/// Get the type of the stream source that produced the message.
|
||||
enum mgp_error mgp_message_source_type(struct mgp_message *message, enum mgp_source_type *result);
|
||||
|
||||
/// Payload is not null terminated and not a string but rather a byte array.
|
||||
/// You need to call mgp_message_payload_size() first, to read the size of
|
||||
/// the payload.
|
||||
/// Supported stream sources:
|
||||
/// - Kafka
|
||||
/// - Pulsar
|
||||
/// Return MGP_ERROR_INVALID_ARGUMENT if the message is from an unsupported stream source.
|
||||
enum mgp_error mgp_message_payload(struct mgp_message *message, const char **result);
|
||||
|
||||
/// Get the payload size
|
||||
/// Supported stream sources:
|
||||
/// - Kafka
|
||||
/// - Pulsar
|
||||
/// Return MGP_ERROR_INVALID_ARGUMENT if the message is from an unsupported stream source.
|
||||
enum mgp_error mgp_message_payload_size(struct mgp_message *message, size_t *result);
|
||||
|
||||
/// Get the name of topic
|
||||
/// Supported stream sources:
|
||||
/// - Kafka
|
||||
/// - Pulsar
|
||||
/// Return MGP_ERROR_INVALID_ARGUMENT if the message is from an unsupported stream source.
|
||||
enum mgp_error mgp_message_topic_name(struct mgp_message *message, const char **result);
|
||||
|
||||
/// Get the key of mgp_message as a byte array
|
||||
/// Supported stream sources:
|
||||
/// - Kafka
|
||||
/// Return MGP_ERROR_INVALID_ARGUMENT if the message is from an unsupported stream source.
|
||||
enum mgp_error mgp_message_key(struct mgp_message *message, const char **result);
|
||||
|
||||
/// Get the key size of mgp_message
|
||||
/// Supported stream sources:
|
||||
/// - Kafka
|
||||
/// Return MGP_ERROR_INVALID_ARGUMENT if the message is from an unsupported stream source.
|
||||
enum mgp_error mgp_message_key_size(struct mgp_message *message, size_t *result);
|
||||
|
||||
/// Get the timestamp of mgp_message as a byte array
|
||||
/// Supported stream sources:
|
||||
/// - Kafka
|
||||
/// Return MGP_ERROR_INVALID_ARGUMENT if the message is from an unsupported stream source.
|
||||
enum mgp_error mgp_message_timestamp(struct mgp_message *message, int64_t *result);
|
||||
|
||||
/// Get the message offset from a message.
|
||||
/// Supported stream sources:
|
||||
/// - Kafka
|
||||
/// Return MGP_ERROR_INVALID_ARGUMENT if the message is from an unsupported stream source.
|
||||
enum mgp_error mgp_message_offset(struct mgp_message *message, int64_t *result);
|
||||
|
||||
/// Get the number of messages contained in the mgp_messages list
|
||||
/// Current implementation always returns without errors.
|
||||
enum mgp_error mgp_messages_size(struct mgp_messages *message, size_t *result);
|
||||
|
||||
@@ -1260,9 +1260,6 @@ class InvalidMessageError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
SOURCE_TYPE_KAFKA = _mgp.SOURCE_TYPE_KAFKA
|
||||
SOURCE_TYPE_PULSAR = _mgp.SOURCE_TYPE_PULSAR
|
||||
|
||||
class Message:
|
||||
"""Represents a message from a stream."""
|
||||
__slots__ = ('_message',)
|
||||
@@ -1283,73 +1280,26 @@ class Message:
|
||||
"""Return True if `self` is in valid context and may be used."""
|
||||
return self._message.is_valid()
|
||||
|
||||
def source_type(self) -> str:
|
||||
"""
|
||||
Supported in all stream sources
|
||||
|
||||
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
raise InvalidMessageError()
|
||||
return self._message.source_type()
|
||||
|
||||
def payload(self) -> bytes:
|
||||
"""
|
||||
Supported stream sources:
|
||||
- Kafka
|
||||
- Pulsar
|
||||
|
||||
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
raise InvalidMessageError()
|
||||
return self._message.payload()
|
||||
|
||||
def topic_name(self) -> str:
|
||||
"""
|
||||
Supported stream sources:
|
||||
- Kafka
|
||||
- Pulsar
|
||||
|
||||
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
raise InvalidMessageError()
|
||||
return self._message.topic_name()
|
||||
|
||||
def key(self) -> bytes:
|
||||
"""
|
||||
Supported stream sources:
|
||||
- Kafka
|
||||
|
||||
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
raise InvalidMessageError()
|
||||
return self._message.key()
|
||||
|
||||
def timestamp(self) -> int:
|
||||
"""
|
||||
Supported stream sources:
|
||||
- Kafka
|
||||
|
||||
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
raise InvalidMessageError()
|
||||
return self._message.timestamp()
|
||||
|
||||
def offset(self) -> int:
|
||||
"""
|
||||
Supported stream sources:
|
||||
- Kafka
|
||||
|
||||
Raise InvalidArgumentError if the message is from an unsupported stream source.
|
||||
"""
|
||||
if not self.is_valid():
|
||||
raise InvalidMessageError()
|
||||
return self._message.offset()
|
||||
|
||||
|
||||
class InvalidMessagesError(Exception):
|
||||
"""Signals using a messages instance outside of the registered transformation."""
|
||||
|
||||
@@ -107,13 +107,12 @@ import_external_library(benchmark STATIC
|
||||
# Skip testing. The tests don't compile with Clang 8.
|
||||
CMAKE_ARGS -DBENCHMARK_ENABLE_TESTING=OFF)
|
||||
|
||||
include(FetchContent)
|
||||
|
||||
# setup fmt format
|
||||
FetchContent_Declare(fmt
|
||||
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/fmt)
|
||||
|
||||
FetchContent_MakeAvailable(fmt)
|
||||
import_external_library(fmt STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/fmt/${CMAKE_INSTALL_LIBDIR}/libfmt.a
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/fmt/include
|
||||
# Skip testing.
|
||||
CMAKE_ARGS -DFMT_TEST=OFF)
|
||||
|
||||
# setup rapidcheck (it cannot be external, since it doesn't have install
|
||||
# target)
|
||||
@@ -226,11 +225,10 @@ add_external_project(mgconsole
|
||||
add_custom_target(mgconsole DEPENDS mgconsole-proj)
|
||||
|
||||
# Setup spdlog
|
||||
set(SPDLOG_FMT_EXTERNAL ON)
|
||||
FetchContent_Declare(spdlog
|
||||
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/spdlog)
|
||||
|
||||
FetchContent_MakeAvailable(spdlog)
|
||||
import_external_library(spdlog STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/spdlog/${CMAKE_INSTALL_LIBDIR}/libspdlog.a
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/spdlog/include
|
||||
BUILD_COMMAND $(MAKE) spdlog)
|
||||
|
||||
include(jemalloc.cmake)
|
||||
|
||||
@@ -262,7 +260,6 @@ import_external_library(protobuf STATIC
|
||||
CONFIGURE_COMMAND true)
|
||||
|
||||
set(BOOST_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/boost/lib)
|
||||
set(BOOST_ROOT ${BOOST_ROOT} PARENT_SCOPE)
|
||||
|
||||
import_external_library(pulsar STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/pulsar/pulsar-client-cpp/lib/libpulsarwithdeps.a
|
||||
|
||||
@@ -60,7 +60,7 @@ index 508e4f4..87c5f2a 100644
|
||||
decompressed.bytesWritten(uncompressedSize);
|
||||
decoded = decompressed;
|
||||
diff --git a/pulsar-client-cpp/lib/lz4/lz4.c b/pulsar-client-cpp/lib/lz4/lz4.c
|
||||
index 08cf6b5..07d3e01 100644
|
||||
index 08cf6b5..d74c287 100644
|
||||
--- a/pulsar-client-cpp/lib/lz4/lz4.c
|
||||
+++ b/pulsar-client-cpp/lib/lz4/lz4.c
|
||||
@@ -45,7 +45,7 @@
|
||||
@@ -174,7 +174,7 @@ index 08cf6b5..07d3e01 100644
|
||||
|
||||
#define KB *(1 <<10)
|
||||
#define MB *(1 <<20)
|
||||
@@ -239,15 +239,15 @@ static const int LZ4_minLength = (MFLIMIT+1);
|
||||
@@ -239,7 +239,7 @@ static const int LZ4_minLength = (MFLIMIT+1);
|
||||
/**************************************
|
||||
* Common Utils
|
||||
**************************************/
|
||||
@@ -183,10 +183,9 @@ index 08cf6b5..07d3e01 100644
|
||||
|
||||
|
||||
/**************************************
|
||||
* Common functions
|
||||
@@ -247,7 +247,7 @@ static const int LZ4_minLength = (MFLIMIT+1);
|
||||
**************************************/
|
||||
-static unsigned LZ4_NbCommonBytes (register size_t val)
|
||||
+static unsigned PULSAR_LZ4_NbCommonBytes (register size_t val)
|
||||
static unsigned LZ4_NbCommonBytes (register size_t val)
|
||||
{
|
||||
- if (LZ4_isLittleEndian())
|
||||
+ if (PULSAR_LZ4_isLittleEndian())
|
||||
@@ -207,8 +206,7 @@ index 08cf6b5..07d3e01 100644
|
||||
- size_t diff = LZ4_read_ARCH(pMatch) ^ LZ4_read_ARCH(pIn);
|
||||
+ size_t diff = PULSAR_LZ4_read_ARCH(pMatch) ^ PULSAR_LZ4_read_ARCH(pIn);
|
||||
if (!diff) { pIn+=STEPSIZE; pMatch+=STEPSIZE; continue; }
|
||||
- pIn += LZ4_NbCommonBytes(diff);
|
||||
+ pIn += PULSAR_LZ4_NbCommonBytes(diff);
|
||||
pIn += LZ4_NbCommonBytes(diff);
|
||||
return (unsigned)(pIn - pStart);
|
||||
}
|
||||
|
||||
@@ -219,13 +217,11 @@ index 08cf6b5..07d3e01 100644
|
||||
if ((pIn<pInLimit) && (*pMatch == *pIn)) pIn++;
|
||||
return (unsigned)(pIn - pStart);
|
||||
}
|
||||
@@ -339,8 +339,8 @@ static unsigned LZ4_count(const BYTE* pIn, const BYTE* pMatch, const BYTE* pInLi
|
||||
#define HASHTABLESIZE (1 << LZ4_MEMORY_USAGE)
|
||||
@@ -340,7 +340,7 @@ static unsigned LZ4_count(const BYTE* pIn, const BYTE* pMatch, const BYTE* pInLi
|
||||
#define HASH_SIZE_U32 (1 << LZ4_HASHLOG) /* required as macro for static allocation */
|
||||
|
||||
-static const int LZ4_64Klimit = ((64 KB) + (MFLIMIT-1));
|
||||
static const int LZ4_64Klimit = ((64 KB) + (MFLIMIT-1));
|
||||
-static const U32 LZ4_skipTrigger = 6; /* Increase this value ==> compression run slower on incompressible data */
|
||||
+static const int PULSAR_LZ4_64Klimit = ((64 KB) + (MFLIMIT-1));
|
||||
+static const U32 PULSAR_LZ4_skipTrigger = 6; /* Increase this value ==> compression run slower on incompressible data */
|
||||
|
||||
|
||||
@@ -335,13 +331,11 @@ index 08cf6b5..07d3e01 100644
|
||||
|
||||
const BYTE* ip = (const BYTE*) source;
|
||||
const BYTE* base;
|
||||
@@ -482,12 +482,12 @@ FORCE_INLINE int LZ4_compress_generic(
|
||||
lowLimit = (const BYTE*)source;
|
||||
@@ -483,11 +483,11 @@ FORCE_INLINE int LZ4_compress_generic(
|
||||
break;
|
||||
}
|
||||
- if ((tableType == byU16) && (inputSize>=LZ4_64Klimit)) return 0; /* Size too large (not within 64K limit) */
|
||||
if ((tableType == byU16) && (inputSize>=LZ4_64Klimit)) return 0; /* Size too large (not within 64K limit) */
|
||||
- if (inputSize<LZ4_minLength) goto _last_literals; /* Input too small, no compression (all literals) */
|
||||
+ if ((tableType == byU16) && (inputSize>=PULSAR_LZ4_64Klimit)) return 0; /* Size too large (not within 64K limit) */
|
||||
+ if (inputSize<PULSAR_LZ4_minLength) goto _last_literals; /* Input too small, no compression (all literals) */
|
||||
|
||||
/* First Byte */
|
||||
@@ -473,9 +467,8 @@ index 08cf6b5..07d3e01 100644
|
||||
- if (maxOutputSize >= LZ4_compressBound(inputSize))
|
||||
+ if (maxOutputSize >= PULSAR_LZ4_compressBound(inputSize))
|
||||
{
|
||||
- if (inputSize < LZ4_64Klimit)
|
||||
if (inputSize < LZ4_64Klimit)
|
||||
- return LZ4_compress_generic(state, source, dest, inputSize, 0, notLimited, byU16, noDict, noDictIssue, acceleration);
|
||||
+ if (inputSize < PULSAR_LZ4_64Klimit)
|
||||
+ return PULSAR_LZ4_compress_generic(state, source, dest, inputSize, 0, notLimited, byU16, noDict, noDictIssue, acceleration);
|
||||
else
|
||||
- return LZ4_compress_generic(state, source, dest, inputSize, 0, notLimited, LZ4_64bits() ? byU32 : byPtr, noDict, noDictIssue, acceleration);
|
||||
@@ -483,9 +476,8 @@ index 08cf6b5..07d3e01 100644
|
||||
}
|
||||
else
|
||||
{
|
||||
- if (inputSize < LZ4_64Klimit)
|
||||
if (inputSize < LZ4_64Klimit)
|
||||
- return LZ4_compress_generic(state, source, dest, inputSize, maxOutputSize, limitedOutput, byU16, noDict, noDictIssue, acceleration);
|
||||
+ if (inputSize < PULSAR_LZ4_64Klimit)
|
||||
+ return PULSAR_LZ4_compress_generic(state, source, dest, inputSize, maxOutputSize, limitedOutput, byU16, noDict, noDictIssue, acceleration);
|
||||
else
|
||||
- return LZ4_compress_generic(state, source, dest, inputSize, maxOutputSize, limitedOutput, LZ4_64bits() ? byU32 : byPtr, noDict, noDictIssue, acceleration);
|
||||
@@ -534,9 +526,8 @@ index 08cf6b5..07d3e01 100644
|
||||
- LZ4_resetStream(&ctx);
|
||||
+ PULSAR_LZ4_resetStream(&ctx);
|
||||
|
||||
- if (inputSize < LZ4_64Klimit)
|
||||
if (inputSize < LZ4_64Klimit)
|
||||
- return LZ4_compress_generic(&ctx, source, dest, inputSize, maxOutputSize, limitedOutput, byU16, noDict, noDictIssue, acceleration);
|
||||
+ if (inputSize < PULSAR_LZ4_64Klimit)
|
||||
+ return PULSAR_LZ4_compress_generic(&ctx, source, dest, inputSize, maxOutputSize, limitedOutput, byU16, noDict, noDictIssue, acceleration);
|
||||
else
|
||||
- return LZ4_compress_generic(&ctx, source, dest, inputSize, maxOutputSize, limitedOutput, LZ4_64bits() ? byU32 : byPtr, noDict, noDictIssue, acceleration);
|
||||
@@ -553,13 +544,11 @@ index 08cf6b5..07d3e01 100644
|
||||
void* const ctx,
|
||||
const char* const src,
|
||||
char* const dst,
|
||||
@@ -747,13 +747,13 @@ static int LZ4_compress_destSize_generic(
|
||||
/* Init conditions */
|
||||
@@ -748,12 +748,12 @@ static int LZ4_compress_destSize_generic(
|
||||
if (targetDstSize < 1) return 0; /* Impossible to store anything */
|
||||
if ((U32)*srcSizePtr > (U32)LZ4_MAX_INPUT_SIZE) return 0; /* Unsupported input size, too large (or negative) */
|
||||
- if ((tableType == byU16) && (*srcSizePtr>=LZ4_64Klimit)) return 0; /* Size too large (not within 64K limit) */
|
||||
if ((tableType == byU16) && (*srcSizePtr>=LZ4_64Klimit)) return 0; /* Size too large (not within 64K limit) */
|
||||
- if (*srcSizePtr<LZ4_minLength) goto _last_literals; /* Input too small, no compression (all literals) */
|
||||
+ if ((tableType == byU16) && (*srcSizePtr>=PULSAR_LZ4_64Klimit)) return 0; /* Size too large (not within 64K limit) */
|
||||
+ if (*srcSizePtr<PULSAR_LZ4_minLength) goto _last_literals; /* Input too small, no compression (all literals) */
|
||||
|
||||
/* First Byte */
|
||||
@@ -666,9 +655,8 @@ index 08cf6b5..07d3e01 100644
|
||||
}
|
||||
else
|
||||
{
|
||||
- if (*srcSizePtr < LZ4_64Klimit)
|
||||
if (*srcSizePtr < LZ4_64Klimit)
|
||||
- return LZ4_compress_destSize_generic(state, src, dst, srcSizePtr, targetDstSize, byU16);
|
||||
+ if (*srcSizePtr < PULSAR_LZ4_64Klimit)
|
||||
+ return PULSAR_LZ4_compress_destSize_generic(state, src, dst, srcSizePtr, targetDstSize, byU16);
|
||||
else
|
||||
- return LZ4_compress_destSize_generic(state, src, dst, srcSizePtr, targetDstSize, LZ4_64bits() ? byU32 : byPtr);
|
||||
|
||||
@@ -125,8 +125,8 @@ declare -A primary_urls=(
|
||||
["neo4j"]="http://$local_cache_host/file/neo4j-community-3.2.3-unix.tar.gz"
|
||||
["librdkafka"]="http://$local_cache_host/git/librdkafka.git"
|
||||
["protobuf"]="http://$local_cache_host/git/protobuf.git"
|
||||
["boost"]="http://$local_cache_host/file/boost_1_77_0.tar.gz"
|
||||
["pulsar"]="http://$local_cache_host/git/pulsar.git"
|
||||
["boost"]="https://boostorg.jfrog.io/artifactory/main/release/1.77.0/source/boost_1_77_0.tar.gz"
|
||||
["pulsar"]="https://github.com/apache/pulsar.git"
|
||||
)
|
||||
|
||||
# The goal of secondary urls is to have links to the "source of truth" of
|
||||
@@ -178,7 +178,7 @@ cppitertools_ref="cb3635456bdb531121b82b4d2e3afc7ae1f56d47"
|
||||
repo_clone_try_double "${primary_urls[cppitertools]}" "${secondary_urls[cppitertools]}" "cppitertools" "$cppitertools_ref"
|
||||
|
||||
# fmt
|
||||
fmt_tag="8.0.1" # (2021-07-03)
|
||||
fmt_tag="7.1.3" # (2020-11-25)
|
||||
repo_clone_try_double "${primary_urls[fmt]}" "${secondary_urls[fmt]}" "fmt" "$fmt_tag" true
|
||||
|
||||
# rapidcheck
|
||||
@@ -241,7 +241,7 @@ repo_clone_try_double "${primary_urls[pymgclient]}" "${secondary_urls[pymgclient
|
||||
mgconsole_tag="v1.1.0" # (2021-10-07)
|
||||
repo_clone_try_double "${primary_urls[mgconsole]}" "${secondary_urls[mgconsole]}" "mgconsole" "$mgconsole_tag" true
|
||||
|
||||
spdlog_tag="v1.9.2" # (2021-08-12)
|
||||
spdlog_tag="v1.8.2" # (2020-12-01)
|
||||
repo_clone_try_double "${primary_urls[spdlog]}" "${secondary_urls[spdlog]}" "spdlog" "$spdlog_tag" true
|
||||
|
||||
jemalloc_tag="ea6b3e973b477b8061e0076bb257dbd7f3faa756" # (2021-02-11)
|
||||
@@ -271,7 +271,7 @@ repo_clone_try_double "${primary_urls[librdkafka]}" "${secondary_urls[librdkafka
|
||||
protobuf_tag="v3.12.4"
|
||||
repo_clone_try_double "${primary_urls[protobuf]}" "${secondary_urls[protobuf]}" "protobuf" "$protobuf_tag" true
|
||||
pushd protobuf
|
||||
./autogen.sh && ./configure CC=clang CXX=clang++ --prefix=$(pwd)/lib
|
||||
./autogen.sh && ./configure --prefix=$(pwd)/lib
|
||||
popd
|
||||
|
||||
# boost
|
||||
@@ -279,8 +279,8 @@ file_get_try_double "${primary_urls[boost]}" "${secondary_urls[boost]}"
|
||||
tar -xzf boost_1_77_0.tar.gz
|
||||
mv boost_1_77_0 boost
|
||||
pushd boost
|
||||
./bootstrap.sh --prefix=$(pwd)/lib --with-libraries="system,regex" --with-toolset=clang
|
||||
./b2 toolset=clang -j$(nproc) install variant=release
|
||||
./bootstrap.sh --prefix=$(pwd)/lib --with-libraries="system,regex"
|
||||
./b2 -j$(nproc) install
|
||||
popd
|
||||
|
||||
#pulsar
|
||||
|
||||
@@ -36,7 +36,7 @@ ADDITIONAL USE GRANT: You may use the Licensed Work in accordance with the
|
||||
3. using the Licensed Work to create a work or solution
|
||||
which competes (or might reasonably be expected to
|
||||
compete) with the Licensed Work.
|
||||
CHANGE DATE: 2025-12-08
|
||||
CHANGE DATE: 2025-10-12
|
||||
CHANGE LICENSE: Apache License, Version 2.0
|
||||
|
||||
For information about alternative licensing arrangements, please visit: https://memgraph.com/legal.
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
FROM debian:bullseye
|
||||
FROM debian:buster
|
||||
# NOTE: If you change the base distro update release/package as well.
|
||||
|
||||
ARG deb_release
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
openssl libcurl4 libssl1.1 libseccomp2 python3 libpython3.9 python3-pip \
|
||||
openssl libcurl4 libssl1.1 libseccomp2 python3 libpython3.7 python3-pip \
|
||||
--no-install-recommends \
|
||||
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
|
||||
RUN pip3 install networkx==2.4 numpy==1.21.4 scipy==1.7.3
|
||||
RUN pip3 install networkx==2.4 numpy==1.19.2 scipy==1.5.2
|
||||
|
||||
COPY ${deb_release} /
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ make_package () {
|
||||
docker exec "$build_container" bash -c "/memgraph/environment/os/$os.sh install MEMGRAPH_BUILD_DEPS"
|
||||
|
||||
echo "Building targeted package..."
|
||||
docker exec "$build_container" bash -c "cd /memgraph && $ACTIVATE_TOOLCHAIN && ./init"
|
||||
docker exec "$build_container" bash -c "cd /memgraph && ./init"
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && rm -rf ./*"
|
||||
docker exec "$build_container" bash -c "cd $container_build_dir && $ACTIVATE_TOOLCHAIN && cmake -DCMAKE_BUILD_TYPE=release $telemetry_id_override_flag .."
|
||||
# ' is used instead of " because we need to run make within the allowed
|
||||
@@ -100,8 +100,8 @@ case "$1" in
|
||||
;;
|
||||
|
||||
docker)
|
||||
# NOTE: Docker is build on top of Debian 11 package.
|
||||
based_on_os="debian-11"
|
||||
# NOTE: Docker is build on top of Debian 10 package.
|
||||
based_on_os="debian-10"
|
||||
# shellcheck disable=SC2012
|
||||
last_package_name=$(cd "$HOST_OUTPUT_DIR/$based_on_os" && ls -t memgraph* | head -1)
|
||||
docker_build_folder="$PROJECT_ROOT/release/docker"
|
||||
|
||||
@@ -37,7 +37,7 @@ set(mg_single_node_v2_sources
|
||||
)
|
||||
|
||||
set(mg_single_node_v2_libs stdc++fs Threads::Threads
|
||||
telemetry_lib mg-query mg-communication mg-memory mg-utils mg-auth mg-license mg-settings)
|
||||
telemetry_lib mg-query mg-communication mg-memory mg-utils mg-auth)
|
||||
if (MG_ENTERPRISE)
|
||||
# These are enterprise subsystems
|
||||
set(mg_single_node_v2_libs ${mg_single_node_v2_libs} mg-audit)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
set(audit_src_files log.cpp)
|
||||
|
||||
add_library(mg-audit STATIC ${audit_src_files})
|
||||
target_link_libraries(mg-audit json gflags fmt::fmt)
|
||||
target_link_libraries(mg-audit json gflags fmt)
|
||||
target_link_libraries(mg-audit mg-utils mg-storage-v2)
|
||||
|
||||
@@ -7,7 +7,7 @@ set(auth_src_files
|
||||
find_package(Seccomp REQUIRED)
|
||||
|
||||
add_library(mg-auth STATIC ${auth_src_files})
|
||||
target_link_libraries(mg-auth json libbcrypt gflags fmt::fmt)
|
||||
target_link_libraries(mg-auth json libbcrypt gflags fmt)
|
||||
target_link_libraries(mg-auth mg-utils mg-kvstore)
|
||||
|
||||
target_link_libraries(mg-auth ${Seccomp_LIBRARIES})
|
||||
|
||||
@@ -6,10 +6,8 @@ set(communication_src_files
|
||||
helpers.cpp
|
||||
init.cpp)
|
||||
|
||||
find_package(Boost REQUIRED)
|
||||
|
||||
add_library(mg-communication STATIC ${communication_src_files})
|
||||
target_link_libraries(mg-communication Boost::headers Threads::Threads mg-utils mg-io fmt::fmt gflags)
|
||||
target_link_libraries(mg-communication Threads::Threads mg-utils mg-io fmt gflags)
|
||||
|
||||
find_package(OpenSSL REQUIRED)
|
||||
target_link_libraries(mg-communication ${OPENSSL_LIBRARIES})
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <list>
|
||||
#include <memory>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/core.hpp>
|
||||
|
||||
#include "communication/websocket/session.hpp"
|
||||
#include "utils/spin_lock.hpp"
|
||||
#include "utils/synchronized.hpp"
|
||||
|
||||
namespace communication::websocket {
|
||||
template <typename TSession, typename TSessionData>
|
||||
class Listener : public std::enable_shared_from_this<Listener<TSession, TSessionData>> {
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
public:
|
||||
template <typename... Args>
|
||||
static std::shared_ptr<Listener> Create(Args &&...args) {
|
||||
return std::shared_ptr<Listener>{new Listener(std::forward<Args>(args)...)};
|
||||
}
|
||||
|
||||
// Start accepting incoming connections
|
||||
void Run() {
|
||||
// The new connection gets its own strand
|
||||
acceptor_.async_accept(ioc_, [shared_this = this->shared_from_this()](auto ec, auto socket) {
|
||||
shared_this->OnAccept(ec, std::move(socket));
|
||||
});
|
||||
}
|
||||
|
||||
private:
|
||||
Listener(boost::asio::io_context &ioc, tcp::endpoint endpoint, TSessionData *data)
|
||||
: data_{data}, ioc_(ioc), acceptor_(ioc) {
|
||||
boost::beast::error_code ec;
|
||||
|
||||
// Open the acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if (ec) {
|
||||
LogError(ec, "open");
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow address reuse
|
||||
acceptor_.set_option(boost::asio::socket_base::reuse_address(true), ec);
|
||||
if (ec) {
|
||||
LogError(ec, "set_option");
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind to the server address
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if (ec) {
|
||||
LogError(ec, "bind");
|
||||
return;
|
||||
}
|
||||
|
||||
acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec);
|
||||
if (ec) {
|
||||
LogError(ec, "listen");
|
||||
return;
|
||||
}
|
||||
|
||||
spdlog::info("WebSocket server is listening on {}:{}", endpoint.address(), endpoint.port());
|
||||
}
|
||||
|
||||
void OnAccept(boost::beast::error_code ec, tcp::socket socket) {
|
||||
if (ec) {
|
||||
return LogError(ec, "accept");
|
||||
}
|
||||
|
||||
Session<TSession, TSessionData>::Create(std::move(socket), data_)->Run();
|
||||
Run();
|
||||
}
|
||||
|
||||
TSessionData *data_;
|
||||
boost::asio::io_context &ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
};
|
||||
} // namespace communication::websocket
|
||||
@@ -1,69 +0,0 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#define BOOST_ASIO_USE_TS_EXECUTOR_AS_DEFAULT
|
||||
|
||||
#include <thread>
|
||||
|
||||
#include <spdlog/sinks/base_sink.h>
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
|
||||
#include "communication/websocket/listener.hpp"
|
||||
#include "io/network/endpoint.hpp"
|
||||
|
||||
namespace communication::websocket {
|
||||
|
||||
template <typename TSession, typename TSessionData>
|
||||
class Server final {
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
public:
|
||||
explicit Server(io::network::Endpoint endpoint, TSessionData *data)
|
||||
: ioc_{},
|
||||
listener_{Listener<TSession, TSessionData>::Create(
|
||||
ioc_, tcp::endpoint{boost::asio::ip::make_address(endpoint.address), endpoint.port}, data)} {}
|
||||
|
||||
Server(const Server &) = delete;
|
||||
Server(Server &&) = delete;
|
||||
Server &operator=(const Server &) = delete;
|
||||
Server &operator=(Server &&) = delete;
|
||||
|
||||
~Server() {
|
||||
MG_ASSERT(!background_thread_ || (ioc_.stopped() && !background_thread_->joinable()),
|
||||
"Server wasn't shutdown properly");
|
||||
}
|
||||
|
||||
void Start() {
|
||||
MG_ASSERT(!background_thread_, "The server was already started!");
|
||||
listener_->Run();
|
||||
background_thread_.emplace([this] { ioc_.run(); });
|
||||
}
|
||||
|
||||
void Shutdown() { ioc_.stop(); }
|
||||
|
||||
void AwaitShutdown() {
|
||||
if (background_thread_ && background_thread_->joinable()) {
|
||||
background_thread_->join();
|
||||
}
|
||||
}
|
||||
|
||||
bool IsRunning() const { return background_thread_ && !ioc_.stopped(); }
|
||||
|
||||
private:
|
||||
boost::asio::io_context ioc_;
|
||||
|
||||
std::shared_ptr<Listener<TSession, TSessionData>> listener_;
|
||||
std::optional<std::thread> background_thread_;
|
||||
};
|
||||
} // namespace communication::websocket
|
||||
@@ -1,157 +0,0 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <span>
|
||||
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/beast/core/tcp_stream.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/websocket.hpp>
|
||||
|
||||
#include "communication/buffer.hpp"
|
||||
#include "communication/session.hpp"
|
||||
|
||||
namespace communication::websocket {
|
||||
void LogError(boost::beast::error_code ec, const std::string_view what) {
|
||||
spdlog::warn("Websocket session failed on {}: {}", what, ec.message());
|
||||
}
|
||||
|
||||
template <typename TSession, typename TSessionData>
|
||||
class Session : public std::enable_shared_from_this<Session<TSession, TSessionData>> {
|
||||
using tcp = boost::asio::ip::tcp;
|
||||
|
||||
public:
|
||||
template <typename... Args>
|
||||
static std::shared_ptr<Session> Create(Args &&...args) {
|
||||
return std::shared_ptr<Session>{new Session{std::forward<Args>(args)...}};
|
||||
}
|
||||
|
||||
void Run() {
|
||||
boost::asio::dispatch(strand_, [shared_this = this->shared_from_this()] { shared_this->OnRun(); });
|
||||
}
|
||||
|
||||
private:
|
||||
explicit Session(tcp::socket &&socket, TSessionData *data)
|
||||
: endpoint_(socket.local_endpoint()),
|
||||
ws_(std::move(socket)),
|
||||
strand_{boost::asio::make_strand(ws_.get_executor())},
|
||||
data_{data} {}
|
||||
|
||||
void OnRun() {
|
||||
ws_.set_option(boost::beast::websocket::stream_base::timeout::suggested(boost::beast::role_type::server));
|
||||
boost::asio::socket_base::keep_alive option(true);
|
||||
ws_.set_option(boost::beast::websocket::stream_base::decorator([](boost::beast::websocket::response_type &res) {
|
||||
res.set(boost::beast::http::field::server, "Memgraph WS");
|
||||
res.set(boost::beast::http::field::sec_websocket_protocol, "binary");
|
||||
}));
|
||||
|
||||
ws_.binary(true);
|
||||
// This buffer will hold the HTTP request as raw characters
|
||||
|
||||
// This buffer is required for reading HTTP messages
|
||||
// flat_buffer buffer;
|
||||
|
||||
// Read the HTTP request ourselves
|
||||
// boost::beast::http::request<http::string_body> req;
|
||||
// http::read(sock, buffer, req);
|
||||
// std::cout << "bu
|
||||
// Read into our buffer until we reach the end of the HTTP request.
|
||||
// No parsing takes place here, we are just accumulating data.
|
||||
|
||||
// boost::beast::net::read_until(sock, net::dynamic_buffer(s), "\r\n\r\n");
|
||||
|
||||
// Now accept the connection, using the buffered data.
|
||||
// ws_.accept(net::buffer(s));
|
||||
ws_.async_accept(boost::asio::bind_executor(
|
||||
strand_, [shared_this = this->shared_from_this()](auto ec) { shared_this->OnAccept(ec); }));
|
||||
}
|
||||
|
||||
void OnAccept(boost::beast::error_code ec) {
|
||||
if (ec) {
|
||||
return LogError(ec, "accept");
|
||||
}
|
||||
|
||||
session_data_.emplace(this->shared_from_this());
|
||||
|
||||
// run on the strand
|
||||
boost::asio::dispatch(strand_, [shared_this = this->shared_from_this()] { shared_this->DoRead(); });
|
||||
}
|
||||
|
||||
void DoWrite(const uint8_t *data, size_t len, bool have_more = false) {
|
||||
boost::beast::error_code ec;
|
||||
ws_.write(boost::asio::const_buffer{data, len}, ec);
|
||||
if (ec) {
|
||||
return LogError(ec, "write");
|
||||
}
|
||||
}
|
||||
|
||||
void DoRead() {
|
||||
auto buf = session_data_->input_buffer_.write_end()->Allocate();
|
||||
|
||||
auto mutable_buffer = std::make_unique<boost::asio::mutable_buffer>(buf.data, buf.len);
|
||||
|
||||
ws_.async_read_some(
|
||||
*mutable_buffer,
|
||||
boost::asio::bind_executor(
|
||||
strand_, [mutable_buffer = std::move(mutable_buffer), shared_this = this->shared_from_this()](
|
||||
auto ec, auto bytes_transferred) { shared_this->OnRead(ec, bytes_transferred); }));
|
||||
}
|
||||
|
||||
void OnRead(boost::beast::error_code ec, size_t bytes_transferred) {
|
||||
if (ec == boost::beast::websocket::error::closed) {
|
||||
return;
|
||||
}
|
||||
if (ec) {
|
||||
return LogError(ec, "read");
|
||||
}
|
||||
|
||||
session_data_->input_buffer_.write_end()->Written(bytes_transferred);
|
||||
|
||||
try {
|
||||
session_data_->session_.Execute();
|
||||
} catch (const SessionClosedException &e) {
|
||||
spdlog::info("Closed connection");
|
||||
return;
|
||||
}
|
||||
DoRead();
|
||||
}
|
||||
|
||||
boost::asio::ip::tcp::endpoint endpoint_;
|
||||
boost::beast::websocket::stream<boost::beast::tcp_stream> ws_;
|
||||
boost::beast::flat_buffer buffer_;
|
||||
boost::asio::strand<decltype(ws_)::executor_type> strand_;
|
||||
|
||||
TSessionData *data_;
|
||||
|
||||
struct SessionData {
|
||||
explicit SessionData(std::shared_ptr<Session<TSession, TSessionData>> session)
|
||||
: output_stream_([session](const uint8_t *data, size_t len, bool have_more) {
|
||||
session->DoWrite(data, len, have_more);
|
||||
return true;
|
||||
}),
|
||||
session_(session->data_,
|
||||
io::network::Endpoint{session->endpoint_.address().to_string(), session->endpoint_.port()},
|
||||
input_buffer_.read_end(), &output_stream_) {}
|
||||
|
||||
Buffer input_buffer_;
|
||||
communication::OutputStream output_stream_;
|
||||
TSession session_;
|
||||
};
|
||||
|
||||
std::optional<SessionData> session_data_;
|
||||
};
|
||||
} // namespace communication::websocket
|
||||
@@ -13,7 +13,9 @@
|
||||
#include <chrono>
|
||||
|
||||
namespace integrations {
|
||||
constexpr int64_t kDefaultCheckBatchLimit{1};
|
||||
constexpr std::chrono::milliseconds kDefaultBatchInterval{100};
|
||||
constexpr int64_t kDefaultBatchSize = 1000;
|
||||
constexpr int64_t kDefaultCheckBatchLimit = 1;
|
||||
constexpr std::chrono::milliseconds kDefaultCheckTimeout{30000};
|
||||
constexpr std::chrono::milliseconds kMinimumInterval{1};
|
||||
constexpr int64_t kMinimumSize{1};
|
||||
|
||||
@@ -35,13 +35,14 @@ utils::BasicResult<std::string, std::vector<Message>> GetBatch(RdKafka::KafkaCon
|
||||
std::atomic<bool> &is_running) {
|
||||
std::vector<Message> batch{};
|
||||
|
||||
batch.reserve(info.batch_size);
|
||||
int64_t batch_size = info.batch_size.value_or(kDefaultBatchSize);
|
||||
batch.reserve(batch_size);
|
||||
|
||||
auto remaining_timeout_in_ms = info.batch_interval.count();
|
||||
auto remaining_timeout_in_ms = info.batch_interval.value_or(kDefaultBatchInterval).count();
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
bool run_batch = true;
|
||||
for (int64_t i = 0; remaining_timeout_in_ms > 0 && i < info.batch_size && is_running.load(); ++i) {
|
||||
for (int64_t i = 0; remaining_timeout_in_ms > 0 && i < batch_size && is_running.load(); ++i) {
|
||||
std::unique_ptr<RdKafka::Message> msg(consumer.consume(remaining_timeout_in_ms));
|
||||
switch (msg->err()) {
|
||||
case RdKafka::ERR__TIMED_OUT:
|
||||
@@ -72,7 +73,7 @@ utils::BasicResult<std::string, std::vector<Message>> GetBatch(RdKafka::KafkaCon
|
||||
start = now;
|
||||
}
|
||||
|
||||
return std::move(batch);
|
||||
return {std::move(batch)};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -103,19 +104,14 @@ int64_t Message::Timestamp() const {
|
||||
return rd_kafka_message_timestamp(c_message, nullptr);
|
||||
}
|
||||
|
||||
int64_t Message::Offset() const {
|
||||
const auto *c_message = message_->c_ptr();
|
||||
return c_message->offset;
|
||||
}
|
||||
|
||||
Consumer::Consumer(ConsumerInfo info, ConsumerFunction consumer_function)
|
||||
: info_{std::move(info)}, consumer_function_(std::move(consumer_function)), cb_(info_.consumer_name) {
|
||||
: info_{std::move(info)}, consumer_function_(std::move(consumer_function)) {
|
||||
MG_ASSERT(consumer_function_, "Empty consumer function for Kafka consumer");
|
||||
// NOLINTNEXTLINE (modernize-use-nullptr)
|
||||
if (info_.batch_interval < kMinimumInterval) {
|
||||
if (info_.batch_interval.value_or(kMinimumInterval) < kMinimumInterval) {
|
||||
throw ConsumerFailedToInitializeException(info_.consumer_name, "Batch interval has to be positive!");
|
||||
}
|
||||
if (info_.batch_size < kMinimumSize) {
|
||||
if (info_.batch_size.value_or(kMinimumSize) < kMinimumSize) {
|
||||
throw ConsumerFailedToInitializeException(info_.consumer_name, "Batch size has to be positive!");
|
||||
}
|
||||
|
||||
@@ -130,10 +126,6 @@ Consumer::Consumer(ConsumerInfo info, ConsumerFunction consumer_function)
|
||||
throw ConsumerFailedToInitializeException(info_.consumer_name, error);
|
||||
}
|
||||
|
||||
if (conf->set("rebalance_cb", &cb_, error) != RdKafka::Conf::CONF_OK) {
|
||||
throw ConsumerFailedToInitializeException(info_.consumer_name, error);
|
||||
}
|
||||
|
||||
if (conf->set("enable.partition.eof", "false", error) != RdKafka::Conf::CONF_OK) {
|
||||
throw ConsumerFailedToInitializeException(info_.consumer_name, error);
|
||||
}
|
||||
@@ -173,16 +165,7 @@ Consumer::Consumer(ConsumerInfo info, ConsumerFunction consumer_function)
|
||||
std::inserter(topic_names_from_metadata, topic_names_from_metadata.begin()),
|
||||
[](const auto topic_metadata) { return topic_metadata->topic(); });
|
||||
|
||||
constexpr size_t max_topic_name_length = 249;
|
||||
constexpr auto is_valid_topic_name = [](const auto c) { return std::isalnum(c) || c == '.' || c == '_' || c == '-'; };
|
||||
|
||||
for (const auto &topic_name : info_.topics) {
|
||||
if (topic_name.size() > max_topic_name_length ||
|
||||
std::any_of(topic_name.begin(), topic_name.end(), [&](const auto c) { return !is_valid_topic_name(c); })) {
|
||||
throw ConsumerFailedToInitializeException(info_.consumer_name,
|
||||
fmt::format("'{}' is an invalid topic name", topic_name));
|
||||
}
|
||||
|
||||
if (!topic_names_from_metadata.contains(topic_name)) {
|
||||
throw TopicNotFoundException(info_.consumer_name, topic_name);
|
||||
}
|
||||
@@ -252,18 +235,10 @@ void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::opti
|
||||
utils::OnScopeExit restore_is_running([this] { is_running_.store(false); });
|
||||
|
||||
if (last_assignment_.empty()) {
|
||||
auto throw_consumer_check_failed = [this](const auto err) {
|
||||
if (const auto err = consumer_->assignment(last_assignment_); err != RdKafka::ERR_NO_ERROR) {
|
||||
spdlog::warn("Saving the commited offset of consumer {} failed: {}", info_.consumer_name, RdKafka::err2str(err));
|
||||
throw ConsumerCheckFailedException(info_.consumer_name,
|
||||
fmt::format("Couldn't save commited offsets: '{}'", RdKafka::err2str(err)));
|
||||
};
|
||||
if (const auto err = consumer_->assignment(last_assignment_); err != RdKafka::ERR_NO_ERROR) {
|
||||
spdlog::warn("Saving the assignment of consumer {} failed: {}", info_.consumer_name, RdKafka::err2str(err));
|
||||
throw_consumer_check_failed(err);
|
||||
}
|
||||
if (const auto err = consumer_->position(last_assignment_); err != RdKafka::ERR_NO_ERROR) {
|
||||
spdlog::warn("Saving the position offset assignment of consumer {} failed: {}", info_.consumer_name,
|
||||
RdKafka::err2str(err));
|
||||
throw_consumer_check_failed(err);
|
||||
}
|
||||
} else {
|
||||
if (const auto err = consumer_->assign(last_assignment_); err != RdKafka::ERR_NO_ERROR) {
|
||||
@@ -359,18 +334,7 @@ void Consumer::StartConsuming() {
|
||||
|
||||
try {
|
||||
consumer_function_(batch);
|
||||
std::vector<RdKafka::TopicPartition *> partitions;
|
||||
utils::OnScopeExit clear_partitions([&]() { RdKafka::TopicPartition::destroy(partitions); });
|
||||
|
||||
if (const auto err = consumer_->assignment(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCheckFailedException(
|
||||
info_.consumer_name, fmt::format("Couldn't get assignment to commit offsets: {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer_->position(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
throw ConsumerCheckFailedException(
|
||||
info_.consumer_name, fmt::format("Couldn't get offsets from librdkafka {}", RdKafka::err2str(err)));
|
||||
}
|
||||
if (const auto err = consumer_->commitSync(partitions); err != RdKafka::ERR_NO_ERROR) {
|
||||
if (const auto err = consumer_->commitSync(); err != RdKafka::ERR_NO_ERROR) {
|
||||
spdlog::warn("Committing offset of consumer {} failed: {}", info_.consumer_name, RdKafka::err2str(err));
|
||||
break;
|
||||
}
|
||||
@@ -389,51 +353,4 @@ void Consumer::StopConsuming() {
|
||||
if (thread_.joinable()) thread_.join();
|
||||
}
|
||||
|
||||
utils::BasicResult<std::string> Consumer::SetConsumerOffsets(int64_t offset) {
|
||||
if (is_running_) {
|
||||
throw ConsumerRunningException(info_.consumer_name);
|
||||
}
|
||||
|
||||
if (offset == -1) {
|
||||
offset = RD_KAFKA_OFFSET_BEGINNING;
|
||||
} else if (offset == -2) {
|
||||
offset = RD_KAFKA_OFFSET_END;
|
||||
}
|
||||
|
||||
cb_.set_offset(offset);
|
||||
if (const auto err = consumer_->subscribe(info_.topics); err != RdKafka::ERR_NO_ERROR) {
|
||||
return fmt::format("Could not set offset of consumer: {}. Error: {}", info_.consumer_name, RdKafka::err2str(err));
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
Consumer::ConsumerRebalanceCb::ConsumerRebalanceCb(std::string consumer_name)
|
||||
: consumer_name_(std::move(consumer_name)) {}
|
||||
|
||||
void Consumer::ConsumerRebalanceCb::rebalance_cb(RdKafka::KafkaConsumer *consumer, RdKafka::ErrorCode err,
|
||||
std::vector<RdKafka::TopicPartition *> &partitions) {
|
||||
if (err == RdKafka::ERR__REVOKE_PARTITIONS) {
|
||||
consumer->unassign();
|
||||
return;
|
||||
}
|
||||
if (err != RdKafka::ERR__ASSIGN_PARTITIONS) {
|
||||
spdlog::critical("Consumer {} received an unexpected error {}", consumer_name_, RdKafka::err2str(err));
|
||||
return;
|
||||
}
|
||||
if (offset_) {
|
||||
for (auto &partition : partitions) {
|
||||
partition->set_offset(*offset_);
|
||||
}
|
||||
offset_.reset();
|
||||
}
|
||||
auto maybe_error = consumer->assign(partitions);
|
||||
if (maybe_error != RdKafka::ErrorCode::ERR_NO_ERROR) {
|
||||
spdlog::warn("Assigning offset of consumer {} failed: {}", consumer_name_, RdKafka::err2str(err));
|
||||
}
|
||||
maybe_error = consumer->commitSync(partitions);
|
||||
if (maybe_error != RdKafka::ErrorCode::ERR_NO_ERROR) {
|
||||
spdlog::warn("Commiting offsets of consumer {} failed: {}", consumer_name_, RdKafka::err2str(err));
|
||||
}
|
||||
}
|
||||
void Consumer::ConsumerRebalanceCb::set_offset(int64_t offset) { offset_ = offset; }
|
||||
} // namespace integrations::kafka
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
@@ -69,9 +68,6 @@ class Message final {
|
||||
/// can be implemented knowing that.
|
||||
int64_t Timestamp() const;
|
||||
|
||||
/// Returns the offset of the message
|
||||
int64_t Offset() const;
|
||||
|
||||
private:
|
||||
std::unique_ptr<RdKafka::Message> message_;
|
||||
};
|
||||
@@ -84,8 +80,8 @@ struct ConsumerInfo {
|
||||
std::vector<std::string> topics;
|
||||
std::string consumer_group;
|
||||
std::string bootstrap_servers;
|
||||
std::chrono::milliseconds batch_interval;
|
||||
int64_t batch_size;
|
||||
std::optional<std::chrono::milliseconds> batch_interval;
|
||||
std::optional<int64_t> batch_size;
|
||||
};
|
||||
|
||||
/// Memgraphs Kafka consumer wrapper.
|
||||
@@ -142,13 +138,6 @@ class Consumer final : public RdKafka::EventCb {
|
||||
/// Returns true if the consumer is actively consuming messages.
|
||||
bool IsRunning() const;
|
||||
|
||||
/// Sets the consumer's offset.
|
||||
///
|
||||
/// This function returns the empty string on success or an error message otherwise.
|
||||
///
|
||||
/// @param offset: the offset to set.
|
||||
[[nodiscard]] utils::BasicResult<std::string> SetConsumerOffsets(int64_t offset);
|
||||
|
||||
const ConsumerInfo &Info() const;
|
||||
|
||||
private:
|
||||
@@ -158,20 +147,6 @@ class Consumer final : public RdKafka::EventCb {
|
||||
|
||||
void StopConsuming();
|
||||
|
||||
class ConsumerRebalanceCb : public RdKafka::RebalanceCb {
|
||||
public:
|
||||
ConsumerRebalanceCb(std::string consumer_name);
|
||||
|
||||
void rebalance_cb(RdKafka::KafkaConsumer *consumer, RdKafka::ErrorCode err,
|
||||
std::vector<RdKafka::TopicPartition *> &partitions) override final;
|
||||
|
||||
void set_offset(int64_t offset);
|
||||
|
||||
private:
|
||||
std::optional<int64_t> offset_;
|
||||
std::string consumer_name_;
|
||||
};
|
||||
|
||||
ConsumerInfo info_;
|
||||
ConsumerFunction consumer_function_;
|
||||
mutable std::atomic<bool> is_running_{false};
|
||||
@@ -179,6 +154,5 @@ class Consumer final : public RdKafka::EventCb {
|
||||
std::optional<int64_t> limit_batches_{std::nullopt};
|
||||
std::unique_ptr<RdKafka::KafkaConsumer, std::function<void(RdKafka::KafkaConsumer *)>> consumer_;
|
||||
std::thread thread_;
|
||||
ConsumerRebalanceCb cb_;
|
||||
};
|
||||
} // namespace integrations::kafka
|
||||
|
||||
@@ -45,25 +45,23 @@ pulsar_client::Result ConsumeMessage(pulsar_client::Reader &reader, pulsar_clien
|
||||
|
||||
template <PulsarConsumer TConsumer>
|
||||
utils::BasicResult<std::string, std::vector<Message>> GetBatch(TConsumer &consumer, const ConsumerInfo &info,
|
||||
std::atomic<bool> &is_running,
|
||||
const pulsar_client::MessageId &last_message_id) {
|
||||
std::atomic<bool> &is_running) {
|
||||
std::vector<Message> batch{};
|
||||
|
||||
batch.reserve(info.batch_size);
|
||||
const auto batch_size = info.batch_size.value_or(kDefaultBatchSize);
|
||||
batch.reserve(batch_size);
|
||||
|
||||
auto remaining_timeout_in_ms = info.batch_interval.count();
|
||||
auto remaining_timeout_in_ms = info.batch_interval.value_or(kDefaultBatchInterval).count();
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
|
||||
while (remaining_timeout_in_ms > 0 && batch.size() < info.batch_size && is_running) {
|
||||
for (int64_t i = 0; remaining_timeout_in_ms > 0 && i < batch_size && is_running.load(); ++i) {
|
||||
pulsar_client::Message message;
|
||||
const auto result = ConsumeMessage(consumer, message, remaining_timeout_in_ms);
|
||||
switch (result) {
|
||||
case pulsar_client::Result::ResultTimeout:
|
||||
return std::move(batch);
|
||||
case pulsar_client::Result::ResultOk:
|
||||
if (message.getMessageId() != last_message_id) {
|
||||
batch.emplace_back(Message{std::move(message)});
|
||||
}
|
||||
batch.emplace_back(Message{std::move(message)});
|
||||
break;
|
||||
default:
|
||||
spdlog::warn(fmt::format("Unexpected error while consuming message from consumer {}, error: {}",
|
||||
@@ -89,7 +87,15 @@ class SpdlogLogger : public pulsar_client::Logger {
|
||||
};
|
||||
|
||||
class SpdlogLoggerFactory : public pulsar_client::LoggerFactory {
|
||||
pulsar_client::Logger *getLogger(const std::string & /*file_name*/) override { return new SpdlogLogger; }
|
||||
pulsar_client::Logger *getLogger(const std::string & /*file_name*/) override {
|
||||
if (!logger_) {
|
||||
logger_ = std::make_unique<SpdlogLogger>();
|
||||
}
|
||||
return logger_.get();
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<SpdlogLogger> logger_;
|
||||
};
|
||||
|
||||
pulsar_client::Client CreateClient(const std::string &service_url) {
|
||||
@@ -105,8 +111,6 @@ std::span<const char> Message::Payload() const {
|
||||
return {static_cast<const char *>(message_.getData()), message_.getLength()};
|
||||
}
|
||||
|
||||
std::string_view Message::TopicName() const { return message_.getTopicName(); }
|
||||
|
||||
Consumer::Consumer(ConsumerInfo info, ConsumerFunction consumer_function)
|
||||
: info_{std::move(info)},
|
||||
client_{CreateClient(info_.service_url)},
|
||||
@@ -181,7 +185,7 @@ void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::opti
|
||||
const auto start = std::chrono::steady_clock::now();
|
||||
|
||||
if (info_.topics.size() != 1) {
|
||||
throw ConsumerCheckFailedException(info_.consumer_name, "Check cannot be used for consumers with multiple topics.");
|
||||
throw ConsumerCheckFailedException(info_.consumer_name, "Check cannot be used for multiple topics");
|
||||
}
|
||||
|
||||
std::vector<std::string> partitions;
|
||||
@@ -199,7 +203,7 @@ void Consumer::Check(std::optional<std::chrono::milliseconds> timeout, std::opti
|
||||
throw ConsumerCheckFailedException(info_.consumer_name, "Timeout reached");
|
||||
}
|
||||
|
||||
auto maybe_batch = GetBatch(reader, info_, is_running_, last_message_id_);
|
||||
auto maybe_batch = GetBatch(reader, info_, is_running_);
|
||||
|
||||
if (maybe_batch.HasError()) {
|
||||
throw ConsumerCheckFailedException(info_.consumer_name, maybe_batch.GetError());
|
||||
@@ -237,7 +241,7 @@ void Consumer::StartConsuming() {
|
||||
utils::ThreadSetName(full_thread_name.substr(0, kMaxThreadNameSize));
|
||||
|
||||
while (is_running_) {
|
||||
auto maybe_batch = GetBatch(consumer_, info_, is_running_, last_message_id_);
|
||||
auto maybe_batch = GetBatch(consumer_, info_, is_running_);
|
||||
|
||||
if (maybe_batch.HasError()) {
|
||||
spdlog::warn("Error happened in consumer {} while fetching messages: {}!", info_.consumer_name,
|
||||
|
||||
@@ -28,7 +28,6 @@ class Message final {
|
||||
explicit Message(pulsar_client::Message &&message);
|
||||
|
||||
std::span<const char> Payload() const;
|
||||
std::string_view TopicName() const;
|
||||
|
||||
private:
|
||||
pulsar_client::Message message_;
|
||||
@@ -39,8 +38,8 @@ class Message final {
|
||||
using ConsumerFunction = std::function<void(const std::vector<Message> &)>;
|
||||
|
||||
struct ConsumerInfo {
|
||||
int64_t batch_size;
|
||||
std::chrono::milliseconds batch_interval;
|
||||
std::optional<int64_t> batch_size;
|
||||
std::optional<std::chrono::milliseconds> batch_interval;
|
||||
std::vector<std::string> topics;
|
||||
std::string consumer_name;
|
||||
std::string service_url;
|
||||
|
||||
@@ -5,4 +5,4 @@ set(io_src_files
|
||||
network/utils.cpp)
|
||||
|
||||
add_library(mg-io STATIC ${io_src_files})
|
||||
target_link_libraries(mg-io stdc++fs Threads::Threads fmt::fmt mg-utils)
|
||||
target_link_libraries(mg-io stdc++fs Threads::Threads fmt mg-utils)
|
||||
|
||||
@@ -32,7 +32,6 @@
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
|
||||
#include "communication/bolt/v1/constants.hpp"
|
||||
#include "communication/websocket/server.hpp"
|
||||
#include "helpers.hpp"
|
||||
#include "py/py.hpp"
|
||||
#include "query/auth_checker.hpp"
|
||||
@@ -187,22 +186,10 @@ DEFINE_bool(telemetry_enabled, false,
|
||||
"the database runtime (vertex and edge counts and resource usage) "
|
||||
"to allow for easier improvement of the product.");
|
||||
|
||||
// Streams flags
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint32(
|
||||
stream_transaction_conflict_retries, 30,
|
||||
"Number of times to retry when a stream transformation fails to commit because of conflicting transactions");
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint32(
|
||||
stream_transaction_retry_interval, 500,
|
||||
"Retry interval in milliseconds when a stream transformation fails to commit because of conflicting transactions");
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(kafka_bootstrap_servers, "",
|
||||
"List of default Kafka brokers as a comma separated list of broker host or host:port.");
|
||||
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_string(pulsar_service_url, "", "Default URL used while connecting to Pulsar brokers.");
|
||||
|
||||
// Audit logging flags.
|
||||
#ifdef MG_ENTERPRISE
|
||||
DEFINE_bool(audit_enabled, false, "Set to true to enable audit logging.");
|
||||
@@ -1132,15 +1119,11 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
storage::Storage db(db_config);
|
||||
|
||||
query::InterpreterContext interpreter_context{
|
||||
&db,
|
||||
{.query = {.allow_load_csv = FLAGS_allow_load_csv},
|
||||
.execution_timeout_sec = FLAGS_query_execution_timeout_sec,
|
||||
.default_kafka_bootstrap_servers = FLAGS_kafka_bootstrap_servers,
|
||||
.default_pulsar_service_url = FLAGS_pulsar_service_url,
|
||||
.stream_transaction_conflict_retries = FLAGS_stream_transaction_conflict_retries,
|
||||
.stream_transaction_retry_interval = std::chrono::milliseconds(FLAGS_stream_transaction_retry_interval)},
|
||||
FLAGS_data_directory};
|
||||
query::InterpreterContext interpreter_context{&db,
|
||||
{.query = {.allow_load_csv = FLAGS_allow_load_csv},
|
||||
.execution_timeout_sec = FLAGS_query_execution_timeout_sec,
|
||||
.default_kafka_bootstrap_servers = FLAGS_kafka_bootstrap_servers},
|
||||
FLAGS_data_directory};
|
||||
#ifdef MG_ENTERPRISE
|
||||
SessionData session_data{&db, &interpreter_context, &auth, &audit_log};
|
||||
#else
|
||||
@@ -1178,8 +1161,8 @@ int main(int argc, char **argv) {
|
||||
spdlog::warn(utils::MessageWithLink("Using non-secure Bolt connection (without SSL).", "https://memgr.ph/ssl"));
|
||||
}
|
||||
|
||||
auto server = communication::websocket::Server<BoltSession, SessionData>{
|
||||
{FLAGS_bolt_address, static_cast<uint16_t>(FLAGS_bolt_port)}, &session_data};
|
||||
ServerT server({FLAGS_bolt_address, static_cast<uint16_t>(FLAGS_bolt_port)}, &session_data, &context,
|
||||
FLAGS_bolt_session_inactivity_timeout, service_name, FLAGS_bolt_num_workers);
|
||||
|
||||
// Setup telemetry
|
||||
std::optional<telemetry::Telemetry> telemetry;
|
||||
@@ -1213,7 +1196,7 @@ int main(int argc, char **argv) {
|
||||
};
|
||||
InitSignalHandlers(shutdown);
|
||||
|
||||
server.Start();
|
||||
MG_ASSERT(server.Start(), "Couldn't start the Bolt server!");
|
||||
server.AwaitShutdown();
|
||||
query::procedure::gModuleRegistry.UnloadAllModules();
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ set(mg_query_sources
|
||||
interpret/awesome_memgraph_functions.cpp
|
||||
interpret/eval.cpp
|
||||
interpreter.cpp
|
||||
metadata.cpp
|
||||
plan/operator.cpp
|
||||
plan/preprocess.cpp
|
||||
plan/pretty_print.cpp
|
||||
@@ -40,13 +39,11 @@ set(mg_query_sources
|
||||
trigger_context.cpp
|
||||
typed_value.cpp)
|
||||
|
||||
find_package(Boost REQUIRED)
|
||||
|
||||
add_library(mg-query STATIC ${mg_query_sources})
|
||||
add_dependencies(mg-query generate_lcp_query)
|
||||
target_include_directories(mg-query PUBLIC ${CMAKE_SOURCE_DIR}/include)
|
||||
target_link_libraries(mg-query dl cppitertools Boost::headers)
|
||||
target_link_libraries(mg-query mg-integrations-pulsar mg-integrations-kafka mg-storage-v2 mg-license mg-utils mg-kvstore mg-memory)
|
||||
target_link_libraries(mg-query dl cppitertools)
|
||||
target_link_libraries(mg-query mg-integrations-pulsar mg-integrations-kafka mg-storage-v2 mg-utils mg-kvstore mg-memory)
|
||||
if("${MG_PYTHON_VERSION}" STREQUAL "")
|
||||
find_package(Python3 3.5 REQUIRED COMPONENTS Development)
|
||||
else()
|
||||
|
||||
@@ -42,28 +42,15 @@ bool TypedValueCompare(const TypedValue &a, const TypedValue &b) {
|
||||
else
|
||||
return a.ValueDouble() < b.ValueDouble();
|
||||
case TypedValue::Type::String:
|
||||
// NOLINTNEXTLINE(modernize-use-nullptr)
|
||||
return a.ValueString() < b.ValueString();
|
||||
case TypedValue::Type::Date:
|
||||
// NOLINTNEXTLINE(modernize-use-nullptr)
|
||||
return a.ValueDate() < b.ValueDate();
|
||||
case TypedValue::Type::LocalTime:
|
||||
// NOLINTNEXTLINE(modernize-use-nullptr)
|
||||
return a.ValueLocalTime() < b.ValueLocalTime();
|
||||
case TypedValue::Type::LocalDateTime:
|
||||
// NOLINTNEXTLINE(modernize-use-nullptr)
|
||||
return a.ValueLocalDateTime() < b.ValueLocalDateTime();
|
||||
case TypedValue::Type::Duration:
|
||||
// NOLINTNEXTLINE(modernize-use-nullptr)
|
||||
return a.ValueDuration() < b.ValueDuration();
|
||||
case TypedValue::Type::List:
|
||||
case TypedValue::Type::Map:
|
||||
case TypedValue::Type::Vertex:
|
||||
case TypedValue::Type::Edge:
|
||||
case TypedValue::Type::Path:
|
||||
throw QueryRuntimeException("Comparison is not defined for values of type {}.", a.type());
|
||||
case TypedValue::Type::Null:
|
||||
LOG_FATAL("Invalid type");
|
||||
default:
|
||||
LOG_FATAL("Unhandled comparison for types");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,10 @@ namespace impl {
|
||||
bool TypedValueCompare(const TypedValue &a, const TypedValue &b);
|
||||
} // namespace impl
|
||||
|
||||
constexpr inline std::string_view kSerializationErrorMessage{
|
||||
"Cannot resolve conflicting transactions. You can retry this transaction when the conflicting transaction is "
|
||||
"finished."};
|
||||
|
||||
/// Custom Comparator type for comparing vectors of TypedValues.
|
||||
///
|
||||
/// Does lexicographical ordering of elements based on the above
|
||||
@@ -91,7 +95,7 @@ storage::PropertyValue PropsSetChecked(T *record, const storage::PropertyId &key
|
||||
if (maybe_old_value.HasError()) {
|
||||
switch (maybe_old_value.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set properties on a deleted object.");
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
|
||||
namespace query {
|
||||
@@ -23,8 +22,5 @@ struct InterpreterConfig {
|
||||
double execution_timeout_sec{600.0};
|
||||
|
||||
std::string default_kafka_bootstrap_servers;
|
||||
std::string default_pulsar_service_url;
|
||||
uint32_t stream_transaction_conflict_retries;
|
||||
std::chrono::milliseconds stream_transaction_retry_interval;
|
||||
};
|
||||
} // namespace query
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
#include "query/common.hpp"
|
||||
#include "query/frontend/semantic/symbol_table.hpp"
|
||||
#include "query/metadata.hpp"
|
||||
#include "query/parameters.hpp"
|
||||
#include "query/plan/profile.hpp"
|
||||
#include "query/trigger.hpp"
|
||||
@@ -69,7 +68,6 @@ struct ExecutionContext {
|
||||
std::chrono::duration<double> profile_execution_time;
|
||||
plan::ProfilingStats stats;
|
||||
plan::ProfilingStats *stats_root{nullptr};
|
||||
ExecutionStats execution_stats;
|
||||
TriggerContextCollector *trigger_context_collector{nullptr};
|
||||
utils::AsyncTimer timer;
|
||||
};
|
||||
|
||||
@@ -130,18 +130,6 @@ class ExplicitTransactionUsageException : public QueryRuntimeException {
|
||||
using QueryRuntimeException::QueryRuntimeException;
|
||||
};
|
||||
|
||||
/**
|
||||
* An exception for serialization error
|
||||
*/
|
||||
class TransactionSerializationException : public QueryException {
|
||||
public:
|
||||
using QueryException::QueryException;
|
||||
TransactionSerializationException()
|
||||
: QueryException(
|
||||
"Cannot resolve conflicting transactions. You can retry this transaction when the conflicting transaction "
|
||||
"is finished") {}
|
||||
};
|
||||
|
||||
class ReconstructionException : public QueryException {
|
||||
public:
|
||||
ReconstructionException()
|
||||
|
||||
@@ -32,14 +32,11 @@
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
#include <boost/preprocessor/cat.hpp>
|
||||
|
||||
#include "query/exceptions.hpp"
|
||||
#include "query/frontend/parsing.hpp"
|
||||
#include "query/interpret/awesome_memgraph_functions.hpp"
|
||||
@@ -503,23 +500,19 @@ antlrcpp::Any CypherMainVisitor::visitCreateStream(MemgraphCypher::CreateStreamC
|
||||
return stream_query;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::vector<std::string> TopicNamesFromSymbols(
|
||||
antlr4::tree::ParseTreeVisitor &visitor,
|
||||
const std::vector<MemgraphCypher::SymbolicNameWithDotsAndMinusContext *> &topic_name_symbols) {
|
||||
MG_ASSERT(!topic_name_symbols.empty());
|
||||
std::vector<std::string> topic_names;
|
||||
topic_names.reserve(topic_name_symbols.size());
|
||||
topic_names.reserve(topic_names.size());
|
||||
std::transform(topic_name_symbols.begin(), topic_name_symbols.end(), std::back_inserter(topic_names),
|
||||
[&visitor](auto *topic_name) { return JoinSymbolicNamesWithDotsAndMinus(visitor, *topic_name); });
|
||||
return topic_names;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
concept EnumUint8 = std::is_enum_v<T> && std::same_as<uint8_t, std::underlying_type_t<T>>;
|
||||
|
||||
template <bool required, typename... ValueTypes>
|
||||
void MapConfig(auto &memory, const EnumUint8 auto &enum_key, auto &destination) {
|
||||
void MapConfig(auto &memory, const auto &enum_key, auto &destination) {
|
||||
const auto key = static_cast<uint8_t>(enum_key);
|
||||
if (!memory.contains(key)) {
|
||||
if constexpr (required) {
|
||||
@@ -539,11 +532,13 @@ void MapConfig(auto &memory, const EnumUint8 auto &enum_key, auto &destination)
|
||||
}
|
||||
},
|
||||
std::move(memory[key]));
|
||||
memory.erase(key);
|
||||
}
|
||||
|
||||
enum class CommonStreamConfigKey : uint8_t { TRANSFORM, BATCH_INTERVAL, BATCH_SIZE, END };
|
||||
|
||||
constexpr std::array all_common_stream_config_keys{
|
||||
CommonStreamConfigKey::TRANSFORM, CommonStreamConfigKey::BATCH_INTERVAL, CommonStreamConfigKey::BATCH_SIZE};
|
||||
|
||||
std::string_view ToString(const CommonStreamConfigKey key) {
|
||||
switch (key) {
|
||||
case CommonStreamConfigKey::TRANSFORM:
|
||||
@@ -557,15 +552,20 @@ std::string_view ToString(const CommonStreamConfigKey key) {
|
||||
}
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-macro-usage)
|
||||
#define CONCAT_HELPER(a, b) a##b
|
||||
#define CONCAT(a, b) CONCAT_HELPER(a, b)
|
||||
|
||||
#define GENERATE_STREAM_CONFIG_KEY_ENUM(stream, first_config, ...) \
|
||||
enum class BOOST_PP_CAT(stream, ConfigKey) : uint8_t { \
|
||||
enum class CONCAT(stream, ConfigKey) : uint8_t { \
|
||||
first_config = static_cast<uint8_t>(CommonStreamConfigKey::END), \
|
||||
__VA_ARGS__ \
|
||||
};
|
||||
|
||||
GENERATE_STREAM_CONFIG_KEY_ENUM(Kafka, TOPICS, CONSUMER_GROUP, BOOTSTRAP_SERVERS);
|
||||
|
||||
constexpr std::array all_kafka_config_keys{KafkaConfigKey::TOPICS, KafkaConfigKey::CONSUMER_GROUP,
|
||||
KafkaConfigKey::BOOTSTRAP_SERVERS};
|
||||
|
||||
std::string_view ToString(const KafkaConfigKey key) {
|
||||
switch (key) {
|
||||
case KafkaConfigKey::TOPICS:
|
||||
@@ -578,11 +578,22 @@ std::string_view ToString(const KafkaConfigKey key) {
|
||||
}
|
||||
|
||||
void MapCommonStreamConfigs(auto &memory, StreamQuery &stream_query) {
|
||||
MapConfig<true, std::string>(memory, CommonStreamConfigKey::TRANSFORM, stream_query.transform_name_);
|
||||
MapConfig<false, Expression *>(memory, CommonStreamConfigKey::BATCH_INTERVAL, stream_query.batch_interval_);
|
||||
MapConfig<false, Expression *>(memory, CommonStreamConfigKey::BATCH_SIZE, stream_query.batch_size_);
|
||||
for (const auto key : all_common_stream_config_keys) {
|
||||
switch (key) {
|
||||
case CommonStreamConfigKey::TRANSFORM:
|
||||
MapConfig<true, std::string>(memory, CommonStreamConfigKey::TRANSFORM, stream_query.transform_name_);
|
||||
break;
|
||||
case CommonStreamConfigKey::BATCH_INTERVAL:
|
||||
MapConfig<false, Expression *>(memory, CommonStreamConfigKey::BATCH_INTERVAL, stream_query.batch_interval_);
|
||||
break;
|
||||
case CommonStreamConfigKey::BATCH_SIZE:
|
||||
MapConfig<false, Expression *>(memory, CommonStreamConfigKey::BATCH_SIZE, stream_query.batch_size_);
|
||||
break;
|
||||
case CommonStreamConfigKey::END:
|
||||
LOG_FATAL("Invalid config key used");
|
||||
}
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
antlrcpp::Any CypherMainVisitor::visitKafkaCreateStream(MemgraphCypher::KafkaCreateStreamContext *ctx) {
|
||||
auto *stream_query = storage_->Create<StreamQuery>();
|
||||
@@ -594,37 +605,32 @@ antlrcpp::Any CypherMainVisitor::visitKafkaCreateStream(MemgraphCypher::KafkaCre
|
||||
create_config_ctx->accept(this);
|
||||
}
|
||||
|
||||
MapConfig<true, std::vector<std::string>, Expression *>(memory_, KafkaConfigKey::TOPICS, stream_query->topic_names_);
|
||||
MapConfig<false, std::string>(memory_, KafkaConfigKey::CONSUMER_GROUP, stream_query->consumer_group_);
|
||||
MapConfig<false, Expression *>(memory_, KafkaConfigKey::BOOTSTRAP_SERVERS, stream_query->bootstrap_servers_);
|
||||
for (const auto key : all_kafka_config_keys) {
|
||||
switch (key) {
|
||||
case KafkaConfigKey::TOPICS:
|
||||
MapConfig<true, std::vector<std::string>>(memory_, KafkaConfigKey::TOPICS, stream_query->topic_names_);
|
||||
break;
|
||||
case KafkaConfigKey::CONSUMER_GROUP:
|
||||
MapConfig<false, std::string>(memory_, KafkaConfigKey::CONSUMER_GROUP, stream_query->consumer_group_);
|
||||
break;
|
||||
case KafkaConfigKey::BOOTSTRAP_SERVERS:
|
||||
MapConfig<false, Expression *>(memory_, KafkaConfigKey::BOOTSTRAP_SERVERS, stream_query->bootstrap_servers_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MapCommonStreamConfigs(memory_, *stream_query);
|
||||
|
||||
return stream_query;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void ThrowIfExists(const auto &map, const EnumUint8 auto &enum_key) {
|
||||
void ThrowIfExists(auto &map, const auto &enum_key) {
|
||||
const auto key = static_cast<uint8_t>(enum_key);
|
||||
if (map.contains(key)) {
|
||||
throw SemanticException("{} defined multiple times in the query", ToString(enum_key));
|
||||
}
|
||||
}
|
||||
|
||||
void GetTopicNames(auto &destination, MemgraphCypher::TopicNamesContext *topic_names_ctx,
|
||||
antlr4::tree::ParseTreeVisitor &visitor) {
|
||||
MG_ASSERT(topic_names_ctx != nullptr);
|
||||
if (auto *symbolic_topic_names_ctx = topic_names_ctx->symbolicTopicNames()) {
|
||||
destination = TopicNamesFromSymbols(visitor, symbolic_topic_names_ctx->symbolicNameWithDotsAndMinus());
|
||||
} else {
|
||||
if (!topic_names_ctx->literal()->StringLiteral()) {
|
||||
throw SemanticException("Topic names should be defined as a string literal or as symbolic names");
|
||||
}
|
||||
destination = topic_names_ctx->accept(&visitor).as<Expression *>();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
antlrcpp::Any CypherMainVisitor::visitKafkaCreateStreamConfig(MemgraphCypher::KafkaCreateStreamConfigContext *ctx) {
|
||||
if (ctx->commonCreateStreamConfig()) {
|
||||
return ctx->commonCreateStreamConfig()->accept(this);
|
||||
@@ -632,8 +638,10 @@ antlrcpp::Any CypherMainVisitor::visitKafkaCreateStreamConfig(MemgraphCypher::Ka
|
||||
|
||||
if (ctx->TOPICS()) {
|
||||
ThrowIfExists(memory_, KafkaConfigKey::TOPICS);
|
||||
const auto topics_key = static_cast<uint8_t>(KafkaConfigKey::TOPICS);
|
||||
GetTopicNames(memory_[topics_key], ctx->topicNames(), *this);
|
||||
auto *topic_names_ctx = ctx->topicNames();
|
||||
MG_ASSERT(topic_names_ctx != nullptr);
|
||||
const auto topic_key = static_cast<uint8_t>(KafkaConfigKey::TOPICS);
|
||||
memory_[topic_key] = TopicNamesFromSymbols(*this, topic_names_ctx->symbolicNameWithDotsAndMinus());
|
||||
return {};
|
||||
}
|
||||
|
||||
@@ -653,11 +661,12 @@ antlrcpp::Any CypherMainVisitor::visitKafkaCreateStreamConfig(MemgraphCypher::Ka
|
||||
const auto bootstrap_servers_key = static_cast<uint8_t>(KafkaConfigKey::BOOTSTRAP_SERVERS);
|
||||
memory_[bootstrap_servers_key] = ctx->bootstrapServers->accept(this).as<Expression *>();
|
||||
return {};
|
||||
}
|
||||
} // namespace query::frontend
|
||||
|
||||
namespace {
|
||||
GENERATE_STREAM_CONFIG_KEY_ENUM(Pulsar, TOPICS, SERVICE_URL);
|
||||
|
||||
constexpr std::array all_pulsar_config_keys{PulsarConfigKey::TOPICS, PulsarConfigKey::SERVICE_URL};
|
||||
|
||||
std::string_view ToString(const PulsarConfigKey key) {
|
||||
switch (key) {
|
||||
case PulsarConfigKey::TOPICS:
|
||||
@@ -666,7 +675,6 @@ std::string_view ToString(const PulsarConfigKey key) {
|
||||
return "SERVICE_URL";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
antlrcpp::Any CypherMainVisitor::visitPulsarCreateStream(MemgraphCypher::PulsarCreateStreamContext *ctx) {
|
||||
auto *stream_query = storage_->Create<StreamQuery>();
|
||||
@@ -678,8 +686,17 @@ antlrcpp::Any CypherMainVisitor::visitPulsarCreateStream(MemgraphCypher::PulsarC
|
||||
create_config_ctx->accept(this);
|
||||
}
|
||||
|
||||
MapConfig<true, std::vector<std::string>, Expression *>(memory_, PulsarConfigKey::TOPICS, stream_query->topic_names_);
|
||||
MapConfig<false, Expression *>(memory_, PulsarConfigKey::SERVICE_URL, stream_query->service_url_);
|
||||
for (const auto key : all_pulsar_config_keys) {
|
||||
switch (key) {
|
||||
case PulsarConfigKey::TOPICS:
|
||||
MapConfig<true, std::vector<std::string>, Expression *>(memory_, PulsarConfigKey::TOPICS,
|
||||
stream_query->topic_names_);
|
||||
break;
|
||||
case PulsarConfigKey::SERVICE_URL:
|
||||
MapConfig<true, Expression *>(memory_, PulsarConfigKey::SERVICE_URL, stream_query->service_url_);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
MapCommonStreamConfigs(memory_, *stream_query);
|
||||
|
||||
@@ -693,15 +710,24 @@ antlrcpp::Any CypherMainVisitor::visitPulsarCreateStreamConfig(MemgraphCypher::P
|
||||
|
||||
if (ctx->TOPICS()) {
|
||||
ThrowIfExists(memory_, PulsarConfigKey::TOPICS);
|
||||
auto *pulsar_topic_names_ctx = ctx->pulsarTopicNames();
|
||||
MG_ASSERT(pulsar_topic_names_ctx != nullptr);
|
||||
const auto topics_key = static_cast<uint8_t>(PulsarConfigKey::TOPICS);
|
||||
GetTopicNames(memory_[topics_key], ctx->topicNames(), *this);
|
||||
if (auto *topic_names_ctx = pulsar_topic_names_ctx->topicNames()) {
|
||||
memory_[topics_key] = TopicNamesFromSymbols(*this, topic_names_ctx->symbolicNameWithDotsAndMinus());
|
||||
} else {
|
||||
if (!pulsar_topic_names_ctx->literal()->StringLiteral()) {
|
||||
throw SemanticException("Topic names should be defined in a string");
|
||||
}
|
||||
memory_[topics_key] = pulsar_topic_names_ctx->accept(this).as<Expression *>();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
MG_ASSERT(ctx->SERVICE_URL());
|
||||
ThrowIfExists(memory_, PulsarConfigKey::SERVICE_URL);
|
||||
if (!ctx->serviceUrl->StringLiteral()) {
|
||||
throw SemanticException("Service URL must be a string!");
|
||||
throw SemanticException("Service url should be a string!");
|
||||
}
|
||||
const auto service_url_key = static_cast<uint8_t>(PulsarConfigKey::SERVICE_URL);
|
||||
memory_[service_url_key] = ctx->serviceUrl->accept(this).as<Expression *>();
|
||||
@@ -719,7 +745,7 @@ antlrcpp::Any CypherMainVisitor::visitCommonCreateStreamConfig(MemgraphCypher::C
|
||||
if (ctx->BATCH_INTERVAL()) {
|
||||
ThrowIfExists(memory_, CommonStreamConfigKey::BATCH_INTERVAL);
|
||||
if (!ctx->batchInterval->numberLiteral() || !ctx->batchInterval->numberLiteral()->integerLiteral()) {
|
||||
throw SemanticException("Batch interval must be an integer literal!");
|
||||
throw SemanticException("Batch interval should be an integer literal!");
|
||||
}
|
||||
const auto batch_interval_key = static_cast<uint8_t>(CommonStreamConfigKey::BATCH_INTERVAL);
|
||||
memory_[batch_interval_key] = ctx->batchInterval->accept(this).as<Expression *>();
|
||||
@@ -729,7 +755,7 @@ antlrcpp::Any CypherMainVisitor::visitCommonCreateStreamConfig(MemgraphCypher::C
|
||||
MG_ASSERT(ctx->BATCH_SIZE());
|
||||
ThrowIfExists(memory_, CommonStreamConfigKey::BATCH_SIZE);
|
||||
if (!ctx->batchSize->numberLiteral() || !ctx->batchSize->numberLiteral()->integerLiteral()) {
|
||||
throw SemanticException("Batch size must be an integer literal!");
|
||||
throw SemanticException("Batch size should be an integer literal!");
|
||||
}
|
||||
const auto batch_size_key = static_cast<uint8_t>(CommonStreamConfigKey::BATCH_SIZE);
|
||||
memory_[batch_size_key] = ctx->batchSize->accept(this).as<Expression *>();
|
||||
|
||||
@@ -295,9 +295,7 @@ symbolicNameWithMinus : symbolicName ( MINUS symbolicName )* ;
|
||||
|
||||
symbolicNameWithDotsAndMinus: symbolicNameWithMinus ( DOT symbolicNameWithMinus )* ;
|
||||
|
||||
symbolicTopicNames : symbolicNameWithDotsAndMinus ( COMMA symbolicNameWithDotsAndMinus )* ;
|
||||
|
||||
topicNames : symbolicTopicNames | literal ;
|
||||
topicNames : symbolicNameWithDotsAndMinus ( COMMA symbolicNameWithDotsAndMinus )* ;
|
||||
|
||||
commonCreateStreamConfig : TRANSFORM transformationName=procedureName
|
||||
| BATCH_INTERVAL batchInterval=literal
|
||||
@@ -314,8 +312,9 @@ kafkaCreateStreamConfig : TOPICS topicNames
|
||||
|
||||
kafkaCreateStream : CREATE KAFKA STREAM streamName ( kafkaCreateStreamConfig ) * ;
|
||||
|
||||
pulsarTopicNames : topicNames | literal ;
|
||||
|
||||
pulsarCreateStreamConfig : TOPICS topicNames
|
||||
pulsarCreateStreamConfig : TOPICS pulsarTopicNames
|
||||
| SERVICE_URL serviceUrl=literal
|
||||
| commonCreateStreamConfig
|
||||
;
|
||||
|
||||
@@ -10,14 +10,9 @@
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "query/interpreter.hpp"
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
|
||||
@@ -36,7 +31,6 @@
|
||||
#include "query/frontend/semantic/required_privileges.hpp"
|
||||
#include "query/frontend/semantic/symbol_generator.hpp"
|
||||
#include "query/interpret/eval.hpp"
|
||||
#include "query/metadata.hpp"
|
||||
#include "query/plan/planner.hpp"
|
||||
#include "query/plan/profile.hpp"
|
||||
#include "query/plan/vertex_count_cache.hpp"
|
||||
@@ -58,7 +52,7 @@
|
||||
#include "utils/settings.hpp"
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/tsc.hpp"
|
||||
#include "utils/variant_helpers.hpp"
|
||||
#include "utils/variant.hpp"
|
||||
|
||||
namespace EventCounter {
|
||||
extern Event ReadQuery;
|
||||
@@ -413,8 +407,7 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
}
|
||||
|
||||
Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters ¶meters,
|
||||
InterpreterContext *interpreter_context, DbAccessor *db_accessor,
|
||||
std::vector<Notification> *notifications) {
|
||||
InterpreterContext *interpreter_context, DbAccessor *db_accessor) {
|
||||
Frame frame(0);
|
||||
SymbolTable symbol_table;
|
||||
EvaluationContext evaluation_context;
|
||||
@@ -432,19 +425,11 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
if (port.IsInt()) {
|
||||
maybe_port = port.ValueInt();
|
||||
}
|
||||
if (maybe_port == 7687 && repl_query->role_ == ReplicationQuery::ReplicationRole::REPLICA) {
|
||||
notifications->emplace_back(SeverityLevel::WARNING, NotificationCode::REPLICA_PORT_WARNING,
|
||||
"Be careful the replication port must be different from the memgraph port!");
|
||||
}
|
||||
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, role = repl_query->role_,
|
||||
maybe_port]() mutable {
|
||||
handler.SetReplicationRole(role, maybe_port);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
notifications->emplace_back(
|
||||
SeverityLevel::INFO, NotificationCode::SET_REPLICA,
|
||||
fmt::format("Replica role set to {}.",
|
||||
repl_query->role_ == ReplicationQuery::ReplicationRole::MAIN ? "MAIN" : "REPLICA"));
|
||||
return callback;
|
||||
}
|
||||
case ReplicationQuery::Action::SHOW_REPLICATION_ROLE: {
|
||||
@@ -478,8 +463,6 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
handler.RegisterReplica(name, std::string(socket_address.ValueString()), sync_mode, maybe_timeout);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::REGISTER_REPLICA,
|
||||
fmt::format("Replica {} is registered.", repl_query->replica_name_));
|
||||
return callback;
|
||||
}
|
||||
case ReplicationQuery::Action::DROP_REPLICA: {
|
||||
@@ -488,8 +471,6 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
handler.DropReplica(name);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::DROP_REPLICA,
|
||||
fmt::format("Replica {} is dropped.", repl_query->replica_name_));
|
||||
return callback;
|
||||
}
|
||||
case ReplicationQuery::Action::SHOW_REPLICAS: {
|
||||
@@ -532,12 +513,10 @@ std::optional<std::string> StringPointerToOptional(const std::string *str) {
|
||||
return str == nullptr ? std::nullopt : std::make_optional(*str);
|
||||
}
|
||||
|
||||
stream::CommonStreamInfo GetCommonStreamInfo(StreamQuery *stream_query, ExpressionEvaluator &evaluator) {
|
||||
return {
|
||||
.batch_interval = GetOptionalValue<std::chrono::milliseconds>(stream_query->batch_interval_, evaluator)
|
||||
.value_or(stream::kDefaultBatchInterval),
|
||||
.batch_size = GetOptionalValue<int64_t>(stream_query->batch_size_, evaluator).value_or(stream::kDefaultBatchSize),
|
||||
.transformation_name = stream_query->transform_name_};
|
||||
CommonStreamInfo GetCommonStreamInfo(StreamQuery *stream_query, ExpressionEvaluator &evaluator) {
|
||||
return {.batch_interval = GetOptionalValue<std::chrono::milliseconds>(stream_query->batch_interval_, evaluator),
|
||||
.batch_size = GetOptionalValue<int64_t>(stream_query->batch_size_, evaluator),
|
||||
.transformation_name = stream_query->transform_name_};
|
||||
}
|
||||
|
||||
std::vector<std::string> EvaluateTopicNames(ExpressionEvaluator &evaluator,
|
||||
@@ -571,12 +550,12 @@ Callback::CallbackFunction GetKafkaCreateCallback(StreamQuery *stream_query, Exp
|
||||
std::string bootstrap = bootstrap_servers
|
||||
? std::move(*bootstrap_servers)
|
||||
: std::string{interpreter_context->config.default_kafka_bootstrap_servers};
|
||||
interpreter_context->streams.Create<query::stream::KafkaStream>(stream_name,
|
||||
{.common_info = std::move(common_stream_info),
|
||||
.topics = std::move(topic_names),
|
||||
.consumer_group = std::move(consumer_group),
|
||||
.bootstrap_servers = std::move(bootstrap)},
|
||||
std::move(owner));
|
||||
interpreter_context->streams.Create<query::KafkaStream>(stream_name,
|
||||
{.common_info = std::move(common_stream_info),
|
||||
.topics = std::move(topic_names),
|
||||
.consumer_group = std::move(consumer_group),
|
||||
.bootstrap_servers = std::move(bootstrap)},
|
||||
std::move(owner));
|
||||
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
@@ -594,12 +573,11 @@ Callback::CallbackFunction GetPulsarCreateCallback(StreamQuery *stream_query, Ex
|
||||
topic_names = EvaluateTopicNames(evaluator, stream_query->topic_names_),
|
||||
common_stream_info = std::move(common_stream_info), service_url = std::move(service_url),
|
||||
owner = StringPointerToOptional(username)]() mutable {
|
||||
std::string url =
|
||||
service_url ? std::move(*service_url) : std::string{interpreter_context->config.default_pulsar_service_url};
|
||||
interpreter_context->streams.Create<query::stream::PulsarStream>(
|
||||
stream_name,
|
||||
{.common_info = std::move(common_stream_info), .topics = std::move(topic_names), .service_url = std::move(url)},
|
||||
std::move(owner));
|
||||
interpreter_context->streams.Create<query::PulsarStream>(stream_name,
|
||||
{.common_info = std::move(common_stream_info),
|
||||
.topics = std::move(topic_names),
|
||||
.service_url = std::move(*service_url)},
|
||||
std::move(owner));
|
||||
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
@@ -607,7 +585,7 @@ Callback::CallbackFunction GetPulsarCreateCallback(StreamQuery *stream_query, Ex
|
||||
|
||||
Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶meters,
|
||||
InterpreterContext *interpreter_context, DbAccessor *db_accessor,
|
||||
const std::string *username, std::vector<Notification> *notifications) {
|
||||
const std::string *username) {
|
||||
Frame frame(0);
|
||||
SymbolTable symbol_table;
|
||||
EvaluationContext evaluation_context;
|
||||
@@ -629,8 +607,7 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
callback.fn = GetPulsarCreateCallback(stream_query, evaluator, interpreter_context, username);
|
||||
break;
|
||||
}
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::CREATE_STREAM,
|
||||
fmt::format("Created stream {}.", stream_query->stream_name_));
|
||||
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::START_STREAM: {
|
||||
@@ -638,8 +615,6 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
interpreter_context->streams.Start(stream_name);
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::START_STREAM,
|
||||
fmt::format("Started stream {}.", stream_query->stream_name_));
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::START_ALL_STREAMS: {
|
||||
@@ -647,7 +622,6 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
interpreter_context->streams.StartAll();
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::START_ALL_STREAMS, "Started all streams.");
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::STOP_STREAM: {
|
||||
@@ -655,8 +629,6 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
interpreter_context->streams.Stop(stream_name);
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::STOP_STREAM,
|
||||
fmt::format("Stopped stream {}.", stream_query->stream_name_));
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::STOP_ALL_STREAMS: {
|
||||
@@ -664,7 +636,6 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
interpreter_context->streams.StopAll();
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::STOP_ALL_STREAMS, "Stopped all streams.");
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::DROP_STREAM: {
|
||||
@@ -672,27 +643,32 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
interpreter_context->streams.Drop(stream_name);
|
||||
return std::vector<std::vector<TypedValue>>{};
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::DROP_STREAM,
|
||||
fmt::format("Dropped stream {}.", stream_query->stream_name_));
|
||||
return callback;
|
||||
}
|
||||
case StreamQuery::Action::SHOW_STREAMS: {
|
||||
callback.header = {"name", "type", "batch_interval", "batch_size", "transformation_name", "owner", "is running"};
|
||||
callback.header = {"name", "batch_interval", "batch_size", "transformation_name", "owner", "is running"};
|
||||
callback.fn = [interpreter_context]() {
|
||||
auto streams_status = interpreter_context->streams.GetStreamInfo();
|
||||
std::vector<std::vector<TypedValue>> results;
|
||||
results.reserve(streams_status.size());
|
||||
auto stream_info_as_typed_stream_info_emplace_in = [](auto &typed_status, const auto &stream_info) {
|
||||
typed_status.emplace_back(stream_info.batch_interval.count());
|
||||
typed_status.emplace_back(stream_info.batch_size);
|
||||
if (stream_info.batch_interval.has_value()) {
|
||||
typed_status.emplace_back(stream_info.batch_interval->count());
|
||||
} else {
|
||||
typed_status.emplace_back();
|
||||
}
|
||||
if (stream_info.batch_size.has_value()) {
|
||||
typed_status.emplace_back(*stream_info.batch_size);
|
||||
} else {
|
||||
typed_status.emplace_back();
|
||||
}
|
||||
typed_status.emplace_back(stream_info.transformation_name);
|
||||
};
|
||||
|
||||
for (const auto &status : streams_status) {
|
||||
std::vector<TypedValue> typed_status;
|
||||
typed_status.reserve(7);
|
||||
typed_status.reserve(8);
|
||||
typed_status.emplace_back(status.name);
|
||||
typed_status.emplace_back(StreamSourceTypeToString(status.type));
|
||||
stream_info_as_typed_stream_info_emplace_in(typed_status, status.info);
|
||||
if (status.owner.has_value()) {
|
||||
typed_status.emplace_back(*status.owner);
|
||||
@@ -714,8 +690,6 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters ¶mete
|
||||
batch_limit = GetOptionalValue<int64_t>(stream_query->batch_limit_, evaluator)]() mutable {
|
||||
return interpreter_context->streams.Check(stream_name, timeout, batch_limit);
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::CHECK_STREAM,
|
||||
fmt::format("Checked stream {}.", stream_query->stream_name_));
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
@@ -946,19 +920,8 @@ std::optional<plan::ProfilingStatsWithTotalTime> PullPlan::Pull(AnyStream *strea
|
||||
if (has_unsent_results_) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
summary->insert_or_assign("plan_execution_time", execution_time_.count());
|
||||
// We are finished with pulling all the data, therefore we can send any
|
||||
// metadata about the results i.e. notifications and statistics
|
||||
const bool is_any_counter_set =
|
||||
std::any_of(ctx_.execution_stats.counters.begin(), ctx_.execution_stats.counters.end(),
|
||||
[](const auto &counter) { return counter > 0; });
|
||||
if (is_any_counter_set) {
|
||||
std::map<std::string, TypedValue> stats;
|
||||
for (size_t i = 0; i < ctx_.execution_stats.counters.size(); ++i) {
|
||||
stats.emplace(ExecutionStatsKeyToString(ExecutionStats::Key(i)), ctx_.execution_stats.counters[i]);
|
||||
}
|
||||
summary->insert_or_assign("stats", std::move(stats));
|
||||
}
|
||||
cursor_->Shutdown();
|
||||
ctx_.profile_execution_time = execution_time_;
|
||||
return GetStatsWithTotalTime(ctx_);
|
||||
@@ -1039,7 +1002,7 @@ PreparedQuery Interpreter::PrepareTransactionQuery(std::string_view query_upper)
|
||||
|
||||
PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string, TypedValue> *summary,
|
||||
InterpreterContext *interpreter_context, DbAccessor *dba,
|
||||
utils::MemoryResource *execution_memory, std::vector<Notification> *notifications,
|
||||
utils::MemoryResource *execution_memory,
|
||||
TriggerContextCollector *trigger_context_collector = nullptr) {
|
||||
auto *cypher_query = utils::Downcast<CypherQuery>(parsed_query.query);
|
||||
|
||||
@@ -1054,15 +1017,6 @@ PreparedQuery PrepareCypherQuery(ParsedQuery parsed_query, std::map<std::string,
|
||||
spdlog::info("Running query with memory limit of {}", utils::GetReadableSize(*memory_limit));
|
||||
}
|
||||
|
||||
if (const auto &clauses = cypher_query->single_query_->clauses_; std::any_of(
|
||||
clauses.begin(), clauses.end(), [](const auto *clause) { return clause->GetTypeInfo() == LoadCsv::kType; })) {
|
||||
notifications->emplace_back(
|
||||
SeverityLevel::INFO, NotificationCode::LOAD_CSV_TIP,
|
||||
"It's important to note that the parser parses the values as strings. It's up to the user to "
|
||||
"convert the parsed row values to the appropriate type. This can be done using the built-in "
|
||||
"conversion functions such as ToInteger, ToFloat, ToBoolean etc.");
|
||||
}
|
||||
|
||||
auto plan = CypherQueryToPlan(parsed_query.stripped_query.hash(), std::move(parsed_query.ast_storage), cypher_query,
|
||||
parsed_query.parameters,
|
||||
parsed_query.is_cacheable ? &interpreter_context->plan_cache : nullptr, dba);
|
||||
@@ -1083,6 +1037,7 @@ 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, trigger_context_collector, memory_limit);
|
||||
return PreparedQuery{std::move(header), std::move(parsed_query.required_privileges),
|
||||
@@ -1240,13 +1195,14 @@ PreparedQuery PrepareDumpQuery(ParsedQuery parsed_query, std::map<std::string, T
|
||||
}
|
||||
|
||||
PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
|
||||
std::vector<Notification> *notifications, InterpreterContext *interpreter_context) {
|
||||
std::map<std::string, TypedValue> *summary, InterpreterContext *interpreter_context,
|
||||
utils::MemoryResource *execution_memory) {
|
||||
if (in_explicit_transaction) {
|
||||
throw IndexInMulticommandTxException();
|
||||
}
|
||||
|
||||
auto *index_query = utils::Downcast<IndexQuery>(parsed_query.query);
|
||||
std::function<void(Notification &)> handler;
|
||||
std::function<void()> handler;
|
||||
|
||||
// Creating an index influences computed plan costs.
|
||||
auto invalidate_plan_cache = [plan_cache = &interpreter_context->plan_cache] {
|
||||
@@ -1257,45 +1213,26 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
};
|
||||
|
||||
auto label = interpreter_context->db->NameToLabel(index_query->label_.name);
|
||||
|
||||
std::vector<storage::PropertyId> properties;
|
||||
std::vector<std::string> properties_string;
|
||||
properties.reserve(index_query->properties_.size());
|
||||
properties_string.reserve(index_query->properties_.size());
|
||||
for (const auto &prop : index_query->properties_) {
|
||||
properties.push_back(interpreter_context->db->NameToProperty(prop.name));
|
||||
properties_string.push_back(prop.name);
|
||||
}
|
||||
auto properties_stringified = utils::Join(properties_string, ", ");
|
||||
|
||||
if (properties.size() > 1) {
|
||||
throw utils::NotYetImplemented("index on multiple properties");
|
||||
}
|
||||
|
||||
Notification index_notification(SeverityLevel::INFO);
|
||||
switch (index_query->action_) {
|
||||
case IndexQuery::Action::CREATE: {
|
||||
index_notification.code = NotificationCode::CREATE_INDEX;
|
||||
index_notification.title =
|
||||
fmt::format("Created index on label {} on properties {}.", index_query->label_.name, properties_stringified);
|
||||
|
||||
handler = [interpreter_context, label, properties_stringified = std::move(properties_stringified),
|
||||
label_name = index_query->label_.name, properties = std::move(properties),
|
||||
invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
|
||||
handler = [interpreter_context, label, properties = std::move(properties),
|
||||
invalidate_plan_cache = std::move(invalidate_plan_cache)] {
|
||||
if (properties.empty()) {
|
||||
if (!interpreter_context->db->CreateIndex(label)) {
|
||||
index_notification.code = NotificationCode::EXISTANT_INDEX;
|
||||
index_notification.title =
|
||||
fmt::format("Index on label {} on properties {} already exists.", label_name, properties_stringified);
|
||||
}
|
||||
interpreter_context->db->CreateIndex(label);
|
||||
EventCounter::IncrementCounter(EventCounter::LabelIndexCreated);
|
||||
} else {
|
||||
MG_ASSERT(properties.size() == 1U);
|
||||
if (!interpreter_context->db->CreateIndex(label, properties[0])) {
|
||||
index_notification.code = NotificationCode::EXISTANT_INDEX;
|
||||
index_notification.title =
|
||||
fmt::format("Index on label {} on properties {} already exists.", label_name, properties_stringified);
|
||||
}
|
||||
interpreter_context->db->CreateIndex(label, properties[0]);
|
||||
EventCounter::IncrementCounter(EventCounter::LabelPropertyIndexCreated);
|
||||
}
|
||||
invalidate_plan_cache();
|
||||
@@ -1303,25 +1240,13 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
break;
|
||||
}
|
||||
case IndexQuery::Action::DROP: {
|
||||
index_notification.code = NotificationCode::DROP_INDEX;
|
||||
index_notification.title = fmt::format("Dropped index on label {} on properties {}.", index_query->label_.name,
|
||||
utils::Join(properties_string, ", "));
|
||||
handler = [interpreter_context, label, properties_stringified = std::move(properties_stringified),
|
||||
label_name = index_query->label_.name, properties = std::move(properties),
|
||||
invalidate_plan_cache = std::move(invalidate_plan_cache)](Notification &index_notification) {
|
||||
handler = [interpreter_context, label, properties = std::move(properties),
|
||||
invalidate_plan_cache = std::move(invalidate_plan_cache)] {
|
||||
if (properties.empty()) {
|
||||
if (!interpreter_context->db->DropIndex(label)) {
|
||||
index_notification.code = NotificationCode::NONEXISTANT_INDEX;
|
||||
index_notification.title =
|
||||
fmt::format("Index on label {} on properties {} doesn't exist.", label_name, properties_stringified);
|
||||
}
|
||||
interpreter_context->db->DropIndex(label);
|
||||
} else {
|
||||
MG_ASSERT(properties.size() == 1U);
|
||||
if (!interpreter_context->db->DropIndex(label, properties[0])) {
|
||||
index_notification.code = NotificationCode::NONEXISTANT_INDEX;
|
||||
index_notification.title =
|
||||
fmt::format("Index on label {} on properties {} doesn't exist.", label_name, properties_stringified);
|
||||
}
|
||||
interpreter_context->db->DropIndex(label, properties[0]);
|
||||
}
|
||||
invalidate_plan_cache();
|
||||
};
|
||||
@@ -1329,16 +1254,13 @@ PreparedQuery PrepareIndexQuery(ParsedQuery parsed_query, bool in_explicit_trans
|
||||
}
|
||||
}
|
||||
|
||||
return PreparedQuery{
|
||||
{},
|
||||
std::move(parsed_query.required_privileges),
|
||||
[handler = std::move(handler), notifications, index_notification = std::move(index_notification)](
|
||||
AnyStream * /*stream*/, std::optional<int> /*unused*/) mutable {
|
||||
handler(index_notification);
|
||||
notifications->push_back(index_notification);
|
||||
return QueryHandlerResult::NOTHING;
|
||||
},
|
||||
RWType::W};
|
||||
return PreparedQuery{{},
|
||||
std::move(parsed_query.required_privileges),
|
||||
[handler = std::move(handler)](AnyStream *stream, std::optional<int>) {
|
||||
handler();
|
||||
return QueryHandlerResult::NOTHING;
|
||||
},
|
||||
RWType::W};
|
||||
}
|
||||
|
||||
PreparedQuery PrepareAuthQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
|
||||
@@ -1378,15 +1300,13 @@ PreparedQuery PrepareAuthQuery(ParsedQuery parsed_query, bool in_explicit_transa
|
||||
}
|
||||
|
||||
PreparedQuery PrepareReplicationQuery(ParsedQuery parsed_query, const bool in_explicit_transaction,
|
||||
std::vector<Notification> *notifications, InterpreterContext *interpreter_context,
|
||||
DbAccessor *dba) {
|
||||
InterpreterContext *interpreter_context, DbAccessor *dba) {
|
||||
if (in_explicit_transaction) {
|
||||
throw ReplicationModificationInMulticommandTxException();
|
||||
}
|
||||
|
||||
auto *replication_query = utils::Downcast<ReplicationQuery>(parsed_query.query);
|
||||
auto callback =
|
||||
HandleReplicationQuery(replication_query, parsed_query.parameters, interpreter_context, dba, notifications);
|
||||
auto callback = HandleReplicationQuery(replication_query, parsed_query.parameters, interpreter_context, dba);
|
||||
|
||||
return PreparedQuery{callback.header, std::move(parsed_query.required_privileges),
|
||||
[callback_fn = std::move(callback.fn), pull_plan = std::shared_ptr<PullPlanVector>{nullptr}](
|
||||
@@ -1535,8 +1455,8 @@ Callback ShowTriggers(InterpreterContext *interpreter_context) {
|
||||
}
|
||||
|
||||
PreparedQuery PrepareTriggerQuery(ParsedQuery parsed_query, const bool in_explicit_transaction,
|
||||
std::vector<Notification> *notifications, InterpreterContext *interpreter_context,
|
||||
DbAccessor *dba, const std::map<std::string, storage::PropertyValue> &user_parameters,
|
||||
InterpreterContext *interpreter_context, DbAccessor *dba,
|
||||
const std::map<std::string, storage::PropertyValue> &user_parameters,
|
||||
const std::string *username) {
|
||||
if (in_explicit_transaction) {
|
||||
throw TriggerModificationInMulticommandTxException();
|
||||
@@ -1545,36 +1465,27 @@ PreparedQuery PrepareTriggerQuery(ParsedQuery parsed_query, const bool in_explic
|
||||
auto *trigger_query = utils::Downcast<TriggerQuery>(parsed_query.query);
|
||||
MG_ASSERT(trigger_query);
|
||||
|
||||
std::optional<Notification> trigger_notification;
|
||||
auto callback = std::invoke([trigger_query, interpreter_context, dba, &user_parameters,
|
||||
owner = StringPointerToOptional(username), &trigger_notification]() mutable {
|
||||
auto callback = [trigger_query, interpreter_context, dba, &user_parameters,
|
||||
owner = StringPointerToOptional(username)]() mutable {
|
||||
switch (trigger_query->action_) {
|
||||
case TriggerQuery::Action::CREATE_TRIGGER:
|
||||
trigger_notification.emplace(SeverityLevel::INFO, NotificationCode::CREATE_TRIGGER,
|
||||
fmt::format("Created trigger {}.", trigger_query->trigger_name_));
|
||||
EventCounter::IncrementCounter(EventCounter::TriggersCreated);
|
||||
return CreateTrigger(trigger_query, user_parameters, interpreter_context, dba, std::move(owner));
|
||||
case TriggerQuery::Action::DROP_TRIGGER:
|
||||
trigger_notification.emplace(SeverityLevel::INFO, NotificationCode::DROP_TRIGGER,
|
||||
fmt::format("Dropped trigger {}.", trigger_query->trigger_name_));
|
||||
return DropTrigger(trigger_query, interpreter_context);
|
||||
case TriggerQuery::Action::SHOW_TRIGGERS:
|
||||
return ShowTriggers(interpreter_context);
|
||||
}
|
||||
});
|
||||
}();
|
||||
|
||||
return PreparedQuery{std::move(callback.header), std::move(parsed_query.required_privileges),
|
||||
[callback_fn = std::move(callback.fn), pull_plan = std::shared_ptr<PullPlanVector>{nullptr},
|
||||
trigger_notification = std::move(trigger_notification), notifications](
|
||||
[callback_fn = std::move(callback.fn), pull_plan = std::shared_ptr<PullPlanVector>{nullptr}](
|
||||
AnyStream *stream, std::optional<int> n) mutable -> std::optional<QueryHandlerResult> {
|
||||
if (UNLIKELY(!pull_plan)) {
|
||||
pull_plan = std::make_shared<PullPlanVector>(callback_fn());
|
||||
}
|
||||
|
||||
if (pull_plan->Pull(stream, n)) {
|
||||
if (trigger_notification) {
|
||||
notifications->push_back(std::move(*trigger_notification));
|
||||
}
|
||||
return QueryHandlerResult::COMMIT;
|
||||
}
|
||||
return std::nullopt;
|
||||
@@ -1585,9 +1496,8 @@ PreparedQuery PrepareTriggerQuery(ParsedQuery parsed_query, const bool in_explic
|
||||
}
|
||||
|
||||
PreparedQuery PrepareStreamQuery(ParsedQuery parsed_query, const bool in_explicit_transaction,
|
||||
std::vector<Notification> *notifications, InterpreterContext *interpreter_context,
|
||||
DbAccessor *dba,
|
||||
const std::map<std::string, storage::PropertyValue> & /*user_parameters*/,
|
||||
InterpreterContext *interpreter_context, DbAccessor *dba,
|
||||
const std::map<std::string, storage::PropertyValue> &user_parameters,
|
||||
const std::string *username) {
|
||||
if (in_explicit_transaction) {
|
||||
throw StreamQueryInMulticommandTxException();
|
||||
@@ -1595,8 +1505,7 @@ PreparedQuery PrepareStreamQuery(ParsedQuery parsed_query, const bool in_explici
|
||||
|
||||
auto *stream_query = utils::Downcast<StreamQuery>(parsed_query.query);
|
||||
MG_ASSERT(stream_query);
|
||||
auto callback =
|
||||
HandleStreamQuery(stream_query, parsed_query.parameters, interpreter_context, dba, username, notifications);
|
||||
auto callback = HandleStreamQuery(stream_query, parsed_query.parameters, interpreter_context, dba, username);
|
||||
|
||||
return PreparedQuery{std::move(callback.header), std::move(parsed_query.required_privileges),
|
||||
[callback_fn = std::move(callback.fn), pull_plan = std::shared_ptr<PullPlanVector>{nullptr}](
|
||||
@@ -1796,31 +1705,24 @@ PreparedQuery PrepareInfoQuery(ParsedQuery parsed_query, bool in_explicit_transa
|
||||
}
|
||||
|
||||
PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_transaction,
|
||||
std::vector<Notification> *notifications,
|
||||
InterpreterContext *interpreter_context) {
|
||||
std::map<std::string, TypedValue> *summary,
|
||||
InterpreterContext *interpreter_context, utils::MemoryResource *execution_memory) {
|
||||
if (in_explicit_transaction) {
|
||||
throw ConstraintInMulticommandTxException();
|
||||
}
|
||||
|
||||
auto *constraint_query = utils::Downcast<ConstraintQuery>(parsed_query.query);
|
||||
std::function<void(Notification &)> handler;
|
||||
std::function<void()> handler;
|
||||
|
||||
auto label = interpreter_context->db->NameToLabel(constraint_query->constraint_.label.name);
|
||||
std::vector<storage::PropertyId> properties;
|
||||
std::vector<std::string> properties_string;
|
||||
properties.reserve(constraint_query->constraint_.properties.size());
|
||||
properties_string.reserve(constraint_query->constraint_.properties.size());
|
||||
for (const auto &prop : constraint_query->constraint_.properties) {
|
||||
properties.push_back(interpreter_context->db->NameToProperty(prop.name));
|
||||
properties_string.push_back(prop.name);
|
||||
}
|
||||
auto properties_stringified = utils::Join(properties_string, ", ");
|
||||
|
||||
Notification constraint_notification(SeverityLevel::INFO);
|
||||
switch (constraint_query->action_type_) {
|
||||
case ConstraintQuery::ActionType::CREATE: {
|
||||
constraint_notification.code = NotificationCode::CREATE_CONSTRAINT;
|
||||
|
||||
switch (constraint_query->constraint_.type) {
|
||||
case Constraint::Type::NODE_KEY:
|
||||
throw utils::NotYetImplemented("Node key constraints");
|
||||
@@ -1828,11 +1730,7 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
if (properties.empty() || properties.size() > 1) {
|
||||
throw SyntaxException("Exactly one property must be used for existence constraints.");
|
||||
}
|
||||
constraint_notification.title = fmt::format("Created EXISTS constraint on label {} on properties {}.",
|
||||
constraint_query->constraint_.label.name, properties_stringified);
|
||||
handler = [interpreter_context, label, label_name = constraint_query->constraint_.label.name,
|
||||
properties_stringified = std::move(properties_stringified),
|
||||
properties = std::move(properties)](Notification &constraint_notification) {
|
||||
handler = [interpreter_context, label, properties = std::move(properties)] {
|
||||
auto res = interpreter_context->db->CreateExistenceConstraint(label, properties[0]);
|
||||
if (res.HasError()) {
|
||||
auto violation = res.GetError();
|
||||
@@ -1844,11 +1742,6 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
"existing node violates it.",
|
||||
label_name, property_name);
|
||||
}
|
||||
if (res.HasValue() && !res.GetValue()) {
|
||||
constraint_notification.code = NotificationCode::EXISTANT_CONSTRAINT;
|
||||
constraint_notification.title = fmt::format(
|
||||
"Constraint EXISTS on label {} on properties {} already exists.", label_name, properties_stringified);
|
||||
}
|
||||
};
|
||||
break;
|
||||
case Constraint::Type::UNIQUE:
|
||||
@@ -1859,12 +1752,7 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
if (property_set.size() != properties.size()) {
|
||||
throw SyntaxException("The given set of properties contains duplicates.");
|
||||
}
|
||||
constraint_notification.title =
|
||||
fmt::format("Created UNIQUE constraint on label {} on properties {}.",
|
||||
constraint_query->constraint_.label.name, utils::Join(properties_string, ", "));
|
||||
handler = [interpreter_context, label, label_name = constraint_query->constraint_.label.name,
|
||||
properties_stringified = std::move(properties_stringified),
|
||||
property_set = std::move(property_set)](Notification &constraint_notification) {
|
||||
handler = [interpreter_context, label, property_set = std::move(property_set)] {
|
||||
auto res = interpreter_context->db->CreateUniqueConstraint(label, property_set);
|
||||
if (res.HasError()) {
|
||||
auto violation = res.GetError();
|
||||
@@ -1878,33 +1766,29 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
"Unable to create unique constraint :{}({}), because an "
|
||||
"existing node violates it.",
|
||||
label_name, property_names_stream.str());
|
||||
}
|
||||
switch (res.GetValue()) {
|
||||
case storage::UniqueConstraints::CreationStatus::EMPTY_PROPERTIES:
|
||||
throw SyntaxException(
|
||||
"At least one property must be used for unique "
|
||||
"constraints.");
|
||||
case storage::UniqueConstraints::CreationStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED:
|
||||
throw SyntaxException(
|
||||
"Too many properties specified. Limit of {} properties "
|
||||
"for unique constraints is exceeded.",
|
||||
storage::kUniqueConstraintsMaxProperties);
|
||||
case storage::UniqueConstraints::CreationStatus::ALREADY_EXISTS:
|
||||
constraint_notification.code = NotificationCode::EXISTANT_CONSTRAINT;
|
||||
constraint_notification.title =
|
||||
fmt::format("Constraint UNIQUE on label {} on properties {} already exists.", label_name,
|
||||
properties_stringified);
|
||||
break;
|
||||
case storage::UniqueConstraints::CreationStatus::SUCCESS:
|
||||
break;
|
||||
} else {
|
||||
switch (res.GetValue()) {
|
||||
case storage::UniqueConstraints::CreationStatus::EMPTY_PROPERTIES:
|
||||
throw SyntaxException(
|
||||
"At least one property must be used for unique "
|
||||
"constraints.");
|
||||
break;
|
||||
case storage::UniqueConstraints::CreationStatus::PROPERTIES_SIZE_LIMIT_EXCEEDED:
|
||||
throw SyntaxException(
|
||||
"Too many properties specified. Limit of {} properties "
|
||||
"for unique constraints is exceeded.",
|
||||
storage::kUniqueConstraintsMaxProperties);
|
||||
break;
|
||||
case storage::UniqueConstraints::CreationStatus::ALREADY_EXISTS:
|
||||
case storage::UniqueConstraints::CreationStatus::SUCCESS:
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
break;
|
||||
}
|
||||
} break;
|
||||
case ConstraintQuery::ActionType::DROP: {
|
||||
constraint_notification.code = NotificationCode::DROP_CONSTRAINT;
|
||||
|
||||
switch (constraint_query->constraint_.type) {
|
||||
case Constraint::Type::NODE_KEY:
|
||||
throw utils::NotYetImplemented("Node key constraints");
|
||||
@@ -1912,17 +1796,8 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
if (properties.empty() || properties.size() > 1) {
|
||||
throw SyntaxException("Exactly one property must be used for existence constraints.");
|
||||
}
|
||||
constraint_notification.title =
|
||||
fmt::format("Dropped EXISTS constraint on label {} on properties {}.",
|
||||
constraint_query->constraint_.label.name, utils::Join(properties_string, ", "));
|
||||
handler = [interpreter_context, label, label_name = constraint_query->constraint_.label.name,
|
||||
properties_stringified = std::move(properties_stringified),
|
||||
properties = std::move(properties)](Notification &constraint_notification) {
|
||||
if (!interpreter_context->db->DropExistenceConstraint(label, properties[0])) {
|
||||
constraint_notification.code = NotificationCode::NONEXISTANT_CONSTRAINT;
|
||||
constraint_notification.title = fmt::format(
|
||||
"Constraint EXISTS on label {} on properties {} doesn't exist.", label_name, properties_stringified);
|
||||
}
|
||||
handler = [interpreter_context, label, properties = std::move(properties)] {
|
||||
interpreter_context->db->DropExistenceConstraint(label, properties[0]);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
break;
|
||||
@@ -1934,12 +1809,7 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
if (property_set.size() != properties.size()) {
|
||||
throw SyntaxException("The given set of properties contains duplicates.");
|
||||
}
|
||||
constraint_notification.title =
|
||||
fmt::format("Dropped UNIQUE constraint on label {} on properties {}.",
|
||||
constraint_query->constraint_.label.name, utils::Join(properties_string, ", "));
|
||||
handler = [interpreter_context, label, label_name = constraint_query->constraint_.label.name,
|
||||
properties_stringified = std::move(properties_stringified),
|
||||
property_set = std::move(property_set)](Notification &constraint_notification) {
|
||||
handler = [interpreter_context, label, property_set = std::move(property_set)] {
|
||||
auto res = interpreter_context->db->DropUniqueConstraint(label, property_set);
|
||||
switch (res) {
|
||||
case storage::UniqueConstraints::DeletionStatus::EMPTY_PROPERTIES:
|
||||
@@ -1954,11 +1824,6 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
storage::kUniqueConstraintsMaxProperties);
|
||||
break;
|
||||
case storage::UniqueConstraints::DeletionStatus::NOT_FOUND:
|
||||
constraint_notification.code = NotificationCode::NONEXISTANT_CONSTRAINT;
|
||||
constraint_notification.title =
|
||||
fmt::format("Constraint UNIQUE on label {} on properties {} doesn't exist.", label_name,
|
||||
properties_stringified);
|
||||
break;
|
||||
case storage::UniqueConstraints::DeletionStatus::SUCCESS:
|
||||
break;
|
||||
}
|
||||
@@ -1970,10 +1835,8 @@ PreparedQuery PrepareConstraintQuery(ParsedQuery parsed_query, bool in_explicit_
|
||||
|
||||
return PreparedQuery{{},
|
||||
std::move(parsed_query.required_privileges),
|
||||
[handler = std::move(handler), constraint_notification = std::move(constraint_notification),
|
||||
notifications](AnyStream * /*stream*/, std::optional<int> /*n*/) mutable {
|
||||
handler(constraint_notification);
|
||||
notifications->push_back(constraint_notification);
|
||||
[handler = std::move(handler)](AnyStream *stream, std::optional<int> n) {
|
||||
handler();
|
||||
return QueryHandlerResult::COMMIT;
|
||||
},
|
||||
RWType::NONE};
|
||||
@@ -2059,7 +1922,6 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
if (utils::Downcast<CypherQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareCypherQuery(std::move(parsed_query), &query_execution->summary, interpreter_context_,
|
||||
&*execution_db_accessor_, &query_execution->execution_memory,
|
||||
&query_execution->notifications,
|
||||
trigger_context_collector_ ? &*trigger_context_collector_ : nullptr);
|
||||
} else if (utils::Downcast<ExplainQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareExplainQuery(std::move(parsed_query), &query_execution->summary, interpreter_context_,
|
||||
@@ -2072,8 +1934,8 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
prepared_query = PrepareDumpQuery(std::move(parsed_query), &query_execution->summary, &*execution_db_accessor_,
|
||||
&query_execution->execution_memory);
|
||||
} else if (utils::Downcast<IndexQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareIndexQuery(std::move(parsed_query), in_explicit_transaction_,
|
||||
&query_execution->notifications, interpreter_context_);
|
||||
prepared_query = PrepareIndexQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary,
|
||||
interpreter_context_, &query_execution->execution_memory_with_exception);
|
||||
} else if (utils::Downcast<AuthQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareAuthQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary,
|
||||
interpreter_context_, &*execution_db_accessor_,
|
||||
@@ -2083,25 +1945,23 @@ Interpreter::PrepareResult Interpreter::Prepare(const std::string &query_string,
|
||||
interpreter_context_, interpreter_context_->db,
|
||||
&query_execution->execution_memory_with_exception);
|
||||
} else if (utils::Downcast<ConstraintQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareConstraintQuery(std::move(parsed_query), in_explicit_transaction_,
|
||||
&query_execution->notifications, interpreter_context_);
|
||||
} else if (utils::Downcast<ReplicationQuery>(parsed_query.query)) {
|
||||
prepared_query =
|
||||
PrepareReplicationQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->notifications,
|
||||
interpreter_context_, &*execution_db_accessor_);
|
||||
PrepareConstraintQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->summary,
|
||||
interpreter_context_, &query_execution->execution_memory_with_exception);
|
||||
} else if (utils::Downcast<ReplicationQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareReplicationQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_,
|
||||
&*execution_db_accessor_);
|
||||
} else if (utils::Downcast<LockPathQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareLockPathQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_,
|
||||
&*execution_db_accessor_);
|
||||
} else if (utils::Downcast<FreeMemoryQuery>(parsed_query.query)) {
|
||||
prepared_query = PrepareFreeMemoryQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_);
|
||||
} else if (utils::Downcast<TriggerQuery>(parsed_query.query)) {
|
||||
prepared_query =
|
||||
PrepareTriggerQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->notifications,
|
||||
interpreter_context_, &*execution_db_accessor_, params, username);
|
||||
prepared_query = PrepareTriggerQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_,
|
||||
&*execution_db_accessor_, params, username);
|
||||
} else if (utils::Downcast<StreamQuery>(parsed_query.query)) {
|
||||
prepared_query =
|
||||
PrepareStreamQuery(std::move(parsed_query), in_explicit_transaction_, &query_execution->notifications,
|
||||
interpreter_context_, &*execution_db_accessor_, params, username);
|
||||
prepared_query = PrepareStreamQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_,
|
||||
&*execution_db_accessor_, params, username);
|
||||
} else if (utils::Downcast<IsolationLevelQuery>(parsed_query.query)) {
|
||||
prepared_query =
|
||||
PrepareIsolationLevelQuery(std::move(parsed_query), in_explicit_transaction_, interpreter_context_, this);
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#include "query/frontend/ast/cypher_main_visitor.hpp"
|
||||
#include "query/frontend/stripped.hpp"
|
||||
#include "query/interpret/frame.hpp"
|
||||
#include "query/metadata.hpp"
|
||||
#include "query/plan/operator.hpp"
|
||||
#include "query/plan/read_write_type_checker.hpp"
|
||||
#include "query/stream.hpp"
|
||||
@@ -190,7 +189,7 @@ struct InterpreterContext {
|
||||
|
||||
const InterpreterConfig config;
|
||||
|
||||
query::stream::Streams streams;
|
||||
query::Streams streams;
|
||||
};
|
||||
|
||||
/// Function that is used to tell all active interpreters that they should stop
|
||||
@@ -286,7 +285,6 @@ class Interpreter final {
|
||||
utils::ResourceWithOutOfMemoryException execution_memory_with_exception{&execution_memory};
|
||||
|
||||
std::map<std::string, TypedValue> summary;
|
||||
std::vector<Notification> notifications;
|
||||
|
||||
explicit QueryExecution() = default;
|
||||
QueryExecution(const QueryExecution &) = delete;
|
||||
@@ -379,14 +377,6 @@ std::map<std::string, TypedValue> Interpreter::Pull(TStream *result_stream, std:
|
||||
if (maybe_res) {
|
||||
// Save its summary
|
||||
maybe_summary.emplace(std::move(query_execution->summary));
|
||||
if (!query_execution->notifications.empty()) {
|
||||
std::vector<TypedValue> notifications;
|
||||
notifications.reserve(query_execution->notifications.size());
|
||||
for (const auto ¬ification : query_execution->notifications) {
|
||||
notifications.emplace_back(notification.ConvertToMap());
|
||||
}
|
||||
maybe_summary->insert_or_assign("notifications", std::move(notifications));
|
||||
}
|
||||
if (!in_explicit_transaction_) {
|
||||
switch (*maybe_res) {
|
||||
case QueryHandlerResult::COMMIT:
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
// 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.
|
||||
|
||||
#include "query/metadata.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <compare>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace query {
|
||||
|
||||
namespace {
|
||||
using namespace std::literals;
|
||||
|
||||
constexpr std::string_view GetSeverityLevelString(const SeverityLevel level) {
|
||||
switch (level) {
|
||||
case SeverityLevel::INFO:
|
||||
return "INFO"sv;
|
||||
case SeverityLevel::WARNING:
|
||||
return "WARNING"sv;
|
||||
}
|
||||
}
|
||||
|
||||
constexpr std::string_view GetCodeString(const NotificationCode code) {
|
||||
switch (code) {
|
||||
case NotificationCode::CREATE_CONSTRAINT:
|
||||
return "CreateConstraint"sv;
|
||||
case NotificationCode::CREATE_INDEX:
|
||||
return "CreateIndex"sv;
|
||||
case NotificationCode::CREATE_STREAM:
|
||||
return "CreateStream"sv;
|
||||
case NotificationCode::CHECK_STREAM:
|
||||
return "CheckStream"sv;
|
||||
case NotificationCode::CREATE_TRIGGER:
|
||||
return "CreateTrigger"sv;
|
||||
case NotificationCode::DROP_CONSTRAINT:
|
||||
return "DropConstraint"sv;
|
||||
case NotificationCode::DROP_REPLICA:
|
||||
return "DropReplica"sv;
|
||||
case NotificationCode::DROP_INDEX:
|
||||
return "DropIndex"sv;
|
||||
case NotificationCode::DROP_STREAM:
|
||||
return "DropStream"sv;
|
||||
case NotificationCode::DROP_TRIGGER:
|
||||
return "DropTrigger"sv;
|
||||
case NotificationCode::EXISTANT_CONSTRAINT:
|
||||
return "ConstraintAlreadyExists"sv;
|
||||
case NotificationCode::EXISTANT_INDEX:
|
||||
return "IndexAlreadyExists"sv;
|
||||
case NotificationCode::LOAD_CSV_TIP:
|
||||
return "LoadCSVTip"sv;
|
||||
case NotificationCode::NONEXISTANT_INDEX:
|
||||
return "IndexDoesNotExist"sv;
|
||||
case NotificationCode::NONEXISTANT_CONSTRAINT:
|
||||
return "ConstraintDoesNotExist"sv;
|
||||
case NotificationCode::REGISTER_REPLICA:
|
||||
return "RegisterReplica"sv;
|
||||
case NotificationCode::REPLICA_PORT_WARNING:
|
||||
return "ReplicaPortWarning"sv;
|
||||
case NotificationCode::SET_REPLICA:
|
||||
return "SetReplica"sv;
|
||||
case NotificationCode::START_STREAM:
|
||||
return "StartStream"sv;
|
||||
case NotificationCode::START_ALL_STREAMS:
|
||||
return "StartAllStreams"sv;
|
||||
case NotificationCode::STOP_STREAM:
|
||||
return "StopStream"sv;
|
||||
case NotificationCode::STOP_ALL_STREAMS:
|
||||
return "StopAllStreams"sv;
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Notification::Notification(SeverityLevel level) : level{level} {};
|
||||
|
||||
Notification::Notification(SeverityLevel level, NotificationCode code, std::string title, std::string description)
|
||||
: level{level}, code{code}, title(std::move(title)), description(std::move(description)){};
|
||||
|
||||
Notification::Notification(SeverityLevel level, NotificationCode code, std::string title)
|
||||
: level{level}, code{code}, title(std::move(title)){};
|
||||
|
||||
std::map<std::string, TypedValue> Notification::ConvertToMap() const {
|
||||
return std::map<std::string, TypedValue>{{"severity", TypedValue(GetSeverityLevelString(level))},
|
||||
{"code", TypedValue(GetCodeString(code))},
|
||||
{"title", TypedValue(title)},
|
||||
{"description", TypedValue(description)}};
|
||||
}
|
||||
|
||||
std::string ExecutionStatsKeyToString(const ExecutionStats::Key key) {
|
||||
switch (key) {
|
||||
case ExecutionStats::Key::CREATED_NODES:
|
||||
return std::string("nodes-created");
|
||||
case ExecutionStats::Key::DELETED_NODES:
|
||||
return std::string("nodes-deleted");
|
||||
case ExecutionStats::Key::CREATED_EDGES:
|
||||
return std::string("relationships-created");
|
||||
case ExecutionStats::Key::DELETED_EDGES:
|
||||
return std::string("relationships-deleted");
|
||||
case ExecutionStats::Key::CREATED_LABELS:
|
||||
return std::string("labels-added");
|
||||
case ExecutionStats::Key::DELETED_LABELS:
|
||||
return std::string("labels-removed");
|
||||
case ExecutionStats::Key::UPDATED_PROPERTIES:
|
||||
return std::string("properties-set");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace query
|
||||
@@ -1,90 +0,0 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <type_traits>
|
||||
|
||||
#include "query/typed_value.hpp"
|
||||
|
||||
namespace query {
|
||||
|
||||
enum class SeverityLevel : uint8_t { INFO, WARNING };
|
||||
|
||||
enum class NotificationCode : uint8_t {
|
||||
CREATE_CONSTRAINT,
|
||||
CREATE_INDEX,
|
||||
CHECK_STREAM,
|
||||
CREATE_STREAM,
|
||||
CREATE_TRIGGER,
|
||||
DROP_CONSTRAINT,
|
||||
DROP_INDEX,
|
||||
DROP_REPLICA,
|
||||
DROP_STREAM,
|
||||
DROP_TRIGGER,
|
||||
EXISTANT_INDEX,
|
||||
EXISTANT_CONSTRAINT,
|
||||
LOAD_CSV_TIP,
|
||||
NONEXISTANT_INDEX,
|
||||
NONEXISTANT_CONSTRAINT,
|
||||
REPLICA_PORT_WARNING,
|
||||
REGISTER_REPLICA,
|
||||
SET_REPLICA,
|
||||
START_STREAM,
|
||||
START_ALL_STREAMS,
|
||||
STOP_STREAM,
|
||||
STOP_ALL_STREAMS,
|
||||
};
|
||||
|
||||
struct Notification {
|
||||
SeverityLevel level;
|
||||
NotificationCode code;
|
||||
std::string title;
|
||||
std::string description;
|
||||
|
||||
explicit Notification(SeverityLevel level);
|
||||
|
||||
Notification(SeverityLevel level, NotificationCode code, std::string title, std::string description);
|
||||
|
||||
Notification(SeverityLevel level, NotificationCode code, std::string title);
|
||||
|
||||
std::map<std::string, TypedValue> ConvertToMap() const;
|
||||
};
|
||||
|
||||
struct ExecutionStats {
|
||||
public:
|
||||
// All the stats have specific key to be compatible with neo4j
|
||||
enum class Key : uint8_t {
|
||||
CREATED_NODES,
|
||||
DELETED_NODES,
|
||||
CREATED_EDGES,
|
||||
DELETED_EDGES,
|
||||
CREATED_LABELS,
|
||||
DELETED_LABELS,
|
||||
UPDATED_PROPERTIES,
|
||||
};
|
||||
|
||||
int64_t &operator[](Key key) { return counters[static_cast<size_t>(key)]; }
|
||||
|
||||
private:
|
||||
static constexpr auto kExecutionStatsCountersSize = std::underlying_type_t<Key>(Key::UPDATED_PROPERTIES) + 1;
|
||||
|
||||
public:
|
||||
std::array<int64_t, kExecutionStatsCountersSize> counters{0};
|
||||
};
|
||||
|
||||
std::string ExecutionStatsKeyToString(ExecutionStats::Key key);
|
||||
|
||||
} // namespace query
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "query/plan/operator.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
#include <random>
|
||||
@@ -172,16 +171,15 @@ CreateNode::CreateNode(const std::shared_ptr<LogicalOperator> &input, const Node
|
||||
|
||||
// Creates a vertex on this GraphDb. Returns a reference to vertex placed on the
|
||||
// frame.
|
||||
VertexAccessor &CreateLocalVertex(const NodeCreationInfo &node_info, Frame *frame, ExecutionContext &context) {
|
||||
VertexAccessor &CreateLocalVertex(const NodeCreationInfo &node_info, Frame *frame, const ExecutionContext &context) {
|
||||
auto &dba = *context.db_accessor;
|
||||
auto new_node = dba.InsertVertex();
|
||||
context.execution_stats[ExecutionStats::Key::CREATED_NODES] += 1;
|
||||
for (auto label : node_info.labels) {
|
||||
auto maybe_error = new_node.AddLabel(label);
|
||||
if (maybe_error.HasError()) {
|
||||
switch (maybe_error.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set a label on a deleted node.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
@@ -190,7 +188,6 @@ VertexAccessor &CreateLocalVertex(const NodeCreationInfo &node_info, Frame *fram
|
||||
throw QueryRuntimeException("Unexpected error when setting a label.");
|
||||
}
|
||||
}
|
||||
context.execution_stats[ExecutionStats::Key::CREATED_LABELS] += 1;
|
||||
}
|
||||
// Evaluator should use the latest accessors, as modified in this query, when
|
||||
// setting properties on new nodes.
|
||||
@@ -298,7 +295,7 @@ EdgeAccessor CreateEdge(const EdgeCreationInfo &edge_info, DbAccessor *dba, Vert
|
||||
} else {
|
||||
switch (maybe_edge.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to create an edge on a deleted node.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
@@ -349,7 +346,6 @@ bool CreateExpand::CreateExpandCursor::Pull(Frame &frame, ExecutionContext &cont
|
||||
}
|
||||
}();
|
||||
|
||||
context.execution_stats[ExecutionStats::Key::CREATED_EDGES] += 1;
|
||||
if (context.trigger_context_collector) {
|
||||
context.trigger_context_collector->RegisterCreatedObject(created_edge);
|
||||
}
|
||||
@@ -1921,7 +1917,7 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
if (maybe_value.HasError()) {
|
||||
switch (maybe_value.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
@@ -1929,7 +1925,7 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
throw QueryRuntimeException("Unexpected error when deleting an edge.");
|
||||
}
|
||||
}
|
||||
context.execution_stats[ExecutionStats::Key::DELETED_EDGES] += 1;
|
||||
|
||||
if (context.trigger_context_collector && maybe_value.GetValue()) {
|
||||
context.trigger_context_collector->RegisterDeletedObject(*maybe_value.GetValue());
|
||||
}
|
||||
@@ -1947,7 +1943,7 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
if (res.HasError()) {
|
||||
switch (res.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
@@ -1956,10 +1952,6 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
}
|
||||
}
|
||||
|
||||
context.execution_stats[ExecutionStats::Key::DELETED_NODES] += 1;
|
||||
if (*res) {
|
||||
context.execution_stats[ExecutionStats::Key::DELETED_EDGES] += static_cast<int64_t>((*res)->second.size());
|
||||
}
|
||||
std::invoke([&] {
|
||||
if (!context.trigger_context_collector || !*res) {
|
||||
return;
|
||||
@@ -1978,7 +1970,7 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
if (res.HasError()) {
|
||||
switch (res.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
throw RemoveAttachedVertexException();
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
@@ -1987,7 +1979,7 @@ bool Delete::DeleteCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
throw QueryRuntimeException("Unexpected error when deleting a node.");
|
||||
}
|
||||
}
|
||||
context.execution_stats[ExecutionStats::Key::DELETED_NODES] += 1;
|
||||
|
||||
if (context.trigger_context_collector && res.GetValue()) {
|
||||
context.trigger_context_collector->RegisterDeletedObject(*res.GetValue());
|
||||
}
|
||||
@@ -2046,7 +2038,7 @@ bool SetProperty::SetPropertyCursor::Pull(Frame &frame, ExecutionContext &contex
|
||||
switch (lhs.type()) {
|
||||
case TypedValue::Type::Vertex: {
|
||||
auto old_value = PropsSetChecked(&lhs.ValueVertex(), self_.property_, rhs);
|
||||
context.execution_stats[ExecutionStats::Key::UPDATED_PROPERTIES] += 1;
|
||||
|
||||
if (context.trigger_context_collector) {
|
||||
// rhs cannot be moved because it was created with the allocator that is only valid during current pull
|
||||
context.trigger_context_collector->RegisterSetObjectProperty(lhs.ValueVertex(), self_.property_,
|
||||
@@ -2056,7 +2048,7 @@ bool SetProperty::SetPropertyCursor::Pull(Frame &frame, ExecutionContext &contex
|
||||
}
|
||||
case TypedValue::Type::Edge: {
|
||||
auto old_value = PropsSetChecked(&lhs.ValueEdge(), self_.property_, rhs);
|
||||
context.execution_stats[ExecutionStats::Key::UPDATED_PROPERTIES] += 1;
|
||||
|
||||
if (context.trigger_context_collector) {
|
||||
// rhs cannot be moved because it was created with the allocator that is only valid during current pull
|
||||
context.trigger_context_collector->RegisterSetObjectProperty(lhs.ValueEdge(), self_.property_,
|
||||
@@ -2128,7 +2120,7 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set properties on a deleted graph element.");
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
@@ -2184,7 +2176,7 @@ void SetPropertiesOnRecord(TRecordAccessor *record, const TypedValue &rhs, SetPr
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set properties on a deleted graph element.");
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
throw QueryRuntimeException("Can't set property because properties on edges are disabled.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
@@ -2299,7 +2291,7 @@ bool SetLabels::SetLabelsCursor::Pull(Frame &frame, ExecutionContext &context) {
|
||||
if (maybe_value.HasError()) {
|
||||
switch (maybe_value.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to set a label on a deleted node.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
@@ -2357,7 +2349,7 @@ bool RemoveProperty::RemovePropertyCursor::Pull(Frame &frame, ExecutionContext &
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to remove a property on a deleted graph element.");
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::PROPERTIES_DISABLED:
|
||||
throw QueryRuntimeException(
|
||||
"Can't remove property because properties on edges are "
|
||||
@@ -2428,7 +2420,7 @@ bool RemoveLabels::RemoveLabelsCursor::Pull(Frame &frame, ExecutionContext &cont
|
||||
if (maybe_value.HasError()) {
|
||||
switch (maybe_value.GetError()) {
|
||||
case storage::Error::SERIALIZATION_ERROR:
|
||||
throw TransactionSerializationException();
|
||||
throw QueryRuntimeException(kSerializationErrorMessage);
|
||||
case storage::Error::DELETED_OBJECT:
|
||||
throw QueryRuntimeException("Trying to remove labels from a deleted node.");
|
||||
case storage::Error::VERTEX_HAS_EDGES:
|
||||
@@ -2438,7 +2430,6 @@ bool RemoveLabels::RemoveLabelsCursor::Pull(Frame &frame, ExecutionContext &cont
|
||||
}
|
||||
}
|
||||
|
||||
context.execution_stats[ExecutionStats::Key::DELETED_LABELS] += 1;
|
||||
if (context.trigger_context_collector && *maybe_value) {
|
||||
context.trigger_context_collector->RegisterRemovedVertexLabel(vertex, label);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,6 @@
|
||||
#include "module.hpp"
|
||||
#include "query/procedure/cypher_types.hpp"
|
||||
#include "query/procedure/mg_procedure_helpers.hpp"
|
||||
#include "query/stream/common.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/view.hpp"
|
||||
#include "utils/algorithm.hpp"
|
||||
@@ -36,7 +35,6 @@
|
||||
#include "utils/memory.hpp"
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/temporal.hpp"
|
||||
#include "utils/variant_helpers.hpp"
|
||||
|
||||
// This file contains implementation of top level C API functions, but this is
|
||||
// all actually part of query::procedure. So use that namespace for simplicity.
|
||||
@@ -1583,11 +1581,7 @@ mgp_error mgp_vertex_set_property(struct mgp_vertex *v, const char *property_nam
|
||||
}
|
||||
}
|
||||
|
||||
auto &ctx = v->graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::UPDATED_PROPERTIES] += 1;
|
||||
|
||||
auto *trigger_ctx_collector = ctx->trigger_context_collector;
|
||||
auto *trigger_ctx_collector = v->graph->ctx->trigger_context_collector;
|
||||
if (!trigger_ctx_collector || !trigger_ctx_collector->ShouldRegisterObjectPropertyChange<query::VertexAccessor>()) {
|
||||
return;
|
||||
}
|
||||
@@ -1623,12 +1617,8 @@ mgp_error mgp_vertex_add_label(struct mgp_vertex *v, mgp_label label) {
|
||||
}
|
||||
}
|
||||
|
||||
auto &ctx = v->graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::CREATED_LABELS] += 1;
|
||||
|
||||
if (ctx->trigger_context_collector) {
|
||||
ctx->trigger_context_collector->RegisterSetVertexLabel(v->impl, label_id);
|
||||
if (v->graph->ctx->trigger_context_collector) {
|
||||
v->graph->ctx->trigger_context_collector->RegisterSetVertexLabel(v->impl, label_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1654,13 +1644,8 @@ mgp_error mgp_vertex_remove_label(struct mgp_vertex *v, mgp_label label) {
|
||||
throw SerializationException{"Cannot serialize removing a label from a vertex."};
|
||||
}
|
||||
}
|
||||
|
||||
auto &ctx = v->graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::DELETED_LABELS] += 1;
|
||||
|
||||
if (ctx->trigger_context_collector) {
|
||||
ctx->trigger_context_collector->RegisterRemovedVertexLabel(v->impl, label_id);
|
||||
if (v->graph->ctx->trigger_context_collector) {
|
||||
v->graph->ctx->trigger_context_collector->RegisterRemovedVertexLabel(v->impl, label_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2007,10 +1992,6 @@ mgp_error mgp_edge_set_property(struct mgp_edge *e, const char *property_name, m
|
||||
}
|
||||
}
|
||||
|
||||
auto &ctx = e->from.graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::UPDATED_PROPERTIES] += 1;
|
||||
|
||||
auto *trigger_ctx_collector = e->from.graph->ctx->trigger_context_collector;
|
||||
if (!trigger_ctx_collector || !trigger_ctx_collector->ShouldRegisterObjectPropertyChange<query::EdgeAccessor>()) {
|
||||
return;
|
||||
@@ -2076,12 +2057,8 @@ mgp_error mgp_graph_create_vertex(struct mgp_graph *graph, mgp_memory *memory, m
|
||||
throw ImmutableObjectException{"Cannot create a vertex in an immutable graph!"};
|
||||
}
|
||||
auto vertex = graph->impl->InsertVertex();
|
||||
|
||||
auto &ctx = graph->ctx;
|
||||
ctx->execution_stats[query::ExecutionStats::Key::CREATED_NODES] += 1;
|
||||
|
||||
if (ctx->trigger_context_collector) {
|
||||
ctx->trigger_context_collector->RegisterCreatedObject(vertex);
|
||||
if (graph->ctx->trigger_context_collector) {
|
||||
graph->ctx->trigger_context_collector->RegisterCreatedObject(vertex);
|
||||
}
|
||||
return NewRawMgpObject<mgp_vertex>(memory, vertex, graph);
|
||||
},
|
||||
@@ -2108,17 +2085,8 @@ mgp_error mgp_graph_delete_vertex(struct mgp_graph *graph, mgp_vertex *vertex) {
|
||||
throw SerializationException{"Cannot serialize removing a vertex."};
|
||||
}
|
||||
}
|
||||
|
||||
if (!*result) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &ctx = graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::DELETED_NODES] += 1;
|
||||
|
||||
if (ctx->trigger_context_collector) {
|
||||
ctx->trigger_context_collector->RegisterDeletedObject(**result);
|
||||
if (graph->ctx->trigger_context_collector && *result) {
|
||||
graph->ctx->trigger_context_collector->RegisterDeletedObject(**result);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2143,20 +2111,10 @@ mgp_error mgp_graph_detach_delete_vertex(struct mgp_graph *graph, mgp_vertex *ve
|
||||
}
|
||||
}
|
||||
|
||||
if (!*result) {
|
||||
auto *trigger_ctx_collector = graph->ctx->trigger_context_collector;
|
||||
if (!trigger_ctx_collector || !*result) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto &ctx = graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::DELETED_NODES] += 1;
|
||||
ctx->execution_stats[query::ExecutionStats::Key::DELETED_EDGES] += static_cast<int64_t>((*result)->second.size());
|
||||
|
||||
auto *trigger_ctx_collector = ctx->trigger_context_collector;
|
||||
if (!trigger_ctx_collector) {
|
||||
return;
|
||||
}
|
||||
|
||||
trigger_ctx_collector->RegisterDeletedObject((*result)->first);
|
||||
if (!trigger_ctx_collector->ShouldRegisterDeletedObject<query::EdgeAccessor>()) {
|
||||
return;
|
||||
@@ -2189,12 +2147,8 @@ mgp_error mgp_graph_create_edge(mgp_graph *graph, mgp_vertex *from, mgp_vertex *
|
||||
throw SerializationException{"Cannot serialize creating an edge."};
|
||||
}
|
||||
}
|
||||
auto &ctx = graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::CREATED_EDGES] += 1;
|
||||
|
||||
if (ctx->trigger_context_collector) {
|
||||
ctx->trigger_context_collector->RegisterCreatedObject(*edge);
|
||||
if (graph->ctx->trigger_context_collector) {
|
||||
graph->ctx->trigger_context_collector->RegisterCreatedObject(*edge);
|
||||
}
|
||||
return NewRawMgpObject<mgp_edge>(memory, edge.GetValue(), from->graph);
|
||||
},
|
||||
@@ -2220,15 +2174,8 @@ mgp_error mgp_graph_delete_edge(struct mgp_graph *graph, mgp_edge *edge) {
|
||||
throw SerializationException{"Cannot serialize removing an edge."};
|
||||
}
|
||||
}
|
||||
|
||||
if (!*result) {
|
||||
return;
|
||||
}
|
||||
auto &ctx = graph->ctx;
|
||||
|
||||
ctx->execution_stats[query::ExecutionStats::Key::DELETED_EDGES] += 1;
|
||||
if (ctx->trigger_context_collector) {
|
||||
ctx->trigger_context_collector->RegisterDeletedObject(**result);
|
||||
if (graph->ctx->trigger_context_collector && *result) {
|
||||
graph->ctx->trigger_context_collector->RegisterDeletedObject(**result);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2545,55 +2492,21 @@ bool IsValidIdentifierName(const char *name) {
|
||||
|
||||
} // namespace query::procedure
|
||||
|
||||
namespace {
|
||||
using StreamSourceType = query::stream::StreamSourceType;
|
||||
|
||||
class InvalidMessageFunction : public std::invalid_argument {
|
||||
public:
|
||||
InvalidMessageFunction(const StreamSourceType type, const std::string_view function_name)
|
||||
: std::invalid_argument{fmt::format("'{}' is not defined for a message from a stream of type '{}'", function_name,
|
||||
StreamSourceTypeToString(type))} {}
|
||||
};
|
||||
|
||||
StreamSourceType MessageToStreamSourceType(const mgp_message::KafkaMessage & /*msg*/) {
|
||||
return StreamSourceType::KAFKA;
|
||||
}
|
||||
|
||||
StreamSourceType MessageToStreamSourceType(const mgp_message::PulsarMessage & /*msg*/) {
|
||||
return StreamSourceType::PULSAR;
|
||||
}
|
||||
|
||||
mgp_source_type StreamSourceTypeToMgpSourceType(const StreamSourceType type) {
|
||||
switch (type) {
|
||||
case StreamSourceType::KAFKA:
|
||||
return mgp_source_type::KAFKA;
|
||||
case StreamSourceType::PULSAR:
|
||||
return mgp_source_type::PULSAR;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
mgp_error mgp_message_source_type(mgp_message *message, mgp_source_type *result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(utils::Overloaded{[](const auto &message) {
|
||||
return StreamSourceTypeToMgpSourceType(MessageToStreamSourceType(message));
|
||||
}},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
}
|
||||
|
||||
mgp_error mgp_message_payload(mgp_message *message, const char **result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(utils::Overloaded{[](const mgp_message::KafkaMessage &msg) { return msg->Payload().data(); },
|
||||
[](const mgp_message::PulsarMessage &msg) { return msg.Payload().data(); },
|
||||
[](const auto &msg) -> const char * {
|
||||
throw InvalidMessageFunction(MessageToStreamSourceType(msg), "payload");
|
||||
}},
|
||||
message->msg);
|
||||
return std::visit(
|
||||
[]<typename T>(T &&msg) -> const char * {
|
||||
using MessageType = std::decay_t<T>;
|
||||
if constexpr (std::same_as<MessageType, mgp_message::KafkaMessage>) {
|
||||
return msg->Payload().data();
|
||||
} else if constexpr (std::same_as<MessageType, mgp_message::PulsarMessage>) {
|
||||
return msg.Payload().data();
|
||||
} else {
|
||||
throw std::invalid_argument("Invalid source type");
|
||||
}
|
||||
},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
}
|
||||
@@ -2601,13 +2514,18 @@ mgp_error mgp_message_payload(mgp_message *message, const char **result) {
|
||||
mgp_error mgp_message_payload_size(mgp_message *message, size_t *result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(utils::Overloaded{[](const mgp_message::KafkaMessage &msg) { return msg->Payload().size(); },
|
||||
[](const mgp_message::PulsarMessage &msg) { return msg.Payload().size(); },
|
||||
[](const auto &msg) -> size_t {
|
||||
throw InvalidMessageFunction(MessageToStreamSourceType(msg),
|
||||
"payload_size");
|
||||
}},
|
||||
message->msg);
|
||||
return std::visit(
|
||||
[]<typename T>(T &&msg) -> size_t {
|
||||
using MessageType = std::decay_t<T>;
|
||||
if constexpr (std::same_as<MessageType, mgp_message::KafkaMessage>) {
|
||||
return msg->Payload().size();
|
||||
} else if constexpr (std::same_as<MessageType, mgp_message::PulsarMessage>) {
|
||||
return msg.Payload().size();
|
||||
} else {
|
||||
throw std::invalid_argument("Invalid source type");
|
||||
}
|
||||
},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
}
|
||||
@@ -2616,11 +2534,14 @@ mgp_error mgp_message_topic_name(mgp_message *message, const char **result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(
|
||||
utils::Overloaded{[](const mgp_message::KafkaMessage &msg) { return msg->TopicName().data(); },
|
||||
[](const mgp_message::PulsarMessage &msg) { return msg.TopicName().data(); },
|
||||
[](const auto &msg) -> const char * {
|
||||
throw InvalidMessageFunction(MessageToStreamSourceType(msg), "topic_name");
|
||||
}},
|
||||
[]<typename T>(T &&msg) -> const char * {
|
||||
using MessageType = std::decay_t<T>;
|
||||
if constexpr (std::same_as<MessageType, mgp_message::KafkaMessage>) {
|
||||
return msg->TopicName().data();
|
||||
} else {
|
||||
throw std::invalid_argument("Invalid source type");
|
||||
}
|
||||
},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
@@ -2629,11 +2550,16 @@ mgp_error mgp_message_topic_name(mgp_message *message, const char **result) {
|
||||
mgp_error mgp_message_key(mgp_message *message, const char **result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(utils::Overloaded{[](const mgp_message::KafkaMessage &msg) { return msg->Key().data(); },
|
||||
[](const auto &msg) -> const char * {
|
||||
throw InvalidMessageFunction(MessageToStreamSourceType(msg), "key");
|
||||
}},
|
||||
message->msg);
|
||||
return std::visit(
|
||||
[]<typename T>(T &&msg) -> const char * {
|
||||
using MessageType = std::decay_t<T>;
|
||||
if constexpr (std::same_as<MessageType, mgp_message::KafkaMessage>) {
|
||||
return msg->Key().data();
|
||||
} else {
|
||||
throw std::invalid_argument("Invalid source type");
|
||||
}
|
||||
},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
}
|
||||
@@ -2641,11 +2567,16 @@ mgp_error mgp_message_key(mgp_message *message, const char **result) {
|
||||
mgp_error mgp_message_key_size(mgp_message *message, size_t *result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(utils::Overloaded{[](const mgp_message::KafkaMessage &msg) { return msg->Key().size(); },
|
||||
[](const auto &msg) -> size_t {
|
||||
throw InvalidMessageFunction(MessageToStreamSourceType(msg), "key_size");
|
||||
}},
|
||||
message->msg);
|
||||
return std::visit(
|
||||
[]<typename T>(T &&msg) -> size_t {
|
||||
using MessageType = std::decay_t<T>;
|
||||
if constexpr (std::same_as<MessageType, mgp_message::KafkaMessage>) {
|
||||
return msg->Key().size();
|
||||
} else {
|
||||
throw std::invalid_argument("Invalid source type");
|
||||
}
|
||||
},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
}
|
||||
@@ -2653,23 +2584,16 @@ mgp_error mgp_message_key_size(mgp_message *message, size_t *result) {
|
||||
mgp_error mgp_message_timestamp(mgp_message *message, int64_t *result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(utils::Overloaded{[](const mgp_message::KafkaMessage &msg) { return msg->Timestamp(); },
|
||||
[](const auto &msg) -> int64_t {
|
||||
throw InvalidMessageFunction(MessageToStreamSourceType(msg), "timestamp");
|
||||
}},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
}
|
||||
|
||||
mgp_error mgp_message_offset(struct mgp_message *message, int64_t *result) {
|
||||
return WrapExceptions(
|
||||
[message] {
|
||||
return std::visit(utils::Overloaded{[](const mgp_message::KafkaMessage &msg) { return msg->Offset(); },
|
||||
[](const auto &msg) -> int64_t {
|
||||
throw InvalidMessageFunction(MessageToStreamSourceType(msg), "offset");
|
||||
}},
|
||||
message->msg);
|
||||
return std::visit(
|
||||
[]<typename T>(T &&msg) -> int64_t {
|
||||
using MessageType = std::decay_t<T>;
|
||||
if constexpr (std::same_as<MessageType, mgp_message::KafkaMessage>) {
|
||||
return msg->Timestamp();
|
||||
} else {
|
||||
throw std::invalid_argument("Invalid source type");
|
||||
}
|
||||
},
|
||||
message->msg);
|
||||
},
|
||||
result);
|
||||
}
|
||||
|
||||
@@ -677,17 +677,6 @@ struct mgp_proc {
|
||||
results(memory),
|
||||
is_write_procedure(is_write_procedure) {}
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
/// @throw std::length_error
|
||||
mgp_proc(const std::string_view name, std::function<void(mgp_list *, mgp_graph *, mgp_result *, mgp_memory *)> cb,
|
||||
utils::MemoryResource *memory, bool is_write_procedure)
|
||||
: name(name, memory),
|
||||
cb(cb),
|
||||
args(memory),
|
||||
opt_args(memory),
|
||||
results(memory),
|
||||
is_write_procedure(is_write_procedure) {}
|
||||
|
||||
/// @throw std::bad_alloc
|
||||
/// @throw std::length_error
|
||||
mgp_proc(const mgp_proc &other, utils::MemoryResource *memory)
|
||||
|
||||
@@ -644,16 +644,6 @@ void ModuleRegistry::UnloadAllModules() {
|
||||
|
||||
utils::MemoryResource &ModuleRegistry::GetSharedMemoryResource() noexcept { return *shared_; }
|
||||
|
||||
bool ModuleRegistry::RegisterMgProcedure(const std::string_view name, mgp_proc proc) {
|
||||
std::unique_lock<utils::RWLock> guard(lock_);
|
||||
if (auto module = modules_.find("mg"); module != modules_.end()) {
|
||||
auto *builtin_module = dynamic_cast<BuiltinModule *>(module->second.get());
|
||||
builtin_module->AddProcedure(name, std::move(proc));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/// This function returns a pair of either
|
||||
|
||||
@@ -117,8 +117,6 @@ class ModuleRegistry final {
|
||||
/// Returns the shared memory allocator used by modules
|
||||
utils::MemoryResource &GetSharedMemoryResource() noexcept;
|
||||
|
||||
bool RegisterMgProcedure(std::string_view name, mgp_proc proc);
|
||||
|
||||
private:
|
||||
std::vector<std::filesystem::path> modules_dirs_;
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "mg_procedure.h"
|
||||
#include "query/procedure/mg_procedure_helpers.hpp"
|
||||
#include "query/procedure/mg_procedure_impl.hpp"
|
||||
#include "utils/memory.hpp"
|
||||
@@ -556,21 +555,6 @@ PyObject *PyMessageIsValid(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
||||
return PyMessagesIsValid(self->messages, nullptr);
|
||||
}
|
||||
|
||||
PyObject *PyMessageGetSourceType(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
||||
MG_ASSERT(self->message);
|
||||
MG_ASSERT(self->memory);
|
||||
mgp_source_type source_type{mgp_source_type::KAFKA};
|
||||
if (RaiseExceptionFromErrorCode(mgp_message_source_type(self->message, &source_type))) {
|
||||
return nullptr;
|
||||
}
|
||||
auto *py_source_type = PyLong_FromLong(static_cast<int64_t>(source_type));
|
||||
if (!py_source_type) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unable to get long from source type");
|
||||
return nullptr;
|
||||
}
|
||||
return py_source_type;
|
||||
}
|
||||
|
||||
PyObject *PyMessageGetPayload(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
||||
MG_ASSERT(self->message);
|
||||
size_t payload_size{0};
|
||||
@@ -598,7 +582,7 @@ PyObject *PyMessageGetTopicName(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
||||
}
|
||||
auto *py_topic_name = PyUnicode_FromString(topic_name);
|
||||
if (!py_topic_name) {
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unable to get string from topic_name");
|
||||
PyErr_SetString(PyExc_RuntimeError, "Unable to get raw bytes from payload");
|
||||
return nullptr;
|
||||
}
|
||||
return py_topic_name;
|
||||
@@ -638,32 +622,15 @@ PyObject *PyMessageGetTimestamp(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
||||
return py_int;
|
||||
}
|
||||
|
||||
PyObject *PyMessageGetOffset(PyMessage *self, PyObject *Py_UNUSED(ignored)) {
|
||||
MG_ASSERT(self->message);
|
||||
MG_ASSERT(self->memory);
|
||||
int64_t offset{0};
|
||||
if (RaiseExceptionFromErrorCode(mgp_message_offset(self->message, &offset))) {
|
||||
return nullptr;
|
||||
}
|
||||
auto *py_int = PyLong_FromLongLong(offset);
|
||||
if (!py_int) {
|
||||
PyErr_SetString(PyExc_IndexError, "Unable to get offset");
|
||||
return nullptr;
|
||||
}
|
||||
return py_int;
|
||||
}
|
||||
|
||||
// NOLINTNEXTLINE
|
||||
static PyMethodDef PyMessageMethods[] = {
|
||||
{"__reduce__", reinterpret_cast<PyCFunction>(DisallowPickleAndCopy), METH_NOARGS, "__reduce__ is not supported"},
|
||||
{"is_valid", reinterpret_cast<PyCFunction>(PyMessageIsValid), METH_NOARGS,
|
||||
"Return True if messages is in valid context and may be used."},
|
||||
{"source_type", reinterpret_cast<PyCFunction>(PyMessageGetSourceType), METH_NOARGS, "Get stream source type."},
|
||||
{"payload", reinterpret_cast<PyCFunction>(PyMessageGetPayload), METH_NOARGS, "Get payload"},
|
||||
{"topic_name", reinterpret_cast<PyCFunction>(PyMessageGetTopicName), METH_NOARGS, "Get topic name."},
|
||||
{"key", reinterpret_cast<PyCFunction>(PyMessageGetKey), METH_NOARGS, "Get message key."},
|
||||
{"timestamp", reinterpret_cast<PyCFunction>(PyMessageGetTimestamp), METH_NOARGS, "Get message timestamp."},
|
||||
{"offset", reinterpret_cast<PyCFunction>(PyMessageGetOffset), METH_NOARGS, "Get message offset."},
|
||||
{nullptr},
|
||||
};
|
||||
|
||||
@@ -1938,18 +1905,6 @@ struct PyMgpError {
|
||||
const char *docstring;
|
||||
};
|
||||
|
||||
bool AddModuleConstants(PyObject &module) {
|
||||
// add source type constants
|
||||
if (PyModule_AddIntConstant(&module, "SOURCE_TYPE_KAFKA", static_cast<int64_t>(mgp_source_type::KAFKA))) {
|
||||
return false;
|
||||
}
|
||||
if (PyModule_AddIntConstant(&module, "SOURCE_TYPE_PULSAR", static_cast<int64_t>(mgp_source_type::PULSAR))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PyObject *PyInitMgpModule() {
|
||||
PyObject *mgp = PyModule_Create(&PyMgpModule);
|
||||
if (!mgp) return nullptr;
|
||||
@@ -1966,9 +1921,6 @@ PyObject *PyInitMgpModule() {
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!AddModuleConstants(*mgp)) return nullptr;
|
||||
|
||||
if (!register_type(&PyPropertiesIteratorType, "PropertiesIterator")) return nullptr;
|
||||
if (!register_type(&PyVerticesIteratorType, "VerticesIterator")) return nullptr;
|
||||
if (!register_type(&PyEdgesIteratorType, "EdgesIterator")) return nullptr;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
#include <json/json.hpp>
|
||||
|
||||
namespace query::stream {
|
||||
namespace query {
|
||||
namespace {
|
||||
const std::string kBatchIntervalKey{"batch_interval"};
|
||||
const std::string kBatchSizeKey{"batch_size"};
|
||||
@@ -21,25 +21,35 @@ const std::string kTransformationName{"transformation_name"};
|
||||
} // namespace
|
||||
|
||||
void to_json(nlohmann::json &data, CommonStreamInfo &&common_info) {
|
||||
data[kBatchIntervalKey] = common_info.batch_interval.count();
|
||||
data[kBatchSizeKey] = common_info.batch_size;
|
||||
if (common_info.batch_interval) {
|
||||
data[kBatchIntervalKey] = common_info.batch_interval->count();
|
||||
} else {
|
||||
data[kBatchIntervalKey] = nullptr;
|
||||
}
|
||||
|
||||
if (common_info.batch_size) {
|
||||
data[kBatchSizeKey] = *common_info.batch_size;
|
||||
} else {
|
||||
data[kBatchSizeKey] = nullptr;
|
||||
}
|
||||
|
||||
data[kTransformationName] = common_info.transformation_name;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json &data, CommonStreamInfo &common_info) {
|
||||
if (const auto batch_interval = data.at(kBatchIntervalKey); !batch_interval.is_null()) {
|
||||
using BatchInterval = decltype(common_info.batch_interval);
|
||||
using BatchInterval = typename decltype(common_info.batch_interval)::value_type;
|
||||
common_info.batch_interval = BatchInterval{batch_interval.get<typename BatchInterval::rep>()};
|
||||
} else {
|
||||
common_info.batch_interval = kDefaultBatchInterval;
|
||||
common_info.batch_interval = {};
|
||||
}
|
||||
|
||||
if (const auto batch_size = data.at(kBatchSizeKey); !batch_size.is_null()) {
|
||||
common_info.batch_size = batch_size.get<decltype(common_info.batch_size)>();
|
||||
common_info.batch_size = batch_size.get<typename decltype(common_info.batch_size)::value_type>();
|
||||
} else {
|
||||
common_info.batch_size = kDefaultBatchSize;
|
||||
common_info.batch_size = {};
|
||||
}
|
||||
|
||||
data.at(kTransformationName).get_to(common_info.transformation_name);
|
||||
}
|
||||
} // namespace query::stream
|
||||
} // namespace query
|
||||
|
||||
@@ -21,17 +21,14 @@
|
||||
|
||||
#include "query/procedure/mg_procedure_impl.hpp"
|
||||
|
||||
namespace query::stream {
|
||||
|
||||
constexpr std::chrono::milliseconds kDefaultBatchInterval{100};
|
||||
constexpr int64_t kDefaultBatchSize{1000};
|
||||
namespace query {
|
||||
|
||||
template <typename TMessage>
|
||||
using ConsumerFunction = std::function<void(const std::vector<TMessage> &)>;
|
||||
|
||||
struct CommonStreamInfo {
|
||||
std::chrono::milliseconds batch_interval;
|
||||
int64_t batch_size;
|
||||
std::optional<std::chrono::milliseconds> batch_interval;
|
||||
std::optional<int64_t> batch_size;
|
||||
std::string transformation_name;
|
||||
};
|
||||
|
||||
@@ -66,15 +63,6 @@ concept Stream = requires(TStream stream) {
|
||||
|
||||
enum class StreamSourceType : uint8_t { KAFKA, PULSAR };
|
||||
|
||||
constexpr std::string_view StreamSourceTypeToString(StreamSourceType type) {
|
||||
switch (type) {
|
||||
case StreamSourceType::KAFKA:
|
||||
return "kafka";
|
||||
case StreamSourceType::PULSAR:
|
||||
return "pulsar";
|
||||
}
|
||||
}
|
||||
|
||||
template <Stream T>
|
||||
StreamSourceType StreamType(const T & /*stream*/);
|
||||
|
||||
@@ -82,4 +70,4 @@ const std::string kCommonInfoKey = "common_info";
|
||||
|
||||
void to_json(nlohmann::json &data, CommonStreamInfo &&info);
|
||||
void from_json(const nlohmann::json &data, CommonStreamInfo &common_info);
|
||||
} // namespace query::stream
|
||||
} // namespace query
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
|
||||
#include <json/json.hpp>
|
||||
|
||||
namespace query::stream {
|
||||
namespace query {
|
||||
KafkaStream::KafkaStream(std::string stream_name, StreamInfo stream_info,
|
||||
ConsumerFunction<integrations::kafka::Message> consumer_function) {
|
||||
integrations::kafka::ConsumerInfo consumer_info{
|
||||
@@ -46,10 +46,6 @@ void KafkaStream::Check(std::optional<std::chrono::milliseconds> timeout, std::o
|
||||
consumer_->Check(timeout, batch_limit, consumer_function);
|
||||
}
|
||||
|
||||
utils::BasicResult<std::string> KafkaStream::SetStreamOffset(const int64_t offset) {
|
||||
return consumer_->SetConsumerOffsets(offset);
|
||||
}
|
||||
|
||||
namespace {
|
||||
const std::string kTopicsKey{"topics"};
|
||||
const std::string kConsumerGroupKey{"consumer_group"};
|
||||
@@ -114,4 +110,4 @@ void from_json(const nlohmann::json &data, PulsarStream::StreamInfo &info) {
|
||||
data.at(kTopicsKey).get_to(info.topics);
|
||||
data.at(kServiceUrl).get_to(info.service_url);
|
||||
}
|
||||
} // namespace query::stream
|
||||
} // namespace query
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
#include "integrations/kafka/consumer.hpp"
|
||||
#include "integrations/pulsar/consumer.hpp"
|
||||
|
||||
namespace query::stream {
|
||||
namespace query {
|
||||
|
||||
struct KafkaStream {
|
||||
struct StreamInfo {
|
||||
@@ -40,8 +40,6 @@ struct KafkaStream {
|
||||
void Check(std::optional<std::chrono::milliseconds> timeout, std::optional<int64_t> batch_limit,
|
||||
const ConsumerFunction<Message> &consumer_function) const;
|
||||
|
||||
utils::BasicResult<std::string> SetStreamOffset(int64_t offset);
|
||||
|
||||
private:
|
||||
using Consumer = integrations::kafka::Consumer;
|
||||
std::optional<Consumer> consumer_;
|
||||
@@ -88,4 +86,4 @@ inline StreamSourceType StreamType(const PulsarStream & /*stream*/) {
|
||||
return StreamSourceType::PULSAR;
|
||||
}
|
||||
|
||||
} // namespace query::stream
|
||||
} // namespace query
|
||||
|
||||
@@ -18,27 +18,22 @@
|
||||
#include <spdlog/spdlog.h>
|
||||
#include <json/json.hpp>
|
||||
|
||||
#include "mg_procedure.h"
|
||||
#include "query/db_accessor.hpp"
|
||||
#include "query/discard_value_stream.hpp"
|
||||
#include "query/exceptions.hpp"
|
||||
#include "query/interpreter.hpp"
|
||||
#include "query/procedure/mg_procedure_helpers.hpp"
|
||||
#include "query/procedure/mg_procedure_impl.hpp"
|
||||
#include "query/procedure/module.hpp"
|
||||
#include "query/stream/sources.hpp"
|
||||
#include "query/typed_value.hpp"
|
||||
#include "utils/event_counter.hpp"
|
||||
#include "utils/memory.hpp"
|
||||
#include "utils/on_scope_exit.hpp"
|
||||
#include "utils/pmr/string.hpp"
|
||||
#include "utils/variant_helpers.hpp"
|
||||
|
||||
namespace EventCounter {
|
||||
extern const Event MessagesConsumed;
|
||||
} // namespace EventCounter
|
||||
|
||||
namespace query::stream {
|
||||
namespace query {
|
||||
namespace {
|
||||
constexpr auto kExpectedTransformationResultSize = 2;
|
||||
const utils::pmr::string query_param_name{"query", utils::NewDeleteResource()};
|
||||
@@ -158,282 +153,8 @@ void from_json(const nlohmann::json &data, StreamStatus<TStream> &status) {
|
||||
from_json(data, status.info);
|
||||
}
|
||||
|
||||
namespace {
|
||||
template <typename Fun>
|
||||
[[nodiscard]] bool TryOrSetError(Fun &&func, mgp_result *result) {
|
||||
if (const auto err = func(); err == MGP_ERROR_UNABLE_TO_ALLOCATE) {
|
||||
static_cast<void>(mgp_result_set_error_msg(result, "Not enough memory!"));
|
||||
return false;
|
||||
} else if (err != MGP_ERROR_NO_ERROR) {
|
||||
const auto error_msg = fmt::format("Unexpected error ({})!", err);
|
||||
static_cast<void>(mgp_result_set_error_msg(result, error_msg.c_str()));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] auto GetStringValueOrSetError(const char *string, mgp_memory *memory, mgp_result *result) {
|
||||
procedure::MgpUniquePtr<mgp_value> value{nullptr, mgp_value_destroy};
|
||||
const auto success =
|
||||
TryOrSetError([&] { return procedure::CreateMgpObject(value, mgp_value_make_string, string, memory); }, result);
|
||||
if (!success) {
|
||||
value.reset();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool InsertResultOrSetError(mgp_result *result, mgp_result_record *record, const auto *result_name,
|
||||
mgp_value *value) {
|
||||
if (const auto err = mgp_result_record_insert(record, result_name, value); err != MGP_ERROR_NO_ERROR) {
|
||||
const auto error_msg = fmt::format("Unable to set the result for {}, error = {}", result_name, err);
|
||||
static_cast<void>(mgp_result_set_error_msg(result, error_msg.c_str()));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Streams::Streams(InterpreterContext *interpreter_context, std::filesystem::path directory)
|
||||
: interpreter_context_(interpreter_context), storage_(std::move(directory)) {
|
||||
RegisterProcedures();
|
||||
}
|
||||
|
||||
void Streams::RegisterProcedures() {
|
||||
RegisterKafkaProcedures();
|
||||
RegisterPulsarProcedures();
|
||||
}
|
||||
|
||||
void Streams::RegisterKafkaProcedures() {
|
||||
{
|
||||
constexpr std::string_view proc_name = "kafka_set_stream_offset";
|
||||
auto set_stream_offset = [this, proc_name](mgp_list *args, mgp_graph * /*graph*/, mgp_result *result,
|
||||
mgp_memory * /*memory*/) {
|
||||
auto *arg_stream_name = procedure::Call<mgp_value *>(mgp_list_at, args, 0);
|
||||
const auto *stream_name = procedure::Call<const char *>(mgp_value_get_string, arg_stream_name);
|
||||
auto *arg_offset = procedure::Call<mgp_value *>(mgp_list_at, args, 1);
|
||||
const auto offset = procedure::Call<int64_t>(mgp_value_get_int, arg_offset);
|
||||
auto lock_ptr = streams_.Lock();
|
||||
auto it = GetStream(*lock_ptr, std::string(stream_name));
|
||||
std::visit(utils::Overloaded{
|
||||
[&](StreamData<KafkaStream> &kafka_stream) {
|
||||
auto stream_source_ptr = kafka_stream.stream_source->Lock();
|
||||
const auto error = stream_source_ptr->SetStreamOffset(offset);
|
||||
if (error.HasError()) {
|
||||
MG_ASSERT(mgp_result_set_error_msg(result, error.GetError().c_str()) == MGP_ERROR_NO_ERROR,
|
||||
"Unable to set procedure error message of procedure: {}", proc_name);
|
||||
}
|
||||
},
|
||||
[proc_name](auto && /*other*/) {
|
||||
throw QueryRuntimeException("'{}' can be only used for Kafka stream sources", proc_name);
|
||||
}},
|
||||
it->second);
|
||||
};
|
||||
|
||||
mgp_proc proc(proc_name, set_stream_offset, utils::NewDeleteResource(), false);
|
||||
MG_ASSERT(mgp_proc_add_arg(&proc, "stream_name", procedure::Call<mgp_type *>(mgp_type_string)) ==
|
||||
MGP_ERROR_NO_ERROR);
|
||||
MG_ASSERT(mgp_proc_add_arg(&proc, "offset", procedure::Call<mgp_type *>(mgp_type_int)) == MGP_ERROR_NO_ERROR);
|
||||
|
||||
procedure::gModuleRegistry.RegisterMgProcedure(proc_name, std::move(proc));
|
||||
}
|
||||
|
||||
{
|
||||
constexpr std::string_view proc_name = "kafka_stream_info";
|
||||
|
||||
constexpr std::string_view consumer_group_result_name = "consumer_group";
|
||||
constexpr std::string_view topics_result_name = "topics";
|
||||
constexpr std::string_view bootstrap_servers_result_name = "bootstrap_servers";
|
||||
|
||||
auto get_stream_info = [this, proc_name, consumer_group_result_name, topics_result_name,
|
||||
bootstrap_servers_result_name](mgp_list *args, mgp_graph * /*graph*/, mgp_result *result,
|
||||
mgp_memory *memory) {
|
||||
auto *arg_stream_name = procedure::Call<mgp_value *>(mgp_list_at, args, 0);
|
||||
const auto *stream_name = procedure::Call<const char *>(mgp_value_get_string, arg_stream_name);
|
||||
auto lock_ptr = streams_.Lock();
|
||||
auto it = GetStream(*lock_ptr, std::string(stream_name));
|
||||
std::visit(
|
||||
utils::Overloaded{
|
||||
[&](StreamData<KafkaStream> &kafka_stream) {
|
||||
auto stream_source_ptr = kafka_stream.stream_source->Lock();
|
||||
const auto info = stream_source_ptr->Info(kafka_stream.transformation_name);
|
||||
mgp_result_record *record{nullptr};
|
||||
{
|
||||
const auto success = TryOrSetError([&] { return mgp_result_new_record(result, &record); }, result);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto consumer_group_value = GetStringValueOrSetError(info.consumer_group.c_str(), memory, result);
|
||||
if (!consumer_group_value) {
|
||||
return;
|
||||
}
|
||||
|
||||
procedure::MgpUniquePtr<mgp_list> topic_names{nullptr, mgp_list_destroy};
|
||||
{
|
||||
const auto success = TryOrSetError(
|
||||
[&] {
|
||||
return procedure::CreateMgpObject(topic_names, mgp_list_make_empty, info.topics.size(), memory);
|
||||
},
|
||||
result);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &topic : info.topics) {
|
||||
auto topic_value = GetStringValueOrSetError(topic.c_str(), memory, result);
|
||||
if (!topic_value) {
|
||||
return;
|
||||
}
|
||||
topic_names->elems.push_back(std::move(*topic_value));
|
||||
}
|
||||
|
||||
procedure::MgpUniquePtr<mgp_value> topics_value{nullptr, mgp_value_destroy};
|
||||
{
|
||||
const auto success = TryOrSetError(
|
||||
[&] {
|
||||
return procedure::CreateMgpObject(topics_value, mgp_value_make_list, topic_names.release());
|
||||
},
|
||||
result);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto bootstrap_servers_value =
|
||||
GetStringValueOrSetError(info.bootstrap_servers.c_str(), memory, result);
|
||||
if (!bootstrap_servers_value) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InsertResultOrSetError(result, record, consumer_group_result_name.data(),
|
||||
consumer_group_value.get())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InsertResultOrSetError(result, record, topics_result_name.data(), topics_value.get())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InsertResultOrSetError(result, record, bootstrap_servers_result_name.data(),
|
||||
bootstrap_servers_value.get())) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
[proc_name](auto && /*other*/) {
|
||||
throw QueryRuntimeException("'{}' can be only used for Kafka stream sources", proc_name);
|
||||
}},
|
||||
it->second);
|
||||
};
|
||||
|
||||
mgp_proc proc(proc_name, get_stream_info, utils::NewDeleteResource(), false);
|
||||
MG_ASSERT(mgp_proc_add_arg(&proc, "stream_name", procedure::Call<mgp_type *>(mgp_type_string)) ==
|
||||
MGP_ERROR_NO_ERROR);
|
||||
MG_ASSERT(mgp_proc_add_result(&proc, consumer_group_result_name.data(),
|
||||
procedure::Call<mgp_type *>(mgp_type_string)) == MGP_ERROR_NO_ERROR);
|
||||
MG_ASSERT(
|
||||
mgp_proc_add_result(&proc, topics_result_name.data(),
|
||||
procedure::Call<mgp_type *>(mgp_type_list, procedure::Call<mgp_type *>(mgp_type_string))) ==
|
||||
MGP_ERROR_NO_ERROR);
|
||||
MG_ASSERT(mgp_proc_add_result(&proc, bootstrap_servers_result_name.data(),
|
||||
procedure::Call<mgp_type *>(mgp_type_string)) == MGP_ERROR_NO_ERROR);
|
||||
|
||||
procedure::gModuleRegistry.RegisterMgProcedure(proc_name, std::move(proc));
|
||||
}
|
||||
}
|
||||
|
||||
void Streams::RegisterPulsarProcedures() {
|
||||
{
|
||||
constexpr std::string_view proc_name = "pulsar_stream_info";
|
||||
constexpr std::string_view service_url_result_name = "service_url";
|
||||
constexpr std::string_view topics_result_name = "topics";
|
||||
auto get_stream_info = [this, proc_name, service_url_result_name, topics_result_name](
|
||||
mgp_list *args, mgp_graph * /*graph*/, mgp_result *result, mgp_memory *memory) {
|
||||
auto *arg_stream_name = procedure::Call<mgp_value *>(mgp_list_at, args, 0);
|
||||
const auto *stream_name = procedure::Call<const char *>(mgp_value_get_string, arg_stream_name);
|
||||
auto lock_ptr = streams_.Lock();
|
||||
auto it = GetStream(*lock_ptr, std::string(stream_name));
|
||||
std::visit(
|
||||
utils::Overloaded{
|
||||
[&](StreamData<PulsarStream> &pulsar_stream) {
|
||||
auto stream_source_ptr = pulsar_stream.stream_source->Lock();
|
||||
const auto info = stream_source_ptr->Info(pulsar_stream.transformation_name);
|
||||
mgp_result_record *record{nullptr};
|
||||
{
|
||||
const auto success = TryOrSetError([&] { return mgp_result_new_record(result, &record); }, result);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
auto service_url_value = GetStringValueOrSetError(info.service_url.c_str(), memory, result);
|
||||
if (!service_url_value) {
|
||||
return;
|
||||
}
|
||||
|
||||
procedure::MgpUniquePtr<mgp_list> topic_names{nullptr, mgp_list_destroy};
|
||||
{
|
||||
const auto success = TryOrSetError(
|
||||
[&] {
|
||||
return procedure::CreateMgpObject(topic_names, mgp_list_make_empty, info.topics.size(), memory);
|
||||
},
|
||||
result);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &topic : info.topics) {
|
||||
auto topic_value = GetStringValueOrSetError(topic.c_str(), memory, result);
|
||||
if (!topic_value) {
|
||||
return;
|
||||
}
|
||||
topic_names->elems.push_back(std::move(*topic_value));
|
||||
}
|
||||
|
||||
procedure::MgpUniquePtr<mgp_value> topics_value{nullptr, mgp_value_destroy};
|
||||
{
|
||||
const auto success = TryOrSetError(
|
||||
[&] {
|
||||
return procedure::CreateMgpObject(topics_value, mgp_value_make_list, topic_names.release());
|
||||
},
|
||||
result);
|
||||
if (!success) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!InsertResultOrSetError(result, record, topics_result_name.data(), topics_value.get())) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!InsertResultOrSetError(result, record, service_url_result_name.data(), service_url_value.get())) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
[proc_name](auto && /*other*/) {
|
||||
throw QueryRuntimeException("'{}' can be only used for Pulsar stream sources", proc_name);
|
||||
}},
|
||||
it->second);
|
||||
};
|
||||
|
||||
mgp_proc proc(proc_name, get_stream_info, utils::NewDeleteResource(), false);
|
||||
MG_ASSERT(mgp_proc_add_arg(&proc, "stream_name", procedure::Call<mgp_type *>(mgp_type_string)) ==
|
||||
MGP_ERROR_NO_ERROR);
|
||||
MG_ASSERT(mgp_proc_add_result(&proc, service_url_result_name.data(),
|
||||
procedure::Call<mgp_type *>(mgp_type_string)) == MGP_ERROR_NO_ERROR);
|
||||
|
||||
MG_ASSERT(
|
||||
mgp_proc_add_result(&proc, topics_result_name.data(),
|
||||
procedure::Call<mgp_type *>(mgp_type_list, procedure::Call<mgp_type *>(mgp_type_string))) ==
|
||||
MGP_ERROR_NO_ERROR);
|
||||
|
||||
procedure::gModuleRegistry.RegisterMgProcedure(proc_name, std::move(proc));
|
||||
}
|
||||
}
|
||||
: interpreter_context_(interpreter_context), storage_(std::move(directory)) {}
|
||||
|
||||
template <Stream TStream>
|
||||
void Streams::Create(const std::string &stream_name, typename TStream::StreamInfo info,
|
||||
@@ -469,30 +190,26 @@ Streams::StreamsMap::iterator Streams::CreateConsumer(StreamsMap &map, const std
|
||||
|
||||
auto *memory_resource = utils::NewDeleteResource();
|
||||
|
||||
auto consumer_function = [interpreter_context = interpreter_context_, memory_resource, stream_name,
|
||||
transformation_name = stream_info.common_info.transformation_name, owner = owner,
|
||||
interpreter = std::make_shared<Interpreter>(interpreter_context_),
|
||||
result = mgp_result{nullptr, memory_resource},
|
||||
total_retries = interpreter_context_->config.stream_transaction_conflict_retries,
|
||||
retry_interval = interpreter_context_->config.stream_transaction_retry_interval](
|
||||
const std::vector<typename TStream::Message> &messages) mutable {
|
||||
auto accessor = interpreter_context->db->Access();
|
||||
EventCounter::IncrementCounter(EventCounter::MessagesConsumed, messages.size());
|
||||
CallCustomTransformation(transformation_name, messages, result, accessor, *memory_resource, stream_name);
|
||||
auto consumer_function =
|
||||
[interpreter_context = interpreter_context_, memory_resource, stream_name,
|
||||
transformation_name = stream_info.common_info.transformation_name, owner = owner,
|
||||
interpreter = std::make_shared<Interpreter>(interpreter_context_),
|
||||
result = mgp_result{nullptr, memory_resource}](const std::vector<typename TStream::Message> &messages) mutable {
|
||||
auto accessor = interpreter_context->db->Access();
|
||||
EventCounter::IncrementCounter(EventCounter::MessagesConsumed, messages.size());
|
||||
CallCustomTransformation(transformation_name, messages, result, accessor, *memory_resource, stream_name);
|
||||
|
||||
DiscardValueResultStream stream;
|
||||
DiscardValueResultStream stream;
|
||||
|
||||
spdlog::trace("Start transaction in stream '{}'", stream_name);
|
||||
utils::OnScopeExit cleanup{[&interpreter, &result]() {
|
||||
result.rows.clear();
|
||||
interpreter->Abort();
|
||||
}};
|
||||
|
||||
const static std::map<std::string, storage::PropertyValue> empty_parameters{};
|
||||
uint32_t i = 0;
|
||||
while (true) {
|
||||
try {
|
||||
spdlog::trace("Start transaction in stream '{}'", stream_name);
|
||||
utils::OnScopeExit cleanup{[&interpreter, &result]() {
|
||||
result.rows.clear();
|
||||
interpreter->Abort();
|
||||
}};
|
||||
interpreter->BeginTransaction();
|
||||
|
||||
const static std::map<std::string, storage::PropertyValue> empty_parameters{};
|
||||
|
||||
for (auto &row : result.rows) {
|
||||
spdlog::trace("Processing row in stream '{}'", stream_name);
|
||||
auto [query_value, params_value] =
|
||||
@@ -505,7 +222,7 @@ Streams::StreamsMap::iterator Streams::CreateConsumer(StreamsMap &map, const std
|
||||
interpreter->Prepare(query, params_prop.IsNull() ? empty_parameters : params_prop.ValueMap(), nullptr);
|
||||
if (!interpreter_context->auth_checker->IsUserAuthorized(owner, prepare_result.privileges)) {
|
||||
throw StreamsException{
|
||||
"Couldn't execute query '{}' for stream '{}' because the owner is not authorized to execute the "
|
||||
"Couldn't execute query '{}' for stream '{}' becuase the owner is not authorized to execute the "
|
||||
"query!",
|
||||
query, stream_name};
|
||||
}
|
||||
@@ -515,16 +232,7 @@ Streams::StreamsMap::iterator Streams::CreateConsumer(StreamsMap &map, const std
|
||||
spdlog::trace("Commit transaction in stream '{}'", stream_name);
|
||||
interpreter->CommitTransaction();
|
||||
result.rows.clear();
|
||||
break;
|
||||
} catch (const query::TransactionSerializationException &e) {
|
||||
if (i == total_retries) {
|
||||
throw;
|
||||
}
|
||||
++i;
|
||||
std::this_thread::sleep_for(retry_interval);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
auto insert_result = map.try_emplace(
|
||||
stream_name, StreamData<TStream>{std::move(stream_info.common_info.transformation_name), std::move(owner),
|
||||
@@ -575,22 +283,15 @@ void Streams::RestoreStreams() {
|
||||
};
|
||||
|
||||
auto stream_json_data = nlohmann::json::parse(stream_data);
|
||||
if (const auto it = stream_json_data.find(kType); it != stream_json_data.end()) {
|
||||
const auto stream_type = static_cast<StreamSourceType>(*it);
|
||||
switch (stream_type) {
|
||||
case StreamSourceType::KAFKA:
|
||||
create_consumer(StreamStatus<KafkaStream>{}, std::move(stream_json_data));
|
||||
break;
|
||||
case StreamSourceType::PULSAR:
|
||||
create_consumer(StreamStatus<PulsarStream>{}, std::move(stream_json_data));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
spdlog::warn(
|
||||
"Unable to load stream '{}', because it does not contain the type of the stream. Most probably the stream "
|
||||
"was saved before Memgraph 2.1. Please recreate the stream manually to make it work. For more information "
|
||||
"please check https://memgraph.com/docs/memgraph/changelog#v210---nov-22-2021 .",
|
||||
stream_json_data.value(kStreamName, "<invalid format>"));
|
||||
const auto stream_type = static_cast<StreamSourceType>(stream_json_data.at("type"));
|
||||
|
||||
switch (stream_type) {
|
||||
case StreamSourceType::KAFKA:
|
||||
create_consumer(StreamStatus<KafkaStream>{}, std::move(stream_json_data));
|
||||
break;
|
||||
case StreamSourceType::PULSAR:
|
||||
create_consumer(StreamStatus<PulsarStream>{}, std::move(stream_json_data));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -730,4 +431,4 @@ TransformationResult Streams::Check(const std::string &stream_name, std::optiona
|
||||
it->second);
|
||||
}
|
||||
|
||||
} // namespace query::stream
|
||||
} // namespace query
|
||||
|
||||
@@ -33,10 +33,6 @@
|
||||
|
||||
namespace query {
|
||||
|
||||
struct InterpreterContext;
|
||||
|
||||
namespace stream {
|
||||
|
||||
class StreamsException : public utils::BasicException {
|
||||
public:
|
||||
using BasicException::BasicException;
|
||||
@@ -69,6 +65,8 @@ struct StreamStatus {
|
||||
|
||||
using TransformationResult = std::vector<std::vector<TypedValue>>;
|
||||
|
||||
struct InterpreterContext;
|
||||
|
||||
/// Manages Kafka consumers.
|
||||
///
|
||||
/// This class is responsible for all query supported actions to happen.
|
||||
@@ -180,15 +178,10 @@ class Streams final {
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterProcedures();
|
||||
void RegisterKafkaProcedures();
|
||||
void RegisterPulsarProcedures();
|
||||
|
||||
InterpreterContext *interpreter_context_;
|
||||
kvstore::KVStore storage_;
|
||||
|
||||
SynchronizedStreamsMap streams_;
|
||||
};
|
||||
|
||||
} // namespace stream
|
||||
} // namespace query
|
||||
|
||||
@@ -4,5 +4,5 @@ set(requests_src_files
|
||||
find_package(CURL REQUIRED)
|
||||
|
||||
add_library(mg-requests STATIC ${requests_src_files})
|
||||
target_link_libraries(mg-requests mg-utils spdlog::spdlog fmt::fmt gflags json ${CURL_LIBRARIES})
|
||||
target_link_libraries(mg-requests spdlog fmt gflags json ${CURL_LIBRARIES})
|
||||
target_include_directories(mg-requests PRIVATE ${CURL_INCLUDE_DIRS})
|
||||
|
||||
@@ -4,5 +4,5 @@ set(rpc_src_files
|
||||
server.cpp)
|
||||
|
||||
add_library(mg-rpc STATIC ${rpc_src_files})
|
||||
target_link_libraries(mg-rpc Threads::Threads mg-communication mg-utils mg-io fmt::fmt gflags)
|
||||
target_link_libraries(mg-rpc Threads::Threads mg-communication mg-utils mg-io fmt gflags)
|
||||
target_link_libraries(mg-rpc mg-slk)
|
||||
|
||||
@@ -60,7 +60,6 @@ class LabelIndex {
|
||||
/// @throw std::bad_alloc
|
||||
bool CreateIndex(LabelId label, utils::SkipList<Vertex>::Accessor vertices);
|
||||
|
||||
/// Returns false if there was no index to drop
|
||||
bool DropIndex(LabelId label) { return index_.erase(label) > 0; }
|
||||
|
||||
bool IndexExists(LabelId label) const { return index_.find(label) != index_.end(); }
|
||||
|
||||
@@ -5,9 +5,11 @@ set(utils_src_files
|
||||
csv_parsing.cpp
|
||||
file.cpp
|
||||
file_locker.cpp
|
||||
license.cpp
|
||||
memory.cpp
|
||||
memory_tracker.cpp
|
||||
readable_size.cpp
|
||||
settings.cpp
|
||||
signals.cpp
|
||||
sysinfo/memory.cpp
|
||||
temporal.cpp
|
||||
@@ -15,20 +17,5 @@ set(utils_src_files
|
||||
thread_pool.cpp
|
||||
uuid.cpp)
|
||||
|
||||
find_package(Boost REQUIRED)
|
||||
|
||||
add_library(mg-utils STATIC ${utils_src_files})
|
||||
target_link_libraries(mg-utils PUBLIC Boost::headers fmt::fmt spdlog::spdlog)
|
||||
target_link_libraries(mg-utils PRIVATE stdc++fs Threads::Threads gflags uuid rt)
|
||||
|
||||
set(settings_src_files
|
||||
settings.cpp)
|
||||
|
||||
add_library(mg-settings STATIC ${settings_src_files})
|
||||
target_link_libraries(mg-settings mg-kvstore mg-slk mg-utils)
|
||||
|
||||
set(license_src_files
|
||||
license.cpp)
|
||||
add_library(mg-license STATIC ${license_src_files})
|
||||
target_link_libraries(mg-license mg-settings mg-utils)
|
||||
|
||||
target_link_libraries(mg-utils mg-kvstore mg-slk stdc++fs Threads::Threads spdlog fmt gflags uuid rt)
|
||||
|
||||
@@ -45,7 +45,7 @@ std::optional<utils::pmr::string> Reader::GetNextLine(utils::MemoryResource *mem
|
||||
|
||||
Reader::ParsingResult Reader::ParseHeader() {
|
||||
// header must be the very first line in the file
|
||||
MG_ASSERT(line_count_ == 1, "Invalid use of {}", __func__);
|
||||
MG_ASSERT(line_count_ == 1, fmt::format("Invalid use of {}", __func__));
|
||||
return ParseRow(memory_);
|
||||
}
|
||||
|
||||
@@ -203,7 +203,7 @@ Reader::ParsingResult Reader::ParseRow(utils::MemoryResource *mem) {
|
||||
// parse the header.
|
||||
// Also, if we don't have a header, the 'number_of_columns_' will be 0, so no
|
||||
// need to check the number of columns.
|
||||
if (number_of_columns_ != 0 && row.size() != number_of_columns_) [[unlikely]] {
|
||||
if (UNLIKELY(number_of_columns_ != 0 && row.size() != number_of_columns_)) {
|
||||
return ParseError(ParseError::ErrorCode::BAD_NUM_OF_COLUMNS,
|
||||
// ToDo(the-joksim):
|
||||
// - 'line_count_ - 1' is the last line of a row (as a
|
||||
|
||||
@@ -38,11 +38,28 @@ namespace utils {
|
||||
*/
|
||||
class BasicException : public std::exception {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor (C strings).
|
||||
*
|
||||
* @param message C-style string error message.
|
||||
* The string contents are copied upon construction.
|
||||
* Hence, responsibility for deleting the `char*` lies
|
||||
* with the caller.
|
||||
*/
|
||||
explicit BasicException(const char *message) noexcept : msg_(message) {}
|
||||
|
||||
/**
|
||||
* @brief Constructor (C++ STL strings).
|
||||
*
|
||||
* @param message The error message.
|
||||
*/
|
||||
explicit BasicException(const std::string &message) noexcept : msg_(message) {}
|
||||
|
||||
/**
|
||||
* @brief Constructor (C++ STL string_view).
|
||||
*
|
||||
* @param message The error message.
|
||||
*/
|
||||
explicit BasicException(const std::string_view message) noexcept : msg_(message) {}
|
||||
|
||||
/**
|
||||
@@ -52,8 +69,20 @@ class BasicException : public std::exception {
|
||||
* @param args Arguments for format string.
|
||||
*/
|
||||
template <class... Args>
|
||||
explicit BasicException(fmt::format_string<Args...> fmt, Args &&...args) noexcept
|
||||
: msg_(fmt::format(fmt, std::forward<Args>(args)...)) {}
|
||||
explicit BasicException(const std::string &format, Args &&...args) noexcept
|
||||
: BasicException(fmt::format(format, std::forward<Args>(args)...)) {}
|
||||
|
||||
/**
|
||||
* @brief Constructor with format string (C strings).
|
||||
*
|
||||
* @param format The error format message. The string contents are copied upon
|
||||
* construction. Hence, the responsibility for deleting `char*` lies with the
|
||||
* caller.
|
||||
* @param args Arguments for format string.
|
||||
*/
|
||||
template <class... Args>
|
||||
explicit BasicException(const char *format, Args &&...args) noexcept
|
||||
: BasicException(fmt::format(std::string(format), std::forward<Args>(args)...)) {}
|
||||
|
||||
/**
|
||||
* @brief Virtual destructor to allow for subclassing.
|
||||
@@ -90,12 +119,22 @@ class BasicException : public std::exception {
|
||||
*/
|
||||
class StacktraceException : public std::exception {
|
||||
public:
|
||||
/**
|
||||
* @brief Constructor (C strings).
|
||||
*
|
||||
* @param message C-style string error message.
|
||||
* The string contents are copied upon construction.
|
||||
* Hence, responsibility for deleting the `char*` lies
|
||||
* with the caller.
|
||||
*/
|
||||
explicit StacktraceException(const char *message) noexcept : message_(message), stacktrace_(Stacktrace().dump()) {}
|
||||
|
||||
/**
|
||||
* @brief Constructor (C++ STL strings).
|
||||
*
|
||||
* @param message The error message.
|
||||
*/
|
||||
explicit StacktraceException(const std::string_view message) noexcept
|
||||
explicit StacktraceException(const std::string &message) noexcept
|
||||
: message_(message), stacktrace_(Stacktrace().dump()) {}
|
||||
|
||||
/**
|
||||
@@ -105,8 +144,20 @@ class StacktraceException : public std::exception {
|
||||
* @param args Arguments for format string.
|
||||
*/
|
||||
template <class... Args>
|
||||
explicit StacktraceException(fmt::format_string<Args...> fmt, Args &&...args) noexcept
|
||||
: StacktraceException(fmt::format(fmt, std::forward<Args>(args)...)) {}
|
||||
explicit StacktraceException(const std::string &format, Args &&...args) noexcept
|
||||
: StacktraceException(fmt::format(format, std::forward<Args>(args)...)) {}
|
||||
|
||||
/**
|
||||
* @brief Constructor with format string (C strings).
|
||||
*
|
||||
* @param format The error format message. The string contents are copied upon
|
||||
* construction. Hence, the responsibility for deleting `char*` lies with the
|
||||
* caller.
|
||||
* @param args Arguments for format string.
|
||||
*/
|
||||
template <class... Args>
|
||||
explicit StacktraceException(const char *format, Args &&...args) noexcept
|
||||
: StacktraceException(fmt::format(std::string(format), std::forward<Args>(args)...)) {}
|
||||
|
||||
/**
|
||||
* @brief Virtual destructor to allow for subclassing.
|
||||
@@ -144,8 +195,8 @@ class NotYetImplemented final : public BasicException {
|
||||
explicit NotYetImplemented(const std::string &what) noexcept : BasicException("Not yet implemented: " + what) {}
|
||||
|
||||
template <class... Args>
|
||||
explicit NotYetImplemented(fmt::format_string<Args...> fmt, Args &&...args) noexcept
|
||||
: NotYetImplemented(fmt::format(fmt, std::forward<Args>(args)...)) {}
|
||||
explicit NotYetImplemented(const std::string &format, Args &&...args) noexcept
|
||||
: NotYetImplemented(fmt::format(format, std::forward<Args>(args)...)) {}
|
||||
};
|
||||
|
||||
} // namespace utils
|
||||
|
||||
@@ -22,37 +22,49 @@
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <spdlog/fmt/ostr.h>
|
||||
#include <spdlog/sinks/stdout_color_sinks.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include <boost/preprocessor/comparison/equal.hpp>
|
||||
#include <boost/preprocessor/control/if.hpp>
|
||||
#include <boost/preprocessor/variadic/size.hpp>
|
||||
#include "utils/likely.hpp"
|
||||
|
||||
namespace logging {
|
||||
|
||||
#ifndef NDEBUG
|
||||
// TODO (antonio2368): Replace with std::source_location when it's supported by
|
||||
// compilers
|
||||
inline void AssertFailed(const char *file_name, int line_num, const char *expr, const std::string &message) {
|
||||
template <typename... Args>
|
||||
void AssertFailed(const char *file_name, int line_num, const char *expr, const Args &...msg_args) {
|
||||
std::optional<std::string> message;
|
||||
if constexpr (sizeof...(msg_args) > 0) {
|
||||
message.emplace(fmt::format(msg_args...));
|
||||
}
|
||||
|
||||
spdlog::critical(
|
||||
"\nAssertion failed in file {} at line {}."
|
||||
"\n\tExpression: '{}'"
|
||||
"{}",
|
||||
file_name, line_num, expr, !message.empty() ? fmt::format("\n\tMessage: '{}'", message) : "");
|
||||
file_name, line_num, expr, message ? fmt::format("\n\tMessage: '{}'", *message) : "");
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
#define GET_MESSAGE(...) \
|
||||
BOOST_PP_IF(BOOST_PP_EQUAL(BOOST_PP_VARIADIC_SIZE(__VA_ARGS__), 0), "", fmt::format(__VA_ARGS__))
|
||||
|
||||
// TODO (antonio2368): Replace with attribute [[likely]] when it's supported by
|
||||
// compilers
|
||||
#define MG_ASSERT(expr, ...) \
|
||||
[[likely]] !!(expr) ? (void)0 : ::logging::AssertFailed(__FILE__, __LINE__, #expr, GET_MESSAGE(__VA_ARGS__))
|
||||
|
||||
#ifndef NDEBUG
|
||||
LIKELY(!!(expr)) \
|
||||
? (void)0 : ::logging::AssertFailed(__FILE__, __LINE__, #expr, ##__VA_ARGS__)
|
||||
#define DMG_ASSERT(expr, ...) MG_ASSERT(expr, __VA_ARGS__)
|
||||
#else
|
||||
template <typename... Args>
|
||||
void AssertFailed(const Args &...msg_args) {
|
||||
if constexpr (sizeof...(msg_args) > 0) {
|
||||
spdlog::critical("Assertion failed with message: '{}'", fmt::format(msg_args...).c_str());
|
||||
} else {
|
||||
spdlog::critical("Assertion failed");
|
||||
}
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
#define MG_ASSERT(expr, ...) LIKELY(!!(expr)) ? (void)0 : ::logging::AssertFailed(__VA_ARGS__)
|
||||
#define DMG_ASSERT(...)
|
||||
#endif
|
||||
|
||||
|
||||
@@ -16,9 +16,8 @@
|
||||
namespace utils {
|
||||
|
||||
template <typename... Args>
|
||||
std::string MessageWithLink(fmt::format_string<Args...> fmt, Args &&...args) {
|
||||
return fmt::format(fmt::runtime(fmt::format(fmt::runtime("{} For more details, visit {{}}."), fmt)),
|
||||
std::forward<Args>(args)...);
|
||||
std::string MessageWithLink(const std::string_view format, Args &&...args) {
|
||||
return fmt::format(fmt::format("{} For more details, visit {{}}.", format), std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
} // namespace utils
|
||||
|
||||
@@ -69,7 +69,7 @@ class Timestamp final {
|
||||
}
|
||||
|
||||
const std::string ToString(const std::string &format = fiso8601) const {
|
||||
return fmt::format(fmt::runtime(format), Year(), Month(), Day(), Hour(), Min(), Sec(), Usec());
|
||||
return fmt::format(format, Year(), Month(), Day(), Hour(), Min(), Sec(), Usec());
|
||||
}
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &stream, const Timestamp &ts) { return stream << ts.ToIso8601(); }
|
||||
|
||||
@@ -11,7 +11,7 @@ function(add_benchmark test_cpp)
|
||||
# used to help create two targets of the same name even though CMake
|
||||
# requires unique logical target names
|
||||
set_target_properties(${target_name} PROPERTIES OUTPUT_NAME ${exec_name})
|
||||
target_link_libraries(${target_name} benchmark gflags)
|
||||
target_link_libraries(${target_name} benchmark)
|
||||
# register test
|
||||
add_test(${target_name} ${exec_name})
|
||||
add_dependencies(memgraph__benchmark ${target_name})
|
||||
@@ -53,7 +53,7 @@ add_benchmark(skip_list_vs_stl.cpp)
|
||||
target_link_libraries(${test_prefix}skip_list_vs_stl mg-utils)
|
||||
|
||||
add_benchmark(expansion.cpp ${CMAKE_SOURCE_DIR}/src/glue/communication.cpp)
|
||||
target_link_libraries(${test_prefix}expansion mg-query mg-communication mg-license)
|
||||
target_link_libraries(${test_prefix}expansion mg-query mg-communication)
|
||||
|
||||
add_benchmark(storage_v2_gc.cpp)
|
||||
target_link_libraries(${test_prefix}storage_v2_gc mg-storage-v2)
|
||||
|
||||
@@ -27,7 +27,7 @@ add_concurrent_test(network_session_leak.cpp)
|
||||
target_link_libraries(${test_prefix}network_session_leak mg-communication)
|
||||
|
||||
add_concurrent_test(stack.cpp)
|
||||
target_link_libraries(${test_prefix}stack mg-utils gflags)
|
||||
target_link_libraries(${test_prefix}stack mg-utils)
|
||||
|
||||
add_concurrent_test(skip_list_insert.cpp)
|
||||
target_link_libraries(${test_prefix}skip_list_insert mg-utils)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<p>Check console for Cypher query outputs...</p>
|
||||
<script>
|
||||
const driver = neo4j.driver(
|
||||
"bolt://localhost:7687",
|
||||
"bolt://localhost:9999",
|
||||
neo4j.auth.basic("", ""),
|
||||
);
|
||||
|
||||
|
||||
@@ -4,8 +4,7 @@ endfunction()
|
||||
|
||||
copy_streams_e2e_python_files(common.py)
|
||||
copy_streams_e2e_python_files(conftest.py)
|
||||
copy_streams_e2e_python_files(kafka_streams_tests.py)
|
||||
copy_streams_e2e_python_files(streams_tests.py)
|
||||
copy_streams_e2e_python_files(streams_owner_tests.py)
|
||||
copy_streams_e2e_python_files(pulsar_streams_tests.py)
|
||||
|
||||
add_subdirectory(transformations)
|
||||
|
||||
@@ -12,24 +12,14 @@
|
||||
import mgclient
|
||||
import time
|
||||
|
||||
from multiprocessing import Process, Value
|
||||
|
||||
# These are the indices of the different values in the result of SHOW STREAM
|
||||
# query
|
||||
NAME = 0
|
||||
TYPE = 1
|
||||
BATCH_INTERVAL = 2
|
||||
BATCH_SIZE = 3
|
||||
TRANSFORM = 4
|
||||
OWNER = 5
|
||||
IS_RUNNING = 6
|
||||
|
||||
# These are the indices of the query and parameters in the result of CHECK
|
||||
# STREAM query
|
||||
QUERY = 0
|
||||
PARAMS = 1
|
||||
|
||||
SIMPLE_MSG = b"message"
|
||||
BATCH_INTERVAL = 1
|
||||
BATCH_SIZE = 2
|
||||
TRANSFORM = 3
|
||||
OWNER = 4
|
||||
IS_RUNNING = 5
|
||||
|
||||
|
||||
def execute_and_fetch_all(cursor, query):
|
||||
@@ -80,12 +70,12 @@ def check_one_result_row(cursor, query):
|
||||
return len(results) == 1
|
||||
|
||||
|
||||
def check_vertex_exists_with_properties(cursor, properties):
|
||||
properties_string = ', '.join([f'{k}: {v}' for k, v in properties.items()])
|
||||
def check_vertex_exists_with_topic_and_payload(cursor, topic, payload_bytes):
|
||||
assert check_one_result_row(
|
||||
cursor,
|
||||
"MATCH (n: MESSAGE {"
|
||||
f"{properties_string}"
|
||||
f"payload: '{payload_bytes.decode('utf-8')}',"
|
||||
f"topic: '{topic}'"
|
||||
"}) RETURN n",
|
||||
)
|
||||
|
||||
@@ -124,162 +114,8 @@ def drop_stream(cursor, stream_name):
|
||||
assert get_stream_info(cursor, stream_name) is None
|
||||
|
||||
|
||||
def validate_info(actual_stream_info, expected_stream_info):
|
||||
assert len(actual_stream_info) == len(expected_stream_info)
|
||||
for info, expected_info in zip(actual_stream_info, expected_stream_info):
|
||||
assert info == expected_info
|
||||
|
||||
def check_stream_info(cursor, stream_name, expected_stream_info):
|
||||
stream_info = get_stream_info(cursor, stream_name)
|
||||
validate_info(stream_info, expected_stream_info)
|
||||
|
||||
def kafka_check_vertex_exists_with_topic_and_payload(cursor, topic, payload_bytes):
|
||||
decoded_payload = payload_bytes.decode('utf-8')
|
||||
check_vertex_exists_with_properties(
|
||||
cursor, {'topic': f'"{topic}"', 'payload': f'"{decoded_payload}"'})
|
||||
|
||||
|
||||
PULSAR_SERVICE_URL = 'pulsar://127.0.0.1:6650'
|
||||
|
||||
def pulsar_default_namespace_topic(topic):
|
||||
return f'persistent://public/default/{topic}'
|
||||
|
||||
|
||||
def test_start_and_stop_during_check(
|
||||
operation,
|
||||
connection,
|
||||
stream_creator,
|
||||
message_sender,
|
||||
already_stopped_error):
|
||||
# This test is quite complex. The goal is to call START/STOP queries
|
||||
# while a CHECK query is waiting for its result. Because the Global
|
||||
# Interpreter Lock, running queries on multiple threads is not useful,
|
||||
# because only one of them can call Cursor::execute at a time. Therefore
|
||||
# multiple processes are used to execute the queries, because different
|
||||
# processes have different GILs.
|
||||
# The counter variables are thread- and process-safe variables to
|
||||
# synchronize between the different processes. Each value represents a
|
||||
# specific phase of the execution of the processes.
|
||||
assert operation in ["START", "STOP"]
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
stream_creator('test_stream')
|
||||
)
|
||||
|
||||
check_counter = Value("i", 0)
|
||||
check_result_len = Value("i", 0)
|
||||
operation_counter = Value("i", 0)
|
||||
|
||||
CHECK_BEFORE_EXECUTE = 1
|
||||
CHECK_AFTER_FETCHALL = 2
|
||||
CHECK_CORRECT_RESULT = 3
|
||||
CHECK_INCORRECT_RESULT = 4
|
||||
|
||||
def call_check(counter, result_len):
|
||||
# This process will call the CHECK query and increment the counter
|
||||
# based on its progress and expected behavior
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
counter.value = CHECK_BEFORE_EXECUTE
|
||||
result = execute_and_fetch_all(cursor, "CHECK STREAM test_stream")
|
||||
result_len.value = len(result)
|
||||
counter.value = CHECK_AFTER_FETCHALL
|
||||
if len(result) > 0 and "payload: 'message'" in result[0][QUERY]:
|
||||
counter.value = CHECK_CORRECT_RESULT
|
||||
else:
|
||||
counter.value = CHECK_INCORRECT_RESULT
|
||||
|
||||
OP_BEFORE_EXECUTE = 1
|
||||
OP_AFTER_FETCHALL = 2
|
||||
OP_ALREADY_STOPPED_EXCEPTION = 3
|
||||
OP_INCORRECT_ALREADY_STOPPED_EXCEPTION = 4
|
||||
OP_UNEXPECTED_EXCEPTION = 5
|
||||
|
||||
def call_operation(counter):
|
||||
# This porcess will call the query with the specified operation and
|
||||
# increment the counter based on its progress and expected behavior
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
counter.value = OP_BEFORE_EXECUTE
|
||||
try:
|
||||
execute_and_fetch_all(cursor, f"{operation} STREAM test_stream")
|
||||
counter.value = OP_AFTER_FETCHALL
|
||||
except mgclient.DatabaseError as e:
|
||||
if already_stopped_error in str(e):
|
||||
counter.value = OP_ALREADY_STOPPED_EXCEPTION
|
||||
else:
|
||||
counter.value = OP_INCORRECT_ALREADY_STOPPED_EXCEPTION
|
||||
except Exception:
|
||||
counter.value = OP_UNEXPECTED_EXCEPTION
|
||||
|
||||
check_stream_proc = Process(
|
||||
target=call_check, daemon=True, args=(check_counter, check_result_len)
|
||||
)
|
||||
operation_proc = Process(
|
||||
target=call_operation, daemon=True, args=(operation_counter,)
|
||||
)
|
||||
|
||||
try:
|
||||
check_stream_proc.start()
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
assert timed_wait(lambda: check_counter.value == CHECK_BEFORE_EXECUTE)
|
||||
assert timed_wait(lambda: get_is_running(cursor, "test_stream"))
|
||||
assert check_counter.value == CHECK_BEFORE_EXECUTE, (
|
||||
"SHOW STREAMS " "was blocked until the end of CHECK STREAM"
|
||||
)
|
||||
operation_proc.start()
|
||||
assert timed_wait(lambda: operation_counter.value == OP_BEFORE_EXECUTE)
|
||||
|
||||
message_sender(SIMPLE_MSG)
|
||||
assert timed_wait(lambda: check_counter.value > CHECK_AFTER_FETCHALL)
|
||||
assert check_counter.value == CHECK_CORRECT_RESULT
|
||||
assert check_result_len.value == 1
|
||||
check_stream_proc.join()
|
||||
|
||||
operation_proc.join()
|
||||
if operation == "START":
|
||||
assert operation_counter.value == OP_AFTER_FETCHALL
|
||||
assert get_is_running(cursor, "test_stream")
|
||||
else:
|
||||
assert operation_counter.value == OP_ALREADY_STOPPED_EXCEPTION
|
||||
assert not get_is_running(cursor, "test_stream")
|
||||
|
||||
finally:
|
||||
# to make sure CHECK STREAM finishes
|
||||
message_sender(SIMPLE_MSG)
|
||||
if check_stream_proc.is_alive():
|
||||
check_stream_proc.terminate()
|
||||
if operation_proc.is_alive():
|
||||
operation_proc.terminate()
|
||||
|
||||
def test_start_checked_stream_after_timeout(connection, stream_creator):
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
stream_creator('test_stream')
|
||||
)
|
||||
|
||||
timeout_ms = 2000
|
||||
|
||||
def call_check():
|
||||
execute_and_fetch_all(
|
||||
connect().cursor(),
|
||||
f"CHECK STREAM test_stream TIMEOUT {timeout_ms}")
|
||||
|
||||
check_stream_proc = Process(target=call_check, daemon=True)
|
||||
|
||||
start = time.time()
|
||||
check_stream_proc.start()
|
||||
assert timed_wait(
|
||||
lambda: get_is_running(
|
||||
cursor, "test_stream"))
|
||||
start_stream(cursor, "test_stream")
|
||||
end = time.time()
|
||||
|
||||
assert (end - start) < 1.3 * \
|
||||
timeout_ms, "The START STREAM was blocked too long"
|
||||
assert get_is_running(cursor, "test_stream")
|
||||
stop_stream(cursor, "test_stream")
|
||||
assert len(stream_info) == len(expected_stream_info)
|
||||
for info, expected_info in zip(stream_info, expected_stream_info):
|
||||
assert info == expected_info
|
||||
|
||||
@@ -13,10 +13,7 @@ import pytest
|
||||
from kafka import KafkaProducer
|
||||
from kafka.admin import KafkaAdminClient, NewTopic
|
||||
|
||||
import pulsar
|
||||
import requests
|
||||
|
||||
from common import NAME, connect, execute_and_fetch_all, PULSAR_SERVICE_URL
|
||||
from common import NAME, connect, execute_and_fetch_all
|
||||
|
||||
# To run these test locally a running Kafka sever is necessery. The test tries
|
||||
# to connect on localhost:9092.
|
||||
@@ -36,30 +33,20 @@ def connection():
|
||||
execute_and_fetch_all(cursor, f"DROP USER {username}")
|
||||
|
||||
|
||||
def get_topics(num):
|
||||
return [f'topic_{i}' for i in range(num)]
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def kafka_topics():
|
||||
admin_client = KafkaAdminClient(
|
||||
bootstrap_servers="localhost:9092",
|
||||
client_id="test")
|
||||
# The issue arises if we remove default kafka topics, e.g.
|
||||
# "__consumer_offsets"
|
||||
previous_topics = [
|
||||
topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
|
||||
def topics():
|
||||
admin_client = KafkaAdminClient(bootstrap_servers="localhost:9092", client_id="test")
|
||||
# The issue arises if we remove default kafka topics, e.g. "__consumer_offsets"
|
||||
previous_topics = [topic for topic in admin_client.list_topics() if topic != "__consumer_offsets"]
|
||||
if previous_topics:
|
||||
admin_client.delete_topics(topics=previous_topics, timeout_ms=5000)
|
||||
|
||||
topics = get_topics(3)
|
||||
topics = []
|
||||
topics_to_create = []
|
||||
for topic in topics:
|
||||
topics_to_create.append(
|
||||
NewTopic(
|
||||
name=topic,
|
||||
num_partitions=1,
|
||||
replication_factor=1))
|
||||
for index in range(3):
|
||||
topic = f"topic_{index}"
|
||||
topics.append(topic)
|
||||
topics_to_create.append(NewTopic(name=topic, num_partitions=1, replication_factor=1))
|
||||
|
||||
admin_client.create_topics(new_topics=topics_to_create, timeout_ms=5000)
|
||||
yield topics
|
||||
@@ -67,19 +54,5 @@ def kafka_topics():
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def kafka_producer():
|
||||
def producer():
|
||||
yield KafkaProducer(bootstrap_servers="localhost:9092")
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def pulsar_client():
|
||||
yield pulsar.Client(PULSAR_SERVICE_URL)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def pulsar_topics():
|
||||
topics = get_topics(3)
|
||||
for topic in topics:
|
||||
requests.delete(
|
||||
f'http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true')
|
||||
yield topics
|
||||
|
||||
@@ -18,9 +18,3 @@ services:
|
||||
- ALLOW_PLAINTEXT_LISTENER=yes
|
||||
depends_on:
|
||||
- zookeeper
|
||||
pulsar:
|
||||
image: 'apachepulsar/pulsar:2.8.1'
|
||||
ports:
|
||||
- '6652:8080'
|
||||
- '6650:6650'
|
||||
entrypoint: ['bin/pulsar', 'standalone']
|
||||
|
||||
@@ -1,444 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# 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
|
||||
import pytest
|
||||
import mgclient
|
||||
import time
|
||||
from multiprocessing import Process, Value
|
||||
import common
|
||||
|
||||
TRANSFORMATIONS_TO_CHECK = [
|
||||
"kafka_transform.simple",
|
||||
"kafka_transform.with_parameters"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_simple(kafka_producer, kafka_topics, connection, transformation):
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM test "
|
||||
f"TOPICS {','.join(kafka_topics)} "
|
||||
f"TRANSFORM {transformation}",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in kafka_topics:
|
||||
kafka_producer.send(topic, common.SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
for topic in kafka_topics:
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
cursor, topic, common.SIMPLE_MSG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_separate_consumers(
|
||||
kafka_producer,
|
||||
kafka_topics,
|
||||
connection,
|
||||
transformation):
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
|
||||
stream_names = []
|
||||
for topic in kafka_topics:
|
||||
stream_name = "stream_" + topic
|
||||
stream_names.append(stream_name)
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CREATE KAFKA STREAM {stream_name} "
|
||||
f"TOPICS {topic} "
|
||||
f"TRANSFORM {transformation}",
|
||||
)
|
||||
|
||||
for stream_name in stream_names:
|
||||
common.start_stream(cursor, stream_name)
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
for topic in kafka_topics:
|
||||
kafka_producer.send(topic, common.SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
for topic in kafka_topics:
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
cursor, topic, common.SIMPLE_MSG)
|
||||
|
||||
|
||||
def test_start_from_last_committed_offset(
|
||||
kafka_producer, kafka_topics, connection):
|
||||
# This test creates a stream, consumes a message to have a committed
|
||||
# offset, then destroys the stream. A new message is sent before the
|
||||
# stream is recreated and then restarted. This simulates when Memgraph is
|
||||
# stopped (stream is destroyed) and then restarted (stream is recreated).
|
||||
# This is of course not as good as restarting memgraph would be, but
|
||||
# restarting Memgraph during a single workload cannot be done currently.
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor, "CREATE KAFKA STREAM test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
"TRANSFORM kafka_transform.simple", )
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(1)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], common.SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
cursor, kafka_topics[0], common.SIMPLE_MSG)
|
||||
|
||||
common.stop_stream(cursor, "test")
|
||||
common.drop_stream(cursor, "test")
|
||||
|
||||
messages = [b"second message", b"third message"]
|
||||
for message in messages:
|
||||
kafka_producer.send(kafka_topics[0], message).get(timeout=60)
|
||||
|
||||
for message in messages:
|
||||
vertices_with_msg = common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"MATCH (n: MESSAGE {" f"payload: '{message.decode('utf-8')}'" "}) RETURN n",
|
||||
)
|
||||
|
||||
assert len(vertices_with_msg) == 0
|
||||
|
||||
common.execute_and_fetch_all(
|
||||
cursor, "CREATE KAFKA STREAM test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
"TRANSFORM kafka_transform.simple", )
|
||||
common.start_stream(cursor, "test")
|
||||
|
||||
for message in messages:
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
cursor, kafka_topics[0], message)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_check_stream(
|
||||
kafka_producer,
|
||||
kafka_topics,
|
||||
connection,
|
||||
transformation):
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM {transformation} "
|
||||
"BATCH_SIZE 1",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(1)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], common.SIMPLE_MSG).get(timeout=60)
|
||||
common.stop_stream(cursor, "test")
|
||||
|
||||
messages = [b"first message", b"second message", b"third message"]
|
||||
for message in messages:
|
||||
kafka_producer.send(kafka_topics[0], message).get(timeout=60)
|
||||
|
||||
def check_check_stream(batch_limit):
|
||||
assert (
|
||||
transformation == "kafka_transform.simple"
|
||||
or transformation == "kafka_transform.with_parameters"
|
||||
)
|
||||
test_results = common.execute_and_fetch_all(
|
||||
cursor, f"CHECK STREAM test BATCH_LIMIT {batch_limit}"
|
||||
)
|
||||
assert len(test_results) == batch_limit
|
||||
|
||||
for i in range(batch_limit):
|
||||
message_as_str = messages[i].decode("utf-8")
|
||||
if transformation == "kafka_transform.simple":
|
||||
assert f"payload: '{message_as_str}'" in test_results[i][common.QUERY]
|
||||
assert test_results[i][common.PARAMS] is None
|
||||
else:
|
||||
assert f"payload: $payload" in test_results[i][
|
||||
common.QUERY] and f"topic: $topic" in test_results[i][common.QUERY]
|
||||
parameters = test_results[i][common.PARAMS]
|
||||
# this is not a very sofisticated test, but checks if
|
||||
# timestamp has some kind of value
|
||||
assert parameters["timestamp"] > 1000000000000
|
||||
assert parameters["topic"] == kafka_topics[0]
|
||||
assert parameters["payload"] == message_as_str
|
||||
|
||||
check_check_stream(1)
|
||||
check_check_stream(2)
|
||||
check_check_stream(3)
|
||||
common.start_stream(cursor, "test")
|
||||
|
||||
for message in messages:
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
cursor, kafka_topics[0], message)
|
||||
|
||||
|
||||
def test_show_streams(kafka_producer, kafka_topics, connection):
|
||||
assert len(kafka_topics) > 1
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM default_values "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple "
|
||||
f"BOOTSTRAP_SERVERS 'localhost:9092'",
|
||||
)
|
||||
|
||||
consumer_group = "my_special_consumer_group"
|
||||
batch_interval = 42
|
||||
batch_size = 3
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM complex_values "
|
||||
f"TOPICS {','.join(kafka_topics)} "
|
||||
f"TRANSFORM kafka_transform.with_parameters "
|
||||
f"CONSUMER_GROUP {consumer_group} "
|
||||
f"BATCH_INTERVAL {batch_interval} "
|
||||
f"BATCH_SIZE {batch_size} ",
|
||||
)
|
||||
|
||||
assert len(common.execute_and_fetch_all(cursor, "SHOW STREAMS")) == 2
|
||||
|
||||
common.check_stream_info(
|
||||
cursor,
|
||||
"default_values",
|
||||
("default_values",
|
||||
"kafka",
|
||||
100,
|
||||
1000,
|
||||
"kafka_transform.simple",
|
||||
None,
|
||||
False),
|
||||
)
|
||||
|
||||
common.check_stream_info(
|
||||
cursor,
|
||||
"complex_values",
|
||||
(
|
||||
"complex_values",
|
||||
"kafka",
|
||||
batch_interval,
|
||||
batch_size,
|
||||
"kafka_transform.with_parameters",
|
||||
None,
|
||||
False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["START", "STOP"])
|
||||
def test_start_and_stop_during_check(
|
||||
kafka_producer,
|
||||
kafka_topics,
|
||||
connection,
|
||||
operation):
|
||||
assert len(kafka_topics) > 1
|
||||
|
||||
def stream_creator(stream_name):
|
||||
return f"CREATE KAFKA STREAM {stream_name} TOPICS {kafka_topics[0]} TRANSFORM kafka_transform.simple"
|
||||
|
||||
def message_sender(msg):
|
||||
kafka_producer.send(kafka_topics[0], msg).get(timeout=60)
|
||||
|
||||
common.test_start_and_stop_during_check(
|
||||
operation,
|
||||
connection,
|
||||
stream_creator,
|
||||
message_sender,
|
||||
"Kafka consumer test_stream is already stopped")
|
||||
|
||||
|
||||
def test_check_already_started_stream(kafka_topics, connection):
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM started_stream "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple",
|
||||
)
|
||||
common.start_stream(cursor, "started_stream")
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
common.execute_and_fetch_all(cursor, "CHECK STREAM started_stream")
|
||||
|
||||
|
||||
def test_start_checked_stream_after_timeout(kafka_topics, connection):
|
||||
def stream_creator(stream_name):
|
||||
return f"CREATE KAFKA STREAM {stream_name} TOPICS {kafka_topics[0]} TRANSFORM kafka_transform.simple"
|
||||
|
||||
common.test_start_checked_stream_after_timeout(connection, stream_creator)
|
||||
|
||||
|
||||
def test_restart_after_error(kafka_producer, kafka_topics, connection):
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM test_stream "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.query",
|
||||
)
|
||||
|
||||
common.start_stream(cursor, "test_stream")
|
||||
time.sleep(1)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], common.SIMPLE_MSG).get(timeout=60)
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(
|
||||
cursor, "test_stream"))
|
||||
|
||||
common.start_stream(cursor, "test_stream")
|
||||
time.sleep(1)
|
||||
kafka_producer.send(kafka_topics[0], b"CREATE (n:VERTEX { id : 42 })")
|
||||
assert common.check_one_result_row(
|
||||
cursor, "MATCH (n:VERTEX { id : 42 }) RETURN n")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_bootstrap_server(
|
||||
kafka_producer,
|
||||
kafka_topics,
|
||||
connection,
|
||||
transformation):
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
local = "localhost:9092"
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM test "
|
||||
f"TOPICS {','.join(kafka_topics)} "
|
||||
f"TRANSFORM {transformation} "
|
||||
f"BOOTSTRAP_SERVERS '{local}'",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in kafka_topics:
|
||||
kafka_producer.send(topic, common.SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
for topic in kafka_topics:
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
cursor, topic, common.SIMPLE_MSG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_bootstrap_server_empty(
|
||||
kafka_producer,
|
||||
kafka_topics,
|
||||
connection,
|
||||
transformation):
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM test "
|
||||
f"TOPICS {','.join(kafka_topics)} "
|
||||
f"TRANSFORM {transformation} "
|
||||
"BOOTSTRAP_SERVERS ''",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_set_offset(kafka_producer, kafka_topics, connection, transformation):
|
||||
assert len(kafka_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE KAFKA STREAM test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM {transformation} "
|
||||
"BATCH_SIZE 1",
|
||||
)
|
||||
|
||||
messages = [f"{i} message" for i in range(1, 21)]
|
||||
for message in messages:
|
||||
kafka_producer.send(kafka_topics[0], message.encode()).get(timeout=60)
|
||||
|
||||
def consume(expected_msgs):
|
||||
common.start_stream(cursor, "test")
|
||||
if len(expected_msgs) == 0:
|
||||
time.sleep(2)
|
||||
else:
|
||||
assert common.check_one_result_row(
|
||||
cursor,
|
||||
(
|
||||
f"MATCH (n: MESSAGE {{payload: '{expected_msgs[-1]}'}})"
|
||||
"RETURN n"
|
||||
),
|
||||
)
|
||||
common.stop_stream(cursor, "test")
|
||||
res = common.execute_and_fetch_all(
|
||||
cursor, "MATCH (n) RETURN n.payload"
|
||||
)
|
||||
return res
|
||||
|
||||
def execute_set_offset_and_consume(id, expected_msgs):
|
||||
common.execute_and_fetch_all(
|
||||
cursor, f"CALL mg.kafka_set_stream_offset('test', {id})"
|
||||
)
|
||||
return consume(expected_msgs)
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
res = common.execute_and_fetch_all(
|
||||
cursor, "CALL mg.kafka_set_stream_offset('foo', 10)"
|
||||
)
|
||||
|
||||
def comparison_check(a, b):
|
||||
return a == str(b).strip("'(,)")
|
||||
|
||||
res = execute_set_offset_and_consume(10, messages[10:])
|
||||
assert len(res) == 10
|
||||
assert all([comparison_check(a, b) for a, b in zip(messages[10:], res)])
|
||||
common.execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n")
|
||||
|
||||
res = execute_set_offset_and_consume(-1, messages)
|
||||
assert len(res) == len(messages)
|
||||
assert all([comparison_check(a, b) for a, b in zip(messages, res)])
|
||||
res = common.execute_and_fetch_all(cursor, "MATCH (n) return n.offset")
|
||||
assert all([comparison_check(str(i), res[i]) for i in range(1, 20)])
|
||||
res = common.execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n")
|
||||
|
||||
res = execute_set_offset_and_consume(-2, [])
|
||||
assert len(res) == 0
|
||||
last_msg = "Final Message"
|
||||
kafka_producer.send(kafka_topics[0], last_msg.encode()).get(timeout=60)
|
||||
res = consume([last_msg])
|
||||
assert len(res) == 1
|
||||
assert comparison_check("Final Message", res[0])
|
||||
common.execute_and_fetch_all(cursor, "MATCH (n) DETACH DELETE n")
|
||||
|
||||
def test_info_procedure(kafka_topics, connection):
|
||||
cursor = connection.cursor()
|
||||
stream_name = 'test_stream'
|
||||
local = "localhost:9092"
|
||||
consumer_group = "ConsumerGr"
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CREATE KAFKA STREAM {stream_name} "
|
||||
f"TOPICS {','.join(kafka_topics)} "
|
||||
f"TRANSFORM pulsar_transform.simple "
|
||||
f"CONSUMER_GROUP {consumer_group} "
|
||||
f"BOOTSTRAP_SERVERS '{local}'"
|
||||
)
|
||||
|
||||
stream_info = common.execute_and_fetch_all(cursor, f"CALL mg.kafka_stream_info('{stream_name}') YIELD *")
|
||||
|
||||
expected_stream_info = [(local, consumer_group, kafka_topics)]
|
||||
common.validate_info(stream_info, expected_stream_info)
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-rA"]))
|
||||
@@ -1,393 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# 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
|
||||
import pytest
|
||||
import mgclient
|
||||
import time
|
||||
from multiprocessing import Process, Value
|
||||
import common
|
||||
|
||||
TRANSFORMATIONS_TO_CHECK = [
|
||||
"pulsar_transform.simple",
|
||||
"pulsar_transform.with_parameters"]
|
||||
|
||||
|
||||
def check_vertex_exists_with_topic_and_payload(cursor, topic, payload_byte):
|
||||
decoded_payload = payload_byte.decode('utf-8')
|
||||
common.check_vertex_exists_with_properties(
|
||||
cursor, {
|
||||
'topic': f'"{common.pulsar_default_namespace_topic(topic)}"',
|
||||
'payload': f'"{decoded_payload}"'})
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_simple(pulsar_client, pulsar_topics, connection, transformation):
|
||||
assert len(pulsar_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE PULSAR STREAM test "
|
||||
f"TOPICS '{','.join(pulsar_topics)}' "
|
||||
f"TRANSFORM {transformation}",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(topic),
|
||||
send_timeout_millis=60000)
|
||||
producer.send(common.SIMPLE_MSG)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, topic, common.SIMPLE_MSG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_separate_consumers(
|
||||
pulsar_client,
|
||||
pulsar_topics,
|
||||
connection,
|
||||
transformation):
|
||||
assert len(pulsar_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
|
||||
stream_names = []
|
||||
for topic in pulsar_topics:
|
||||
stream_name = "stream_" + topic
|
||||
stream_names.append(stream_name)
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CREATE PULSAR STREAM {stream_name} "
|
||||
f"TOPICS {topic} "
|
||||
f"TRANSFORM {transformation}",
|
||||
)
|
||||
|
||||
for stream_name in stream_names:
|
||||
common.start_stream(cursor, stream_name)
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
producer = pulsar_client.create_producer(
|
||||
topic, send_timeout_millis=60000)
|
||||
producer.send(common.SIMPLE_MSG)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, topic, common.SIMPLE_MSG)
|
||||
|
||||
|
||||
def test_start_from_latest_messages(pulsar_client, pulsar_topics, connection):
|
||||
# This test creates a stream, consumes a message, then destroys the stream. A new message is sent before the
|
||||
# stream is recreated, and additional messages after the stream was recreated. Pulsar consumer
|
||||
# should only receive message that were sent after the consumer was created. Everything
|
||||
# inbetween should be lost. Additionally, we check that consumer continues from the correct message
|
||||
# after stopping and starting again.
|
||||
assert len(pulsar_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor, "CREATE PULSAR STREAM test "
|
||||
f"TOPICS {pulsar_topics[0]} "
|
||||
"TRANSFORM pulsar_transform.simple", )
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(1)
|
||||
|
||||
def assert_message_not_consumed(message):
|
||||
vertices_with_msg = common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"MATCH (n: MESSAGE {" f"payload: '{message.decode('utf-8')}'" "}) RETURN n",
|
||||
)
|
||||
|
||||
assert len(vertices_with_msg) == 0
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(
|
||||
pulsar_topics[0]), send_timeout_millis=60000)
|
||||
producer.send(common.SIMPLE_MSG)
|
||||
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, pulsar_topics[0], common.SIMPLE_MSG)
|
||||
|
||||
common.stop_stream(cursor, "test")
|
||||
|
||||
next_message = b"NEXT"
|
||||
producer.send(next_message)
|
||||
|
||||
assert_message_not_consumed(next_message)
|
||||
|
||||
common.start_stream(cursor, "test")
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, pulsar_topics[0], next_message)
|
||||
common.stop_stream(cursor, "test")
|
||||
|
||||
common.drop_stream(cursor, "test")
|
||||
|
||||
lost_message = b"LOST"
|
||||
valid_messages = [b"second message", b"third message"]
|
||||
|
||||
producer.send(lost_message)
|
||||
|
||||
assert_message_not_consumed(lost_message)
|
||||
|
||||
common.execute_and_fetch_all(
|
||||
cursor, "CREATE PULSAR STREAM test "
|
||||
f"TOPICS {pulsar_topics[0]} "
|
||||
"TRANSFORM pulsar_transform.simple", )
|
||||
|
||||
for message in valid_messages:
|
||||
producer.send(message)
|
||||
assert_message_not_consumed(message)
|
||||
|
||||
common.start_stream(cursor, "test")
|
||||
|
||||
assert_message_not_consumed(lost_message)
|
||||
|
||||
for message in valid_messages:
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, pulsar_topics[0], message)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_check_stream(
|
||||
pulsar_client,
|
||||
pulsar_topics,
|
||||
connection,
|
||||
transformation):
|
||||
assert len(pulsar_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE PULSAR STREAM test "
|
||||
f"TOPICS {pulsar_topics[0]} "
|
||||
f"TRANSFORM {transformation} "
|
||||
"BATCH_SIZE 1",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(1)
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(
|
||||
pulsar_topics[0]), send_timeout_millis=60000)
|
||||
producer.send(common.SIMPLE_MSG)
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, pulsar_topics[0], common.SIMPLE_MSG)
|
||||
common.stop_stream(cursor, "test")
|
||||
|
||||
messages = [b"first message", b"second message", b"third message"]
|
||||
for message in messages:
|
||||
producer.send(message)
|
||||
|
||||
def check_check_stream(batch_limit):
|
||||
assert (
|
||||
transformation == "pulsar_transform.simple"
|
||||
or transformation == "pulsar_transform.with_parameters"
|
||||
)
|
||||
test_results = common.execute_and_fetch_all(
|
||||
cursor, f"CHECK STREAM test BATCH_LIMIT {batch_limit}"
|
||||
)
|
||||
assert len(test_results) == batch_limit
|
||||
|
||||
for i in range(batch_limit):
|
||||
message_as_str = messages[i].decode("utf-8")
|
||||
if transformation == "pulsar_transform.simple":
|
||||
assert f"payload: '{message_as_str}'" in test_results[i][common.QUERY]
|
||||
assert test_results[i][common.PARAMS] is None
|
||||
else:
|
||||
assert f"payload: $payload" in test_results[i][
|
||||
common.QUERY] and f"topic: $topic" in test_results[i][common.QUERY]
|
||||
parameters = test_results[i][common.PARAMS]
|
||||
assert parameters["topic"] == common.pulsar_default_namespace_topic(
|
||||
pulsar_topics[0])
|
||||
assert parameters["payload"] == message_as_str
|
||||
|
||||
check_check_stream(1)
|
||||
check_check_stream(2)
|
||||
check_check_stream(3)
|
||||
common.start_stream(cursor, "test")
|
||||
|
||||
for message in messages:
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, pulsar_topics[0], message)
|
||||
|
||||
|
||||
def test_info_procedure(pulsar_client, pulsar_topics, connection):
|
||||
cursor = connection.cursor()
|
||||
stream_name = 'test_stream'
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CREATE PULSAR STREAM {stream_name} "
|
||||
f"TOPICS {','.join(pulsar_topics)} "
|
||||
f"TRANSFORM pulsar_transform.simple ",
|
||||
)
|
||||
|
||||
stream_info = common.execute_and_fetch_all(cursor, f"CALL mg.pulsar_stream_info('{stream_name}') YIELD *")
|
||||
|
||||
expected_stream_info = [(common.PULSAR_SERVICE_URL, pulsar_topics)]
|
||||
common.validate_info(stream_info, expected_stream_info)
|
||||
|
||||
def test_show_streams(pulsar_client, pulsar_topics, connection):
|
||||
assert len(pulsar_topics) > 1
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE PULSAR STREAM default_values "
|
||||
f"TOPICS {pulsar_topics[0]} "
|
||||
f"TRANSFORM pulsar_transform.simple ",
|
||||
)
|
||||
|
||||
batch_interval = 42
|
||||
batch_size = 3
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE PULSAR STREAM complex_values "
|
||||
f"TOPICS {','.join(pulsar_topics)} "
|
||||
f"TRANSFORM pulsar_transform.with_parameters "
|
||||
f"BATCH_INTERVAL {batch_interval} "
|
||||
f"BATCH_SIZE {batch_size} ",
|
||||
)
|
||||
|
||||
assert len(common.execute_and_fetch_all(cursor, "SHOW STREAMS")) == 2
|
||||
|
||||
common.check_stream_info(
|
||||
cursor,
|
||||
"default_values",
|
||||
("default_values",
|
||||
"pulsar",
|
||||
100,
|
||||
1000,
|
||||
"pulsar_transform.simple",
|
||||
None,
|
||||
False),
|
||||
)
|
||||
|
||||
common.check_stream_info(
|
||||
cursor,
|
||||
"complex_values",
|
||||
(
|
||||
"complex_values",
|
||||
"pulsar",
|
||||
batch_interval,
|
||||
batch_size,
|
||||
"pulsar_transform.with_parameters",
|
||||
None,
|
||||
False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["START", "STOP"])
|
||||
def test_start_and_stop_during_check(
|
||||
pulsar_client,
|
||||
pulsar_topics,
|
||||
connection,
|
||||
operation):
|
||||
assert len(pulsar_topics) > 1
|
||||
|
||||
def stream_creator(stream_name):
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple"
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(
|
||||
pulsar_topics[0]), send_timeout_millis=60000)
|
||||
|
||||
def message_sender(msg):
|
||||
producer.send(msg)
|
||||
|
||||
common.test_start_and_stop_during_check(
|
||||
operation,
|
||||
connection,
|
||||
stream_creator,
|
||||
message_sender,
|
||||
"Pulsar consumer test_stream is already stopped")
|
||||
|
||||
|
||||
def test_check_already_started_stream(pulsar_topics, connection):
|
||||
assert len(pulsar_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE PULSAR STREAM started_stream "
|
||||
f"TOPICS {pulsar_topics[0]} "
|
||||
f"TRANSFORM pulsar_transform.simple",
|
||||
)
|
||||
common.start_stream(cursor, "started_stream")
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
common.execute_and_fetch_all(cursor, "CHECK STREAM started_stream")
|
||||
|
||||
|
||||
def test_start_checked_stream_after_timeout(pulsar_topics, connection):
|
||||
def stream_creator(stream_name):
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple"
|
||||
common.test_start_checked_stream_after_timeout(connection, stream_creator)
|
||||
|
||||
|
||||
def test_restart_after_error(pulsar_client, pulsar_topics, connection):
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE PULSAR STREAM test_stream "
|
||||
f"TOPICS {pulsar_topics[0]} "
|
||||
f"TRANSFORM pulsar_transform.query",
|
||||
)
|
||||
|
||||
common.start_stream(cursor, "test_stream")
|
||||
time.sleep(1)
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(
|
||||
pulsar_topics[0]), send_timeout_millis=60000)
|
||||
producer.send(common.SIMPLE_MSG)
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(
|
||||
cursor, "test_stream"))
|
||||
|
||||
common.start_stream(cursor, "test_stream")
|
||||
time.sleep(1)
|
||||
producer.send(b"CREATE (n:VERTEX { id : 42 })")
|
||||
assert common.check_one_result_row(
|
||||
cursor, "MATCH (n:VERTEX { id : 42 }) RETURN n")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_service_url(pulsar_client, pulsar_topics, connection, transformation):
|
||||
assert len(pulsar_topics) > 0
|
||||
cursor = connection.cursor()
|
||||
local = "pulsar://127.0.0.1:6650"
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE PULSAR STREAM test "
|
||||
f"TOPICS {','.join(pulsar_topics)} "
|
||||
f"TRANSFORM {transformation} "
|
||||
f"SERVICE_URL '{local}'",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(topic),
|
||||
send_timeout_millis=60000)
|
||||
producer.send(common.SIMPLE_MSG)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
check_vertex_exists_with_topic_and_payload(
|
||||
cursor, topic, common.SIMPLE_MSG)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-rA"]))
|
||||
@@ -32,20 +32,20 @@ def create_stream_user(cursor, stream_user):
|
||||
cursor, f"GRANT STREAM TO {stream_user}")
|
||||
|
||||
|
||||
def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
assert len(kafka_topics) > 0
|
||||
def test_ownerless_stream(producer, topics, connection):
|
||||
assert len(topics) > 0
|
||||
userless_cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(userless_cursor,
|
||||
"CREATE KAFKA STREAM ownerless "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
"CREATE STREAM ownerless "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple")
|
||||
common.start_stream(userless_cursor, "ownerless")
|
||||
time.sleep(1)
|
||||
|
||||
admin_user = "admin_user"
|
||||
create_admin_user(userless_cursor, admin_user)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
|
||||
producer.send(topics[0], b"first message").get(timeout=60)
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(userless_cursor, "ownerless"))
|
||||
|
||||
@@ -57,32 +57,32 @@ def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
time.sleep(1)
|
||||
|
||||
second_message = b"second message"
|
||||
kafka_producer.send(kafka_topics[0], second_message).get(timeout=60)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], second_message)
|
||||
producer.send(topics[0], second_message).get(timeout=60)
|
||||
common.check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, topics[0], second_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
def test_owner_is_shown(kafka_topics, connection):
|
||||
assert len(kafka_topics) > 0
|
||||
def test_owner_is_shown(topics, connection):
|
||||
assert len(topics) > 0
|
||||
userless_cursor = connection.cursor()
|
||||
|
||||
stream_user = "stream_user"
|
||||
create_stream_user(userless_cursor, stream_user)
|
||||
stream_cursor = get_cursor_with_user(stream_user)
|
||||
|
||||
common.execute_and_fetch_all(stream_cursor, "CREATE KAFKA STREAM test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
common.execute_and_fetch_all(stream_cursor, "CREATE STREAM test "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple")
|
||||
|
||||
common.check_stream_info(userless_cursor, "test", ("test", "kafka", 100, 1000,
|
||||
"kafka_transform.simple", stream_user, False))
|
||||
common.check_stream_info(userless_cursor, "test", ("test", None, None,
|
||||
"transform.simple", stream_user, False))
|
||||
|
||||
|
||||
def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
|
||||
assert len(kafka_topics) > 0
|
||||
def test_insufficient_privileges(producer, topics, connection):
|
||||
assert len(topics) > 0
|
||||
userless_cursor = connection.cursor()
|
||||
|
||||
admin_user = "admin_user"
|
||||
@@ -94,16 +94,16 @@ def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
|
||||
stream_cursor = get_cursor_with_user(stream_user)
|
||||
|
||||
common.execute_and_fetch_all(stream_cursor,
|
||||
"CREATE KAFKA STREAM insufficient_test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
"CREATE STREAM insufficient_test "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple")
|
||||
|
||||
# the stream is started by admin, but should check against the owner
|
||||
# privileges
|
||||
common.start_stream(admin_cursor, "insufficient_test")
|
||||
time.sleep(1)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
|
||||
producer.send(topics[0], b"first message").get(timeout=60)
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(userless_cursor, "insufficient_test"))
|
||||
|
||||
@@ -116,16 +116,16 @@ def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
|
||||
time.sleep(1)
|
||||
|
||||
second_message = b"second message"
|
||||
kafka_producer.send(kafka_topics[0], second_message).get(timeout=60)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], second_message)
|
||||
producer.send(topics[0], second_message).get(timeout=60)
|
||||
common.check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, topics[0], second_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
def test_happy_case(kafka_producer, kafka_topics, connection):
|
||||
assert len(kafka_topics) > 0
|
||||
def test_happy_case(producer, topics, connection):
|
||||
assert len(topics) > 0
|
||||
userless_cursor = connection.cursor()
|
||||
|
||||
admin_user = "admin_user"
|
||||
@@ -139,18 +139,18 @@ def test_happy_case(kafka_producer, kafka_topics, connection):
|
||||
admin_cursor, f"GRANT CREATE TO {stream_user}")
|
||||
|
||||
common.execute_and_fetch_all(stream_cursor,
|
||||
"CREATE KAFKA STREAM insufficient_test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
"CREATE STREAM insufficient_test "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple")
|
||||
|
||||
common.start_stream(stream_cursor, "insufficient_test")
|
||||
time.sleep(1)
|
||||
|
||||
first_message = b"first message"
|
||||
kafka_producer.send(kafka_topics[0], first_message).get(timeout=60)
|
||||
producer.send(topics[0], first_message).get(timeout=60)
|
||||
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], first_message)
|
||||
common.check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, topics[0], first_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
442
tests/e2e/streams/streams_tests.py
Executable file
442
tests/e2e/streams/streams_tests.py
Executable file
@@ -0,0 +1,442 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# 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
|
||||
import pytest
|
||||
import mgclient
|
||||
import time
|
||||
from multiprocessing import Process, Value
|
||||
import common
|
||||
|
||||
# These are the indices of the query and parameters in the result of CHECK
|
||||
# STREAM query
|
||||
QUERY = 0
|
||||
PARAMS = 1
|
||||
|
||||
TRANSFORMATIONS_TO_CHECK = ["transform.simple", "transform.with_parameters"]
|
||||
|
||||
SIMPLE_MSG = b"message"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_simple(producer, topics, connection, transformation):
|
||||
assert len(topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test "
|
||||
f"TOPICS {','.join(topics)} "
|
||||
f"TRANSFORM {transformation}",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in topics:
|
||||
producer.send(topic, SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
for topic in topics:
|
||||
common.check_vertex_exists_with_topic_and_payload(cursor, topic, SIMPLE_MSG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_separate_consumers(producer, topics, connection, transformation):
|
||||
assert len(topics) > 0
|
||||
cursor = connection.cursor()
|
||||
|
||||
stream_names = []
|
||||
for topic in topics:
|
||||
stream_name = "stream_" + topic
|
||||
stream_names.append(stream_name)
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CREATE STREAM {stream_name} "
|
||||
f"TOPICS {topic} "
|
||||
f"TRANSFORM {transformation}",
|
||||
)
|
||||
|
||||
for stream_name in stream_names:
|
||||
common.start_stream(cursor, stream_name)
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
for topic in topics:
|
||||
producer.send(topic, SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
for topic in topics:
|
||||
common.check_vertex_exists_with_topic_and_payload(cursor, topic, SIMPLE_MSG)
|
||||
|
||||
|
||||
def test_start_from_last_committed_offset(producer, topics, connection):
|
||||
# This test creates a stream, consumes a message to have a committed
|
||||
# offset, then destroys the stream. A new message is sent before the
|
||||
# stream is recreated and then restarted. This simulates when Memgraph is
|
||||
# stopped (stream is destroyed) and then restarted (stream is recreated).
|
||||
# This is of course not as good as restarting memgraph would be, but
|
||||
# restarting Memgraph during a single workload cannot be done currently.
|
||||
assert len(topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test " f"TOPICS {topics[0]} " "TRANSFORM transform.simple",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(1)
|
||||
|
||||
producer.send(topics[0], SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
common.check_vertex_exists_with_topic_and_payload(cursor, topics[0], SIMPLE_MSG)
|
||||
|
||||
common.stop_stream(cursor, "test")
|
||||
common.drop_stream(cursor, "test")
|
||||
|
||||
messages = [b"second message", b"third message"]
|
||||
for message in messages:
|
||||
producer.send(topics[0], message).get(timeout=60)
|
||||
|
||||
for message in messages:
|
||||
vertices_with_msg = common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"MATCH (n: MESSAGE {" f"payload: '{message.decode('utf-8')}'" "}) RETURN n",
|
||||
)
|
||||
|
||||
assert len(vertices_with_msg) == 0
|
||||
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test " f"TOPICS {topics[0]} " "TRANSFORM transform.simple",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
|
||||
for message in messages:
|
||||
common.check_vertex_exists_with_topic_and_payload(cursor, topics[0], message)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_check_stream(producer, topics, connection, transformation):
|
||||
assert len(topics) > 0
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM {transformation} "
|
||||
"BATCH_SIZE 1",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(1)
|
||||
|
||||
producer.send(topics[0], SIMPLE_MSG).get(timeout=60)
|
||||
common.stop_stream(cursor, "test")
|
||||
|
||||
messages = [b"first message", b"second message", b"third message"]
|
||||
for message in messages:
|
||||
producer.send(topics[0], message).get(timeout=60)
|
||||
|
||||
def check_check_stream(batch_limit):
|
||||
assert (
|
||||
transformation == "transform.simple"
|
||||
or transformation == "transform.with_parameters"
|
||||
)
|
||||
test_results = common.execute_and_fetch_all(
|
||||
cursor, f"CHECK STREAM test BATCH_LIMIT {batch_limit}"
|
||||
)
|
||||
assert len(test_results) == batch_limit
|
||||
|
||||
for i in range(batch_limit):
|
||||
message_as_str = messages[i].decode("utf-8")
|
||||
if transformation == "transform.simple":
|
||||
assert f"payload: '{message_as_str}'" in test_results[i][QUERY]
|
||||
assert test_results[i][PARAMS] is None
|
||||
else:
|
||||
assert test_results[i][QUERY] == (
|
||||
"CREATE (n:MESSAGE "
|
||||
"{timestamp: $timestamp, "
|
||||
"payload: $payload, "
|
||||
"topic: $topic})"
|
||||
)
|
||||
parameters = test_results[i][PARAMS]
|
||||
# this is not a very sofisticated test, but checks if
|
||||
# timestamp has some kind of value
|
||||
assert parameters["timestamp"] > 1000000000000
|
||||
assert parameters["topic"] == topics[0]
|
||||
assert parameters["payload"] == message_as_str
|
||||
|
||||
check_check_stream(1)
|
||||
check_check_stream(2)
|
||||
check_check_stream(3)
|
||||
common.start_stream(cursor, "test")
|
||||
|
||||
for message in messages:
|
||||
common.check_vertex_exists_with_topic_and_payload(cursor, topics[0], message)
|
||||
|
||||
|
||||
def test_show_streams(producer, topics, connection):
|
||||
assert len(topics) > 1
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM default_values "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple "
|
||||
f"BOOTSTRAP_SERVERS 'localhost:9092'",
|
||||
)
|
||||
|
||||
consumer_group = "my_special_consumer_group"
|
||||
batch_interval = 42
|
||||
batch_size = 3
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM complex_values "
|
||||
f"TOPICS {','.join(topics)} "
|
||||
f"TRANSFORM transform.with_parameters "
|
||||
f"CONSUMER_GROUP {consumer_group} "
|
||||
f"BATCH_INTERVAL {batch_interval} "
|
||||
f"BATCH_SIZE {batch_size} ",
|
||||
)
|
||||
|
||||
assert len(common.execute_and_fetch_all(cursor, "SHOW STREAMS")) == 2
|
||||
|
||||
common.check_stream_info(
|
||||
cursor,
|
||||
"default_values",
|
||||
("default_values", None, None, "transform.simple", None, False),
|
||||
)
|
||||
|
||||
common.check_stream_info(
|
||||
cursor,
|
||||
"complex_values",
|
||||
(
|
||||
"complex_values",
|
||||
batch_interval,
|
||||
batch_size,
|
||||
"transform.with_parameters",
|
||||
None,
|
||||
False,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("operation", ["START", "STOP"])
|
||||
def test_start_and_stop_during_check(producer, topics, connection, operation):
|
||||
# This test is quite complex. The goal is to call START/STOP queries
|
||||
# while a CHECK query is waiting for its result. Because the Global
|
||||
# Interpreter Lock, running queries on multiple threads is not useful,
|
||||
# because only one of them can call Cursor::execute at a time. Therefore
|
||||
# multiple processes are used to execute the queries, because different
|
||||
# processes have different GILs.
|
||||
# The counter variables are thread- and process-safe variables to
|
||||
# synchronize between the different processes. Each value represents a
|
||||
# specific phase of the execution of the processes.
|
||||
assert len(topics) > 1
|
||||
assert operation == "START" or operation == "STOP"
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test_stream "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple",
|
||||
)
|
||||
|
||||
check_counter = Value("i", 0)
|
||||
check_result_len = Value("i", 0)
|
||||
operation_counter = Value("i", 0)
|
||||
|
||||
CHECK_BEFORE_EXECUTE = 1
|
||||
CHECK_AFTER_FETCHALL = 2
|
||||
CHECK_CORRECT_RESULT = 3
|
||||
CHECK_INCORRECT_RESULT = 4
|
||||
|
||||
def call_check(counter, result_len):
|
||||
# This process will call the CHECK query and increment the counter
|
||||
# based on its progress and expected behavior
|
||||
connection = common.connect()
|
||||
cursor = connection.cursor()
|
||||
counter.value = CHECK_BEFORE_EXECUTE
|
||||
result = common.execute_and_fetch_all(cursor, "CHECK STREAM test_stream")
|
||||
result_len.value = len(result)
|
||||
counter.value = CHECK_AFTER_FETCHALL
|
||||
if len(result) > 0 and "payload: 'message'" in result[0][QUERY]:
|
||||
counter.value = CHECK_CORRECT_RESULT
|
||||
else:
|
||||
counter.value = CHECK_INCORRECT_RESULT
|
||||
|
||||
OP_BEFORE_EXECUTE = 1
|
||||
OP_AFTER_FETCHALL = 2
|
||||
OP_ALREADY_STOPPED_EXCEPTION = 3
|
||||
OP_INCORRECT_ALREADY_STOPPED_EXCEPTION = 4
|
||||
OP_UNEXPECTED_EXCEPTION = 5
|
||||
|
||||
def call_operation(counter):
|
||||
# This porcess will call the query with the specified operation and
|
||||
# increment the counter based on its progress and expected behavior
|
||||
connection = common.connect()
|
||||
cursor = connection.cursor()
|
||||
counter.value = OP_BEFORE_EXECUTE
|
||||
try:
|
||||
common.execute_and_fetch_all(cursor, f"{operation} STREAM test_stream")
|
||||
counter.value = OP_AFTER_FETCHALL
|
||||
except mgclient.DatabaseError as e:
|
||||
if "Kafka consumer test_stream is already stopped" in str(e):
|
||||
counter.value = OP_ALREADY_STOPPED_EXCEPTION
|
||||
else:
|
||||
counter.value = OP_INCORRECT_ALREADY_STOPPED_EXCEPTION
|
||||
except Exception:
|
||||
counter.value = OP_UNEXPECTED_EXCEPTION
|
||||
|
||||
check_stream_proc = Process(
|
||||
target=call_check, daemon=True, args=(check_counter, check_result_len)
|
||||
)
|
||||
operation_proc = Process(
|
||||
target=call_operation, daemon=True, args=(operation_counter,)
|
||||
)
|
||||
|
||||
try:
|
||||
check_stream_proc.start()
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
assert common.timed_wait(lambda: check_counter.value == CHECK_BEFORE_EXECUTE)
|
||||
assert common.timed_wait(lambda: common.get_is_running(cursor, "test_stream"))
|
||||
assert check_counter.value == CHECK_BEFORE_EXECUTE, (
|
||||
"SHOW STREAMS " "was blocked until the end of CHECK STREAM"
|
||||
)
|
||||
operation_proc.start()
|
||||
assert common.timed_wait(lambda: operation_counter.value == OP_BEFORE_EXECUTE)
|
||||
|
||||
producer.send(topics[0], SIMPLE_MSG).get(timeout=60)
|
||||
assert common.timed_wait(lambda: check_counter.value > CHECK_AFTER_FETCHALL)
|
||||
assert check_counter.value == CHECK_CORRECT_RESULT
|
||||
assert check_result_len.value == 1
|
||||
check_stream_proc.join()
|
||||
|
||||
operation_proc.join()
|
||||
if operation == "START":
|
||||
assert operation_counter.value == OP_AFTER_FETCHALL
|
||||
assert common.get_is_running(cursor, "test_stream")
|
||||
else:
|
||||
assert operation_counter.value == OP_ALREADY_STOPPED_EXCEPTION
|
||||
assert not common.get_is_running(cursor, "test_stream")
|
||||
|
||||
finally:
|
||||
# to make sure CHECK STREAM finishes
|
||||
producer.send(topics[0], SIMPLE_MSG).get(timeout=60)
|
||||
if check_stream_proc.is_alive():
|
||||
check_stream_proc.terminate()
|
||||
if operation_proc.is_alive():
|
||||
operation_proc.terminate()
|
||||
|
||||
|
||||
def test_check_already_started_stream(topics, connection):
|
||||
assert len(topics) > 0
|
||||
cursor = connection.cursor()
|
||||
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM started_stream "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple",
|
||||
)
|
||||
common.start_stream(cursor, "started_stream")
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
common.execute_and_fetch_all(cursor, "CHECK STREAM started_stream")
|
||||
|
||||
|
||||
def test_start_checked_stream_after_timeout(topics, connection):
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test_stream "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.simple",
|
||||
)
|
||||
|
||||
timeout_ms = 2000
|
||||
|
||||
def call_check():
|
||||
common.execute_and_fetch_all(
|
||||
common.connect().cursor(), f"CHECK STREAM test_stream TIMEOUT {timeout_ms}"
|
||||
)
|
||||
|
||||
check_stream_proc = Process(target=call_check, daemon=True)
|
||||
|
||||
start = time.time()
|
||||
check_stream_proc.start()
|
||||
assert common.timed_wait(lambda: common.get_is_running(cursor, "test_stream"))
|
||||
common.start_stream(cursor, "test_stream")
|
||||
end = time.time()
|
||||
|
||||
assert (end - start) < 1.3 * timeout_ms, "The START STREAM was blocked too long"
|
||||
assert common.get_is_running(cursor, "test_stream")
|
||||
common.stop_stream(cursor, "test_stream")
|
||||
|
||||
|
||||
def test_restart_after_error(producer, topics, connection):
|
||||
cursor = connection.cursor()
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test_stream "
|
||||
f"TOPICS {topics[0]} "
|
||||
f"TRANSFORM transform.query",
|
||||
)
|
||||
|
||||
common.start_stream(cursor, "test_stream")
|
||||
time.sleep(1)
|
||||
|
||||
producer.send(topics[0], SIMPLE_MSG).get(timeout=60)
|
||||
assert common.timed_wait(lambda: not common.get_is_running(cursor, "test_stream"))
|
||||
|
||||
common.start_stream(cursor, "test_stream")
|
||||
time.sleep(1)
|
||||
producer.send(topics[0], b"CREATE (n:VERTEX { id : 42 })")
|
||||
assert common.check_one_result_row(cursor, "MATCH (n:VERTEX { id : 42 }) RETURN n")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_bootstrap_server(producer, topics, connection, transformation):
|
||||
assert len(topics) > 0
|
||||
cursor = connection.cursor()
|
||||
local = "localhost:9092"
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test "
|
||||
f"TOPICS {','.join(topics)} "
|
||||
f"TRANSFORM {transformation} "
|
||||
f"BOOTSTRAP_SERVERS '{local}'",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in topics:
|
||||
producer.send(topic, SIMPLE_MSG).get(timeout=60)
|
||||
|
||||
for topic in topics:
|
||||
common.check_vertex_exists_with_topic_and_payload(cursor, topic, SIMPLE_MSG)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK)
|
||||
def test_bootstrap_server_empty(producer, topics, connection, transformation):
|
||||
assert len(topics) > 0
|
||||
cursor = connection.cursor()
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
common.execute_and_fetch_all(
|
||||
cursor,
|
||||
"CREATE STREAM test "
|
||||
f"TOPICS {','.join(topics)} "
|
||||
f"TRANSFORM {transformation} "
|
||||
"BOOTSTRAP_SERVERS ''",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-rA"]))
|
||||
@@ -1,2 +1 @@
|
||||
copy_streams_e2e_python_files(kafka_transform.py)
|
||||
copy_streams_e2e_python_files(pulsar_transform.py)
|
||||
copy_streams_e2e_python_files(transform.py)
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
# 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 mgp
|
||||
|
||||
|
||||
@mgp.transformation
|
||||
def simple(
|
||||
context: mgp.TransCtx, messages: mgp.Messages
|
||||
) -> mgp.Record(query=str, parameters=mgp.Map):
|
||||
|
||||
result_queries = []
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
message = messages.message_at(i)
|
||||
assert message.source_type() == mgp.SOURCE_TYPE_KAFKA
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
result_queries.append(
|
||||
mgp.Record(
|
||||
query=f"""
|
||||
CREATE (n:MESSAGE {{
|
||||
timestamp: '{message.timestamp()}',
|
||||
payload: '{payload_as_str}',
|
||||
offset: '{message.offset()}',
|
||||
topic: '{message.topic_name()}'
|
||||
}})""",
|
||||
parameters=None))
|
||||
|
||||
return result_queries
|
||||
|
||||
|
||||
@mgp.transformation
|
||||
def with_parameters(
|
||||
context: mgp.TransCtx, messages: mgp.Messages
|
||||
) -> mgp.Record(query=str, parameters=mgp.Map):
|
||||
|
||||
result_queries = []
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
message = messages.message_at(i)
|
||||
assert message.source_type() == mgp.SOURCE_TYPE_KAFKA
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
result_queries.append(
|
||||
mgp.Record(
|
||||
query="""
|
||||
CREATE (n:MESSAGE {
|
||||
timestamp: $timestamp,
|
||||
payload: $payload,
|
||||
offset: $offset,
|
||||
topic: $topic
|
||||
})""",
|
||||
parameters={
|
||||
"timestamp": message.timestamp(),
|
||||
"payload": payload_as_str,
|
||||
"offset": message.offset(),
|
||||
"topic": message.topic_name()}))
|
||||
|
||||
return result_queries
|
||||
|
||||
|
||||
@mgp.transformation
|
||||
def query(
|
||||
messages: mgp.Messages,
|
||||
) -> mgp.Record(query=str, parameters=mgp.Nullable[mgp.Map]):
|
||||
result_queries = []
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
message = messages.message_at(i)
|
||||
assert message.source_type() == mgp.SOURCE_TYPE_KAFKA
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
result_queries.append(
|
||||
mgp.Record(query=payload_as_str, parameters=None)
|
||||
)
|
||||
|
||||
return result_queries
|
||||
@@ -21,16 +21,10 @@ def simple(context: mgp.TransCtx,
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
message = messages.message_at(i)
|
||||
assert message.source_type() == mgp.SOURCE_TYPE_PULSAR
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
result_queries.append(
|
||||
mgp.Record(
|
||||
query=f"""
|
||||
CREATE (n:MESSAGE {{
|
||||
payload: '{payload_as_str}',
|
||||
topic: '{message.topic_name()}'
|
||||
}})""",
|
||||
parameters=None))
|
||||
result_queries.append(mgp.Record(
|
||||
query=f"CREATE (n:MESSAGE {{timestamp: '{message.timestamp()}', payload: '{payload_as_str}', topic: '{message.topic_name()}'}})",
|
||||
parameters=None))
|
||||
|
||||
return result_queries
|
||||
|
||||
@@ -44,18 +38,12 @@ def with_parameters(context: mgp.TransCtx,
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
message = messages.message_at(i)
|
||||
assert message.source_type() == mgp.SOURCE_TYPE_PULSAR
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
result_queries.append(
|
||||
mgp.Record(
|
||||
query="""
|
||||
CREATE (n:MESSAGE {
|
||||
payload: $payload,
|
||||
topic: $topic
|
||||
})""",
|
||||
parameters={
|
||||
"payload": payload_as_str,
|
||||
"topic": message.topic_name()}))
|
||||
result_queries.append(mgp.Record(
|
||||
query="CREATE (n:MESSAGE {timestamp: $timestamp, payload: $payload, topic: $topic})",
|
||||
parameters={"timestamp": message.timestamp(),
|
||||
"payload": payload_as_str,
|
||||
"topic": message.topic_name()}))
|
||||
|
||||
return result_queries
|
||||
|
||||
@@ -67,7 +55,6 @@ def query(messages: mgp.Messages
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
message = messages.message_at(i)
|
||||
assert message.source_type() == mgp.SOURCE_TYPE_PULSAR
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
result_queries.append(mgp.Record(
|
||||
query=payload_as_str, parameters=None))
|
||||
@@ -1,24 +1,19 @@
|
||||
template_cluster: &template_cluster
|
||||
cluster:
|
||||
main:
|
||||
args: ["--bolt-port", "7687", "--log-level=DEBUG", "--kafka-bootstrap-servers=localhost:9092", "--query-execution-timeout-sec=0", "--pulsar-service-url=pulsar://127.0.0.1:6650"]
|
||||
args: ["--bolt-port", "7687", "--log-level=TRACE", "--kafka-bootstrap-servers=localhost:9092", "--query-execution-timeout-sec=0"]
|
||||
log_file: "streams-e2e.log"
|
||||
setup_queries: []
|
||||
validation_queries: []
|
||||
|
||||
workloads:
|
||||
- name: "Kafka streams start, stop and show"
|
||||
- name: "Streams start, stop and show"
|
||||
binary: "tests/e2e/pytest_runner.sh"
|
||||
proc: "tests/e2e/streams/transformations/"
|
||||
args: ["streams/kafka_streams_tests.py"]
|
||||
args: ["streams/streams_tests.py"]
|
||||
<<: *template_cluster
|
||||
- name: "Streams with users"
|
||||
binary: "tests/e2e/pytest_runner.sh"
|
||||
proc: "tests/e2e/streams/transformations/"
|
||||
args: ["streams/streams_owner_tests.py"]
|
||||
<<: *template_cluster
|
||||
- name: "Pulsar streams start, stop and show"
|
||||
binary: "tests/e2e/pytest_runner.sh"
|
||||
proc: "tests/e2e/streams/transformations/"
|
||||
args: ["streams/pulsar_streams_tests.py"]
|
||||
<<: *template_cluster
|
||||
|
||||
@@ -2,5 +2,5 @@ behave==1.2.6
|
||||
neo4j-driver==4.1.1
|
||||
parse==1.18.0
|
||||
parse-type==0.5.2
|
||||
PyYAML==5.4.1
|
||||
PyYAML==5.3.1
|
||||
six==1.15.0
|
||||
|
||||
@@ -405,70 +405,6 @@ Feature: Match
|
||||
"""
|
||||
Then the result should be empty
|
||||
|
||||
Scenario: Test match with order by and date
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE({a: DATE('2021-12-31')}), ({a: DATE('2021-11-11')}), ({a: DATE('2021-12-28')})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH (n) RETURN n.a ORDER BY n.a
|
||||
"""
|
||||
Then the result should be, in order:
|
||||
| n.a |
|
||||
| 2021-11-11 |
|
||||
| 2021-12-28 |
|
||||
| 2021-12-31 |
|
||||
|
||||
Scenario: Test match with order by and localtime
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE({a: LOCALTIME('09:12:31')}), ({a: LOCALTIME('09:09:20')}), ({a: LOCALTIME('09:11:21')})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH (n) RETURN n.a ORDER BY n.a
|
||||
"""
|
||||
Then the result should be, in order:
|
||||
| n.a |
|
||||
| 09:09:20.000000000 |
|
||||
| 09:11:21.000000000 |
|
||||
| 09:12:31.000000000 |
|
||||
|
||||
Scenario: Test match with order by and localdatetime
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE({a: LOCALDATETIME('2021-11-22T09:12:31')}), ({a: LOCALDATETIME('2021-11-23T09:10:30')}), ({a: LOCALDATETIME('2021-11-10T09:14:21')})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH (n) RETURN n.a ORDER BY n.a
|
||||
"""
|
||||
Then the result should be, in order:
|
||||
| n.a |
|
||||
| 2021-11-10T09:14:21.000000000 |
|
||||
| 2021-11-22T09:12:31.000000000 |
|
||||
| 2021-11-23T09:10:30.000000000 |
|
||||
|
||||
Scenario: Test match with order by and duration
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
"""
|
||||
CREATE({a: DURATION('P12DT3M')}), ({a: DURATION('P11DT8M')}), ({a: DURATION('P11DT60H')})
|
||||
"""
|
||||
When executing query:
|
||||
"""
|
||||
MATCH (n) RETURN n.a ORDER BY n.a
|
||||
"""
|
||||
Then the result should be, in order:
|
||||
| n.a |
|
||||
| P11DT8M |
|
||||
| P12DT3M |
|
||||
| P13DT12H |
|
||||
|
||||
Scenario: Test distinct
|
||||
Given an empty graph
|
||||
And having executed:
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
ldap3==2.6
|
||||
pyyaml==5.4.1
|
||||
pyyaml==5.1.2
|
||||
|
||||
@@ -43,7 +43,7 @@ add_manual_test(expression_pretty_printer.cpp)
|
||||
target_link_libraries(${test_prefix}expression_pretty_printer mg-query)
|
||||
|
||||
add_manual_test(single_query.cpp ${CMAKE_SOURCE_DIR}/src/glue/communication.cpp)
|
||||
target_link_libraries(${test_prefix}single_query mg-query mg-communication mg-license)
|
||||
target_link_libraries(${test_prefix}single_query mg-query mg-communication)
|
||||
|
||||
add_manual_test(stripped_timing.cpp)
|
||||
target_link_libraries(${test_prefix}stripped_timing mg-query)
|
||||
|
||||
@@ -8,12 +8,11 @@ PIP_DEPS=(
|
||||
"behave==1.2.6"
|
||||
"ldap3==2.6"
|
||||
"kafka-python==2.0.2"
|
||||
"requests==2.25.1"
|
||||
"neo4j-driver==4.1.1"
|
||||
"parse==1.18.0"
|
||||
"parse-type==0.5.2"
|
||||
"pytest==6.2.3"
|
||||
"pyyaml==5.4.1"
|
||||
"pyyaml==5.3.1"
|
||||
"six==1.15.0"
|
||||
)
|
||||
cd "$DIR"
|
||||
@@ -29,19 +28,6 @@ set +u
|
||||
source "ve3/bin/activate"
|
||||
set -u
|
||||
|
||||
# https://docs.python.org/3/library/sys.html#sys.version_info
|
||||
PYTHON_MINOR=$(python3 -c 'import sys; print(sys.version_info[:][1])')
|
||||
|
||||
# install pulsar-client
|
||||
# NOTE (2021-11-15): PyPi doesn't contain pulsar-client for Python 3.9 so we have to use
|
||||
# our manually built wheel file. When they update the repository, pulsar-client can be
|
||||
# added as a regular PIP dependancy
|
||||
if [ $PYTHON_MINOR -lt 9 ]; then
|
||||
pip --timeout 1000 install "pulsar-client==2.8.1"
|
||||
else
|
||||
pip --timeout 1000 install https://s3-eu-west-1.amazonaws.com/deps.memgraph.io/pulsar_client-2.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl
|
||||
fi
|
||||
|
||||
for pkg in "${PIP_DEPS[@]}"; do
|
||||
pip --timeout 1000 install "$pkg"
|
||||
done
|
||||
|
||||
@@ -270,10 +270,10 @@ add_unit_test(utils_async_timer.cpp)
|
||||
target_link_libraries(${test_prefix}utils_async_timer mg-utils)
|
||||
|
||||
add_unit_test(utils_license.cpp)
|
||||
target_link_libraries(${test_prefix}utils_license mg-utils mg-license)
|
||||
target_link_libraries(${test_prefix}utils_license mg-utils)
|
||||
|
||||
add_unit_test(utils_settings.cpp)
|
||||
target_link_libraries(${test_prefix}utils_settings mg-utils mg-settings)
|
||||
target_link_libraries(${test_prefix}utils_settings mg-utils)
|
||||
|
||||
add_unit_test(utils_temporal utils_temporal.cpp)
|
||||
target_link_libraries(${test_prefix}utils_temporal mg-utils)
|
||||
@@ -326,7 +326,7 @@ target_link_libraries(${test_prefix}storage_v2_isolation_level mg-storage-v2)
|
||||
|
||||
if (MG_ENTERPRISE)
|
||||
add_unit_test(auth.cpp)
|
||||
target_link_libraries(${test_prefix}auth mg-auth mg-license)
|
||||
target_link_libraries(${test_prefix}auth mg-auth)
|
||||
endif()
|
||||
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
#include "query/typed_value.hpp"
|
||||
|
||||
#include "utils/string.hpp"
|
||||
#include "utils/variant_helpers.hpp"
|
||||
#include "utils/variant.hpp"
|
||||
|
||||
using namespace query;
|
||||
using namespace query::frontend;
|
||||
@@ -3476,7 +3476,7 @@ TEST_P(CypherMainVisitorTest, CreateTriggers) {
|
||||
TestInvalidQuery("CREATE TRIGGER trigger ON UPDTE AFTER COMMIT EXECUTE a", ast_generator);
|
||||
TestInvalidQuery("CREATE TRIGGER trigger ON UPDATE COMMIT EXECUTE a", ast_generator);
|
||||
|
||||
constexpr std::string_view query_template = "CREATE TRIGGER trigger {} {} COMMIT EXECUTE {}";
|
||||
const auto *query_template = "CREATE TRIGGER trigger {} {} COMMIT EXECUTE {}";
|
||||
|
||||
constexpr std::array events{std::pair{"", query::TriggerQuery::EventType::ANY},
|
||||
std::pair{"ON CREATE", query::TriggerQuery::EventType::CREATE},
|
||||
@@ -3567,16 +3567,12 @@ void ValidateMostlyEmptyStreamQuery(Base &ast_generator, const std::string &quer
|
||||
EXPECT_EQ(parsed_query->action_, action);
|
||||
EXPECT_EQ(parsed_query->stream_name_, stream_name);
|
||||
auto topic_names = std::get_if<Expression *>(&parsed_query->topic_names_);
|
||||
EXPECT_NE(topic_names, nullptr);
|
||||
EXPECT_EQ(*topic_names, nullptr);
|
||||
EXPECT_TRUE(topic_names);
|
||||
EXPECT_FALSE(*topic_names);
|
||||
EXPECT_TRUE(parsed_query->transform_name_.empty());
|
||||
EXPECT_TRUE(parsed_query->consumer_group_.empty());
|
||||
EXPECT_EQ(parsed_query->batch_interval_, nullptr);
|
||||
EXPECT_EQ(parsed_query->batch_size_, nullptr);
|
||||
EXPECT_EQ(parsed_query->service_url_, nullptr);
|
||||
EXPECT_EQ(parsed_query->bootstrap_servers_, nullptr);
|
||||
EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->batch_limit_, batch_limit));
|
||||
EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->timeout_, timeout));
|
||||
}
|
||||
@@ -3645,28 +3641,22 @@ TEST_P(CypherMainVisitorTest, StopAllStreams) {
|
||||
ValidateMostlyEmptyStreamQuery(ast_generator, "SToP ALL STReaMS", StreamQuery::Action::STOP_ALL_STREAMS, "");
|
||||
}
|
||||
|
||||
void ValidateTopicNames(const auto &topic_names, const std::vector<std::string> &expected_topic_names,
|
||||
Base &ast_generator) {
|
||||
std::visit(utils::Overloaded{
|
||||
[&](Expression *expression) {
|
||||
ast_generator.CheckLiteral(expression, utils::Join(expected_topic_names, ","));
|
||||
},
|
||||
[&](const std::vector<std::string> &topic_names) { EXPECT_EQ(topic_names, expected_topic_names); }},
|
||||
topic_names);
|
||||
}
|
||||
|
||||
void ValidateCreateKafkaStreamQuery(Base &ast_generator, const std::string &query_string,
|
||||
const std::string_view stream_name, const std::vector<std::string> &topic_names,
|
||||
const std::string_view transform_name, const std::string_view consumer_group,
|
||||
const std::optional<TypedValue> &batch_interval,
|
||||
const std::optional<TypedValue> &batch_size,
|
||||
const std::string_view bootstrap_servers = "") {
|
||||
void ValidateCreateStreamQuery(Base &ast_generator, const std::string &query_string, const std::string_view stream_name,
|
||||
const std::vector<std::string> &topic_names, const std::string_view transform_name,
|
||||
const std::string_view consumer_group, const std::optional<TypedValue> &batch_interval,
|
||||
const std::optional<TypedValue> &batch_size,
|
||||
const std::string_view bootstrap_servers = "") {
|
||||
SCOPED_TRACE(query_string);
|
||||
StreamQuery *parsed_query{nullptr};
|
||||
ASSERT_NO_THROW(parsed_query = dynamic_cast<StreamQuery *>(ast_generator.ParseQuery(query_string))) << query_string;
|
||||
ASSERT_NE(parsed_query, nullptr);
|
||||
EXPECT_EQ(parsed_query->stream_name_, stream_name);
|
||||
ValidateTopicNames(parsed_query->topic_names_, topic_names, ast_generator);
|
||||
|
||||
std::visit(utils::Overloaded{
|
||||
[&](Expression *expression) { ast_generator.CheckLiteral(expression, utils::Join(topic_names, ",")); },
|
||||
[&](const std::vector<std::string> &topic_name_list) { EXPECT_EQ(topic_name_list, topic_names); }},
|
||||
parsed_query->topic_names_);
|
||||
|
||||
EXPECT_EQ(parsed_query->transform_name_, transform_name);
|
||||
EXPECT_EQ(parsed_query->consumer_group_, consumer_group);
|
||||
EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->batch_interval_, batch_interval));
|
||||
@@ -3679,7 +3669,7 @@ void ValidateCreateKafkaStreamQuery(Base &ast_generator, const std::string &quer
|
||||
EXPECT_NE(parsed_query->bootstrap_servers_, nullptr);
|
||||
}
|
||||
|
||||
TEST_P(CypherMainVisitorTest, CreateKafkaStream) {
|
||||
TEST_P(CypherMainVisitorTest, CreateStream) {
|
||||
auto &ast_generator = *GetParam();
|
||||
|
||||
TestInvalidQuery("CREATE KAFKA STREAM", ast_generator);
|
||||
@@ -3698,11 +3688,11 @@ TEST_P(CypherMainVisitorTest, CreateKafkaStream) {
|
||||
TestInvalidQuery("CREATE KAFKA STREAM stream TOPICS topic1 TRANSFORM transform BATCH_INTERVAL", ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE KAFKA STREAM stream TOPICS topic1 TRANSFORM transform BATCH_INTERVAL 'invalid interval'", ast_generator);
|
||||
TestInvalidQuery<SemanticException>("CREATE KAFKA STREAM stream TOPICS topic1 TRANSFORM transform TOPICS topic2",
|
||||
ast_generator);
|
||||
TestInvalidQuery("CREATE KAFKA STREAM stream TOPICS topic1 TRANSFORM transform BATCH_SIZE", ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE KAFKA STREAM stream TOPICS topic1 TRANSFORM transform BATCH_SIZE 'invalid size'", ast_generator);
|
||||
TestInvalidQuery("CREATE KAFKA STREAM stream TOPICS topic1 TRANSFORM transform BATCH_INVERVAL 2 CONSUMER_GROUP Gru",
|
||||
ast_generator);
|
||||
TestInvalidQuery("CREATE KAFKA STREAM stream TOPICS topic1, TRANSFORM transform BATCH_SIZE 2 CONSUMER_GROUP Gru",
|
||||
ast_generator);
|
||||
TestInvalidQuery("CREATE KAFKA STREAM stream TOPICS topic1 TRANSFORM transform BOOTSTRAP_SERVERS localhost:9092",
|
||||
@@ -3724,59 +3714,45 @@ TEST_P(CypherMainVisitorTest, CreateKafkaStream) {
|
||||
|
||||
const auto topic_names_as_str = utils::Join(topic_names, ",");
|
||||
|
||||
ValidateCreateKafkaStreamQuery(
|
||||
ValidateCreateStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {}", kStreamName, topic_names_as_str, kTransformName),
|
||||
kStreamName, topic_names, kTransformName, "", std::nullopt, std::nullopt);
|
||||
|
||||
ValidateCreateKafkaStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} CONSUMER_GROUP {} ",
|
||||
kStreamName, topic_names_as_str, kTransformName, kConsumerGroup),
|
||||
kStreamName, topic_names, kTransformName, kConsumerGroup, std::nullopt,
|
||||
std::nullopt);
|
||||
ValidateCreateStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} CONSUMER_GROUP {} ",
|
||||
kStreamName, topic_names_as_str, kTransformName, kConsumerGroup),
|
||||
kStreamName, topic_names, kTransformName, kConsumerGroup, std::nullopt, std::nullopt);
|
||||
|
||||
ValidateCreateKafkaStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TRANSFORM {} TOPICS {} BATCH_INTERVAL {}",
|
||||
kStreamName, kTransformName, topic_names_as_str, kBatchInterval),
|
||||
kStreamName, topic_names, kTransformName, "", batch_interval_value, std::nullopt);
|
||||
ValidateCreateStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} BATCH_INTERVAL {}",
|
||||
kStreamName, topic_names_as_str, kTransformName, kBatchInterval),
|
||||
kStreamName, topic_names, kTransformName, "", batch_interval_value, std::nullopt);
|
||||
|
||||
ValidateCreateKafkaStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} BATCH_SIZE {} TOPICS {} TRANSFORM {}",
|
||||
kStreamName, kBatchSize, topic_names_as_str, kTransformName),
|
||||
kStreamName, topic_names, kTransformName, "", std::nullopt, batch_size_value);
|
||||
ValidateCreateStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} BATCH_SIZE {}", kStreamName,
|
||||
topic_names_as_str, kTransformName, kBatchSize),
|
||||
kStreamName, topic_names, kTransformName, "", std::nullopt, batch_size_value);
|
||||
|
||||
ValidateCreateKafkaStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS '{}' BATCH_SIZE {} TRANSFORM {}",
|
||||
kStreamName, topic_names_as_str, kBatchSize, kTransformName),
|
||||
kStreamName, topic_names, kTransformName, "", std::nullopt, batch_size_value);
|
||||
|
||||
ValidateCreateKafkaStreamQuery(
|
||||
ValidateCreateStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} CONSUMER_GROUP {} BATCH_INTERVAL {} BATCH_SIZE {}",
|
||||
kStreamName, topic_names_as_str, kTransformName, kConsumerGroup, kBatchInterval, kBatchSize),
|
||||
kStreamName, topic_names, kTransformName, kConsumerGroup, batch_interval_value, batch_size_value);
|
||||
using namespace std::string_literals;
|
||||
const auto host1 = "localhost:9094"s;
|
||||
ValidateCreateKafkaStreamQuery(
|
||||
ValidateCreateStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} CONSUMER_GROUP {} BATCH_SIZE {} BATCH_INTERVAL {} TRANSFORM {} "
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} CONSUMER_GROUP {} BATCH_INTERVAL {} BATCH_SIZE {} "
|
||||
"BOOTSTRAP_SERVERS '{}'",
|
||||
kStreamName, topic_names_as_str, kConsumerGroup, kBatchSize, kBatchInterval, kTransformName, host1),
|
||||
kStreamName, topic_names_as_str, kTransformName, kConsumerGroup, kBatchInterval, kBatchSize, host1),
|
||||
kStreamName, topic_names, kTransformName, kConsumerGroup, batch_interval_value, batch_size_value, host1);
|
||||
|
||||
ValidateCreateKafkaStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} CONSUMER_GROUP {} TOPICS {} BATCH_INTERVAL {} TRANSFORM {} BATCH_SIZE {} "
|
||||
"BOOTSTRAP_SERVERS '{}'",
|
||||
kStreamName, kConsumerGroup, topic_names_as_str, kBatchInterval, kTransformName, kBatchSize, host1),
|
||||
kStreamName, topic_names, kTransformName, kConsumerGroup, batch_interval_value, batch_size_value, host1);
|
||||
|
||||
const auto host2 = "localhost:9094,localhost:1994,168.1.1.256:345"s;
|
||||
ValidateCreateKafkaStreamQuery(
|
||||
ValidateCreateStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} BOOTSTRAP_SERVERS '{}' CONSUMER_GROUP {} TRANSFORM {} "
|
||||
"BATCH_INTERVAL {} BATCH_SIZE {}",
|
||||
kStreamName, topic_names_as_str, host2, kConsumerGroup, kTransformName, kBatchInterval, kBatchSize),
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} CONSUMER_GROUP {} BATCH_INTERVAL {} BATCH_SIZE {} "
|
||||
"BOOTSTRAP_SERVERS '{}'",
|
||||
kStreamName, topic_names_as_str, kTransformName, kConsumerGroup, kBatchInterval, kBatchSize, host2),
|
||||
kStreamName, topic_names, kTransformName, kConsumerGroup, batch_interval_value, batch_size_value, host2);
|
||||
};
|
||||
|
||||
@@ -3788,11 +3764,10 @@ TEST_P(CypherMainVisitorTest, CreateKafkaStream) {
|
||||
|
||||
auto check_consumer_group = [&](const std::string_view consumer_group) {
|
||||
const std::string kTopicName{"topic1"};
|
||||
ValidateCreateKafkaStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} CONSUMER_GROUP {}",
|
||||
kStreamName, kTopicName, kTransformName, consumer_group),
|
||||
kStreamName, {kTopicName}, kTransformName, consumer_group, std::nullopt,
|
||||
std::nullopt);
|
||||
ValidateCreateStreamQuery(ast_generator,
|
||||
fmt::format("CREATE KAFKA STREAM {} TOPICS {} TRANSFORM {} CONSUMER_GROUP {}",
|
||||
kStreamName, kTopicName, kTransformName, consumer_group),
|
||||
kStreamName, {kTopicName}, kTransformName, consumer_group, std::nullopt, std::nullopt);
|
||||
};
|
||||
|
||||
using namespace std::literals;
|
||||
@@ -3804,129 +3779,6 @@ TEST_P(CypherMainVisitorTest, CreateKafkaStream) {
|
||||
}
|
||||
}
|
||||
|
||||
void ValidateCreatePulsarStreamQuery(Base &ast_generator, const std::string &query_string,
|
||||
const std::string_view stream_name, const std::vector<std::string> &topic_names,
|
||||
const std::string_view transform_name,
|
||||
const std::optional<TypedValue> &batch_interval,
|
||||
const std::optional<TypedValue> &batch_size, const std::string_view service_url) {
|
||||
SCOPED_TRACE(query_string);
|
||||
|
||||
StreamQuery *parsed_query{nullptr};
|
||||
ASSERT_NO_THROW(parsed_query = dynamic_cast<StreamQuery *>(ast_generator.ParseQuery(query_string))) << query_string;
|
||||
ASSERT_NE(parsed_query, nullptr);
|
||||
EXPECT_EQ(parsed_query->stream_name_, stream_name);
|
||||
ValidateTopicNames(parsed_query->topic_names_, topic_names, ast_generator);
|
||||
EXPECT_EQ(parsed_query->transform_name_, transform_name);
|
||||
EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->batch_interval_, batch_interval));
|
||||
EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->batch_size_, batch_size));
|
||||
EXPECT_EQ(parsed_query->batch_limit_, nullptr);
|
||||
if (service_url.empty()) {
|
||||
EXPECT_EQ(parsed_query->service_url_, nullptr);
|
||||
return;
|
||||
}
|
||||
EXPECT_NE(parsed_query->service_url_, nullptr);
|
||||
}
|
||||
|
||||
TEST_P(CypherMainVisitorTest, CreatePulsarStream) {
|
||||
auto &ast_generator = *GetParam();
|
||||
|
||||
TestInvalidQuery("CREATE PULSAR STREAM", ast_generator);
|
||||
TestInvalidQuery<SemanticException>("CREATE PULSAR STREAM stream", ast_generator);
|
||||
TestInvalidQuery("CREATE PULSAR STREAM stream TOPICS", ast_generator);
|
||||
TestInvalidQuery<SemanticException>("CREATE PULSAR STREAM stream TOPICS topic_name", ast_generator);
|
||||
TestInvalidQuery("CREATE PULSAR STREAM stream TOPICS topic_name TRANSFORM", ast_generator);
|
||||
TestInvalidQuery("CREATE PULSAR STREAM stream TOPICS topic_name TRANSFORM transform.name SERVICE_URL", ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE PULSAR STREAM stream TOPICS topic_name TRANSFORM transform.name SERVICE_URL 1", ast_generator);
|
||||
TestInvalidQuery(
|
||||
"CREATE PULSAR STREAM stream TOPICS topic_name TRANSFORM transform.name BOOTSTRAP_SERVERS 'bootstrap'",
|
||||
ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE PULSAR STREAM stream TOPICS topic_name TRANSFORM transform.name SERVICE_URL 'test' TOPICS topic_name",
|
||||
ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE PULSAR STREAM stream TRANSFORM transform.name TOPICS topic_name TRANSFORM transform.name SERVICE_URL "
|
||||
"'test'",
|
||||
ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE PULSAR STREAM stream BATCH_INTERVAL 1 TOPICS topic_name TRANSFORM transform.name SERVICE_URL 'test' "
|
||||
"BATCH_INTERVAL 1000",
|
||||
ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE PULSAR STREAM stream BATCH_INTERVAL 'a' TOPICS topic_name TRANSFORM transform.name SERVICE_URL 'test'",
|
||||
ast_generator);
|
||||
TestInvalidQuery<SemanticException>(
|
||||
"CREATE PULSAR STREAM stream BATCH_SIZE 'a' TOPICS topic_name TRANSFORM transform.name SERVICE_URL 'test'",
|
||||
ast_generator);
|
||||
|
||||
const std::vector<std::string> topic_names{"topic1", "topic2"};
|
||||
const std::string topic_names_str = utils::Join(topic_names, ",");
|
||||
constexpr std::string_view kStreamName{"PulsarStream"};
|
||||
constexpr std::string_view kTransformName{"boringTransformation"};
|
||||
constexpr std::string_view kServiceUrl{"localhost"};
|
||||
constexpr int kBatchSize{1000};
|
||||
constexpr int kBatchInterval{231321};
|
||||
|
||||
{
|
||||
SCOPED_TRACE("single topic");
|
||||
ValidateCreatePulsarStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} TOPICS {} TRANSFORM {}", kStreamName, topic_names[0], kTransformName),
|
||||
kStreamName, {topic_names[0]}, kTransformName, std::nullopt, std::nullopt, "");
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("multiple topics");
|
||||
ValidateCreatePulsarStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} TRANSFORM {} TOPICS {}", kStreamName, kTransformName, topic_names_str),
|
||||
kStreamName, topic_names, kTransformName, std::nullopt, std::nullopt, "");
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("topic name in string");
|
||||
ValidateCreatePulsarStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} TRANSFORM {} TOPICS '{}'", kStreamName, kTransformName, topic_names_str),
|
||||
kStreamName, topic_names, kTransformName, std::nullopt, std::nullopt, "");
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("service url");
|
||||
ValidateCreatePulsarStreamQuery(ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} SERVICE_URL '{}' TRANSFORM {} TOPICS {}",
|
||||
kStreamName, kServiceUrl, kTransformName, topic_names_str),
|
||||
kStreamName, topic_names, kTransformName, std::nullopt, std::nullopt, kServiceUrl);
|
||||
ValidateCreatePulsarStreamQuery(ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} TRANSFORM {} SERVICE_URL '{}' TOPICS {}",
|
||||
kStreamName, kTransformName, kServiceUrl, topic_names_str),
|
||||
kStreamName, topic_names, kTransformName, std::nullopt, std::nullopt, kServiceUrl);
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("batch size");
|
||||
ValidateCreatePulsarStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} SERVICE_URL '{}' BATCH_SIZE {} TRANSFORM {} TOPICS {}", kStreamName,
|
||||
kServiceUrl, kBatchSize, kTransformName, topic_names_str),
|
||||
kStreamName, topic_names, kTransformName, std::nullopt, TypedValue(kBatchSize), kServiceUrl);
|
||||
ValidateCreatePulsarStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} TRANSFORM {} SERVICE_URL '{}' TOPICS {} BATCH_SIZE {}", kStreamName,
|
||||
kTransformName, kServiceUrl, topic_names_str, kBatchSize),
|
||||
kStreamName, topic_names, kTransformName, std::nullopt, TypedValue(kBatchSize), kServiceUrl);
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("batch interval");
|
||||
ValidateCreatePulsarStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} BATCH_INTERVAL {} SERVICE_URL '{}' BATCH_SIZE {} TRANSFORM {} TOPICS {}",
|
||||
kStreamName, kBatchInterval, kServiceUrl, kBatchSize, kTransformName, topic_names_str),
|
||||
kStreamName, topic_names, kTransformName, TypedValue(kBatchInterval), TypedValue(kBatchSize), kServiceUrl);
|
||||
ValidateCreatePulsarStreamQuery(
|
||||
ast_generator,
|
||||
fmt::format("CREATE PULSAR STREAM {} TRANSFORM {} SERVICE_URL '{}' BATCH_INTERVAL {} TOPICS {} BATCH_SIZE {}",
|
||||
kStreamName, kTransformName, kServiceUrl, kBatchInterval, topic_names_str, kBatchSize),
|
||||
kStreamName, topic_names, kTransformName, TypedValue(kBatchInterval), TypedValue(kBatchSize), kServiceUrl);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(CypherMainVisitorTest, CheckStream) {
|
||||
auto &ast_generator = *GetParam();
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <spdlog/common.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "integrations/kafka/consumer.hpp"
|
||||
#include "integrations/kafka/exceptions.hpp"
|
||||
#include "kafka_mock.hpp"
|
||||
@@ -41,10 +40,6 @@ int SpanToInt(std::span<const char> span) {
|
||||
std::memcpy(&result, span.data(), sizeof(int));
|
||||
return result;
|
||||
}
|
||||
|
||||
constexpr std::chrono::milliseconds kDefaultBatchInterval{100};
|
||||
constexpr int64_t kDefaultBatchSize{1000};
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ConsumerTest : public ::testing::Test {
|
||||
@@ -57,8 +52,8 @@ struct ConsumerTest : public ::testing::Test {
|
||||
.topics = {kTopicName},
|
||||
.consumer_group = "ConsumerGroup " + test_name,
|
||||
.bootstrap_servers = cluster.Bootstraps(),
|
||||
.batch_interval = kDefaultBatchInterval,
|
||||
.batch_size = kDefaultBatchSize,
|
||||
.batch_interval = std::nullopt,
|
||||
.batch_size = std::nullopt,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -291,7 +286,7 @@ TEST_F(ConsumerTest, InvalidBootstrapServers) {
|
||||
|
||||
TEST_F(ConsumerTest, InvalidTopic) {
|
||||
auto info = CreateDefaultConsumerInfo();
|
||||
info.topics = {"Nonexistingtopic"};
|
||||
info.topics = {"Non existing topic"};
|
||||
EXPECT_THROW(Consumer(std::move(info), kDummyConsumerFunction), TopicNotFoundException);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
|
||||
@@ -434,7 +433,7 @@ TEST_F(InterpreterTest, Bfs) {
|
||||
TEST_F(InterpreterTest, ShortestPath) {
|
||||
const auto test_shortest_path = [this](const bool use_duration) {
|
||||
const auto get_weight = [use_duration](const auto value) {
|
||||
return fmt::format(fmt::runtime(use_duration ? "DURATION('PT{}S')" : "{}"), value);
|
||||
return fmt::format(use_duration ? "DURATION('PT{}S')" : "{}", value);
|
||||
};
|
||||
|
||||
Interpret(
|
||||
@@ -1106,364 +1105,3 @@ TEST_F(InterpreterTest, AllowLoadCsvConfig) {
|
||||
check_load_csv_queries(true);
|
||||
check_load_csv_queries(false);
|
||||
}
|
||||
|
||||
void AssertAllValuesAreZero(const std::map<std::string, communication::bolt::Value> &map,
|
||||
const std::vector<std::string> &exceptions) {
|
||||
for (const auto &[key, value] : map) {
|
||||
if (const auto it = std::find(exceptions.begin(), exceptions.end(), key); it != exceptions.end()) continue;
|
||||
ASSERT_EQ(value.ValueInt(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, ExecutionStatsIsValid) {
|
||||
{
|
||||
auto [stream, qid] = Prepare("MATCH (n) DELETE n;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("stats"), 0);
|
||||
}
|
||||
{
|
||||
std::array stats_keys{"nodes-created", "nodes-deleted", "relationships-created", "relationships-deleted",
|
||||
"properties-set", "labels-added", "labels-removed"};
|
||||
auto [stream, qid] = Prepare("CREATE ();");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("stats"), 1);
|
||||
ASSERT_TRUE(stream.GetSummary().at("stats").IsMap());
|
||||
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||
ASSERT_TRUE(
|
||||
std::all_of(stats_keys.begin(), stats_keys.end(), [&stats](const auto &key) { return stats.contains(key); }));
|
||||
AssertAllValuesAreZero(stats, {"nodes-created"});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, ExecutionStatsValues) {
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE (),(),(),();");
|
||||
|
||||
Pull(&stream);
|
||||
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||
ASSERT_EQ(stats["nodes-created"].ValueInt(), 4);
|
||||
AssertAllValuesAreZero(stats, {"nodes-created"});
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("MATCH (n) DELETE n;");
|
||||
Pull(&stream);
|
||||
|
||||
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||
ASSERT_EQ(stats["nodes-deleted"].ValueInt(), 4);
|
||||
AssertAllValuesAreZero(stats, {"nodes-deleted"});
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE (n)-[:TO]->(m), (n)-[:TO]->(m), (n)-[:TO]->(m);");
|
||||
Pull(&stream);
|
||||
|
||||
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||
ASSERT_EQ(stats["nodes-created"].ValueInt(), 2);
|
||||
ASSERT_EQ(stats["relationships-created"].ValueInt(), 3);
|
||||
AssertAllValuesAreZero(stats, {"nodes-created", "relationships-created"});
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("MATCH (n) DETACH DELETE n;");
|
||||
Pull(&stream);
|
||||
|
||||
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||
ASSERT_EQ(stats["nodes-deleted"].ValueInt(), 2);
|
||||
ASSERT_EQ(stats["relationships-deleted"].ValueInt(), 3);
|
||||
AssertAllValuesAreZero(stats, {"nodes-deleted", "relationships-deleted"});
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE (:L1:L2:L3), (:L1), (:L1), (:L2);");
|
||||
Pull(&stream);
|
||||
|
||||
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||
ASSERT_EQ(stats["nodes-created"].ValueInt(), 4);
|
||||
ASSERT_EQ(stats["labels-added"].ValueInt(), 6);
|
||||
AssertAllValuesAreZero(stats, {"nodes-created", "labels-added"});
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("MATCH (n:L1) SET n.name='test';");
|
||||
Pull(&stream);
|
||||
|
||||
auto stats = stream.GetSummary().at("stats").ValueMap();
|
||||
ASSERT_EQ(stats["properties-set"].ValueInt(), 3);
|
||||
AssertAllValuesAreZero(stats, {"properties-set"});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, NotificationsValidStructure) {
|
||||
{
|
||||
auto [stream, qid] = Prepare("MATCH (n) DELETE n;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 0);
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE INDEX ON :Person(id);");
|
||||
Pull(&stream);
|
||||
|
||||
// Assert notifications list
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
ASSERT_TRUE(stream.GetSummary().at("notifications").IsList());
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
// Assert one notification structure
|
||||
ASSERT_EQ(notifications.size(), 1);
|
||||
ASSERT_TRUE(notifications[0].IsMap());
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_TRUE(notification.contains("severity"));
|
||||
ASSERT_TRUE(notification.contains("code"));
|
||||
ASSERT_TRUE(notification.contains("title"));
|
||||
ASSERT_TRUE(notification.contains("description"));
|
||||
ASSERT_TRUE(notification["severity"].IsString());
|
||||
ASSERT_TRUE(notification["code"].IsString());
|
||||
ASSERT_TRUE(notification["title"].IsString());
|
||||
ASSERT_TRUE(notification["description"].IsString());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, IndexInfoNotifications) {
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE INDEX ON :Person;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "CreateIndex");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Created index on label Person on properties .");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE INDEX ON :Person(id);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "CreateIndex");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Created index on label Person on properties id.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE INDEX ON :Person(id);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "IndexAlreadyExists");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Index on label Person on properties id already exists.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("DROP INDEX ON :Person(id);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "DropIndex");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Dropped index on label Person on properties id.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("DROP INDEX ON :Person(id);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "IndexDoesNotExist");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Index on label Person on properties id doesn't exist.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, ConstraintUniqueInfoNotifications) {
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE CONSTRAINT ON (n:Person) ASSERT n.email, n.id IS UNIQUE;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "CreateConstraint");
|
||||
ASSERT_EQ(notification["title"].ValueString(),
|
||||
"Created UNIQUE constraint on label Person on properties email, id.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE CONSTRAINT ON (n:Person) ASSERT n.email, n.id IS UNIQUE;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "ConstraintAlreadyExists");
|
||||
ASSERT_EQ(notification["title"].ValueString(),
|
||||
"Constraint UNIQUE on label Person on properties email, id already exists.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("DROP CONSTRAINT ON (n:Person) ASSERT n.email, n.id IS UNIQUE;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "DropConstraint");
|
||||
ASSERT_EQ(notification["title"].ValueString(),
|
||||
"Dropped UNIQUE constraint on label Person on properties email, id.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("DROP CONSTRAINT ON (n:Person) ASSERT n.email, n.id IS UNIQUE;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "ConstraintDoesNotExist");
|
||||
ASSERT_EQ(notification["title"].ValueString(),
|
||||
"Constraint UNIQUE on label Person on properties email, id doesn't exist.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, ConstraintExistsInfoNotifications) {
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE CONSTRAINT ON (n:L1) ASSERT EXISTS (n.name);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "CreateConstraint");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Created EXISTS constraint on label L1 on properties name.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("CREATE CONSTRAINT ON (n:L1) ASSERT EXISTS (n.name);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "ConstraintAlreadyExists");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Constraint EXISTS on label L1 on properties name already exists.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("DROP CONSTRAINT ON (n:L1) ASSERT EXISTS (n.name);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "DropConstraint");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Dropped EXISTS constraint on label L1 on properties name.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("DROP CONSTRAINT ON (n:L1) ASSERT EXISTS (n.name);");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "ConstraintDoesNotExist");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Constraint EXISTS on label L1 on properties name doesn't exist.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, TriggerInfoNotifications) {
|
||||
{
|
||||
auto [stream, qid] = Prepare(
|
||||
"CREATE TRIGGER bestTriggerEver ON CREATE AFTER COMMIT EXECUTE "
|
||||
"CREATE ();");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "CreateTrigger");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Created trigger bestTriggerEver.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
{
|
||||
auto [stream, qid] = Prepare("DROP TRIGGER bestTriggerEver;");
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "DropTrigger");
|
||||
ASSERT_EQ(notification["title"].ValueString(), "Dropped trigger bestTriggerEver.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(InterpreterTest, LoadCsvClauseNotification) {
|
||||
auto dir_manager = TmpDirManager("csv_directory");
|
||||
const auto csv_path = dir_manager.Path() / "file.csv";
|
||||
auto writer = FileWriter(csv_path);
|
||||
|
||||
const std::string delimiter{"|"};
|
||||
|
||||
const std::vector<std::string> header{"A", "B", "C"};
|
||||
writer.WriteLine(CreateRow(header, delimiter));
|
||||
|
||||
const std::vector<std::string> good_columns_1{"a", "b", "c"};
|
||||
writer.WriteLine(CreateRow(good_columns_1, delimiter));
|
||||
|
||||
writer.Close();
|
||||
|
||||
const std::string query = fmt::format(R"(LOAD CSV FROM "{}" WITH HEADER IGNORE BAD DELIMITER "{}" AS x RETURN x;)",
|
||||
csv_path.string(), delimiter);
|
||||
auto [stream, qid] = Prepare(query);
|
||||
Pull(&stream);
|
||||
|
||||
ASSERT_EQ(stream.GetSummary().count("notifications"), 1);
|
||||
auto notifications = stream.GetSummary().at("notifications").ValueList();
|
||||
|
||||
auto notification = notifications[0].ValueMap();
|
||||
ASSERT_EQ(notification["severity"].ValueString(), "INFO");
|
||||
ASSERT_EQ(notification["code"].ValueString(), "LoadCSVTip");
|
||||
ASSERT_EQ(notification["title"].ValueString(),
|
||||
"It's important to note that the parser parses the values as strings. It's up to the user to "
|
||||
"convert the parsed row values to the appropriate type. This can be done using the built-in "
|
||||
"conversion functions such as ToInteger, ToFloat, ToBoolean etc.");
|
||||
ASSERT_EQ(notification["description"].ValueString(), "");
|
||||
}
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
#include "gtest/gtest.h"
|
||||
#include "integrations/kafka/consumer.hpp"
|
||||
#include "query/procedure/mg_procedure_impl.hpp"
|
||||
#include "query/stream/common.hpp"
|
||||
#include "test_utils.hpp"
|
||||
#include "utils/pmr/vector.hpp"
|
||||
|
||||
@@ -32,12 +31,12 @@
|
||||
/// [[noreturn]] and throw an std::logic_error exception.
|
||||
class MockedRdKafkaMessage : public RdKafka::Message {
|
||||
public:
|
||||
explicit MockedRdKafkaMessage(std::string key, std::string payload, int64_t offset)
|
||||
explicit MockedRdKafkaMessage(std::string key, std::string payload)
|
||||
: key_(std::move(key)), payload_(std::move(payload)) {
|
||||
message_.err = rd_kafka_resp_err_t::RD_KAFKA_RESP_ERR__BEGIN;
|
||||
message_.key = static_cast<void *>(key_.data());
|
||||
message_.key_len = key_.size();
|
||||
message_.offset = offset;
|
||||
message_.offset = 0;
|
||||
message_.payload = static_cast<void *>(payload_.data());
|
||||
message_.len = payload_.size();
|
||||
rd_kafka_ = rd_kafka_new(rd_kafka_type_t::RD_KAFKA_CONSUMER, nullptr, nullptr, 0);
|
||||
@@ -123,19 +122,16 @@ class MgpApiTest : public ::testing::Test {
|
||||
const char key;
|
||||
const char *topic_name;
|
||||
const size_t payload_size;
|
||||
const int64_t offset;
|
||||
};
|
||||
|
||||
static constexpr std::array<ExpectedResult, 2> expected = {ExpectedResult{"payload1", '1', "Topic1", 8, 0},
|
||||
ExpectedResult{"payload2", '2', "Topic1", 8, 1}};
|
||||
static constexpr std::array<ExpectedResult, 2> expected = {ExpectedResult{"payload1", '1', "Topic1", 8},
|
||||
ExpectedResult{"payload2", '2', "Topic1", 8}};
|
||||
|
||||
private:
|
||||
utils::pmr::vector<mgp_message> CreateMockedBatch() {
|
||||
std::transform(
|
||||
expected.begin(), expected.end(), std::back_inserter(msgs_storage_),
|
||||
[i = int64_t(0)](const auto expected) mutable {
|
||||
return Message(std::make_unique<KafkaMessage>(std::string(1, expected.key), expected.payload, i++));
|
||||
});
|
||||
std::transform(expected.begin(), expected.end(), std::back_inserter(msgs_storage_), [](const auto expected) {
|
||||
return Message(std::make_unique<KafkaMessage>(std::string(1, expected.key), expected.payload));
|
||||
});
|
||||
auto v = utils::pmr::vector<mgp_message>(utils::NewDeleteResource());
|
||||
v.reserve(expected.size());
|
||||
std::transform(msgs_storage_.begin(), msgs_storage_.end(), std::back_inserter(v),
|
||||
@@ -157,8 +153,6 @@ TEST_F(MgpApiTest, TestAllMgpKafkaCApi) {
|
||||
EXPECT_EQ(EXPECT_MGP_NO_ERROR(size_t, mgp_message_key_size, message), 1);
|
||||
EXPECT_EQ(*EXPECT_MGP_NO_ERROR(const char *, mgp_message_key, message), expected[i].key);
|
||||
|
||||
// Test for source type
|
||||
EXPECT_EQ(EXPECT_MGP_NO_ERROR(mgp_source_type, mgp_message_source_type, message), mgp_source_type::KAFKA);
|
||||
// Test for payload size
|
||||
EXPECT_EQ(EXPECT_MGP_NO_ERROR(size_t, mgp_message_payload_size, message), expected[i].payload_size);
|
||||
// Test for payload
|
||||
@@ -166,8 +160,6 @@ TEST_F(MgpApiTest, TestAllMgpKafkaCApi) {
|
||||
// Test for topic name
|
||||
EXPECT_FALSE(
|
||||
std::strcmp(EXPECT_MGP_NO_ERROR(const char *, mgp_message_topic_name, message), expected[i].topic_name));
|
||||
// Test for offset
|
||||
EXPECT_EQ(EXPECT_MGP_NO_ERROR(int64_t, mgp_message_offset, message), expected[i].offset);
|
||||
}
|
||||
|
||||
// Unfortunately, we can't test timestamp here because we can't mock (as explained above)
|
||||
|
||||
@@ -22,9 +22,9 @@
|
||||
#include "query/stream/streams.hpp"
|
||||
#include "storage/v2/storage.hpp"
|
||||
|
||||
using Streams = query::stream::Streams;
|
||||
using StreamInfo = query::stream::KafkaStream::StreamInfo;
|
||||
using StreamStatus = query::stream::StreamStatus<query::stream::KafkaStream>;
|
||||
using Streams = query::Streams;
|
||||
using StreamInfo = query::KafkaStream::StreamInfo;
|
||||
using StreamStatus = query::StreamStatus<query::KafkaStream>;
|
||||
namespace {
|
||||
const static std::string kTopicName{"TrialTopic"};
|
||||
|
||||
@@ -90,8 +90,8 @@ class StreamsTest : public ::testing::Test {
|
||||
|
||||
StreamInfo CreateDefaultStreamInfo() {
|
||||
return StreamInfo{.common_info{
|
||||
.batch_interval = query::stream::kDefaultBatchInterval,
|
||||
.batch_size = query::stream::kDefaultBatchSize,
|
||||
.batch_interval = std::nullopt,
|
||||
.batch_size = std::nullopt,
|
||||
.transformation_name = "not used in the tests",
|
||||
},
|
||||
.topics = {kTopicName},
|
||||
@@ -111,7 +111,7 @@ class StreamsTest : public ::testing::Test {
|
||||
|
||||
TEST_F(StreamsTest, SimpleStreamManagement) {
|
||||
auto check_data = CreateDefaultStreamCheckData();
|
||||
streams_->Create<query::stream::KafkaStream>(check_data.name, check_data.info, check_data.owner);
|
||||
streams_->Create<query::KafkaStream>(check_data.name, check_data.info, check_data.owner);
|
||||
EXPECT_NO_FATAL_FAILURE(CheckStreamStatus(check_data));
|
||||
|
||||
EXPECT_NO_THROW(streams_->Start(check_data.name));
|
||||
@@ -137,12 +137,12 @@ TEST_F(StreamsTest, SimpleStreamManagement) {
|
||||
TEST_F(StreamsTest, CreateAlreadyExisting) {
|
||||
auto stream_info = CreateDefaultStreamInfo();
|
||||
auto stream_name = GetDefaultStreamName();
|
||||
streams_->Create<query::stream::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
streams_->Create<query::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
|
||||
try {
|
||||
streams_->Create<query::stream::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
streams_->Create<query::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
FAIL() << "Creating already existing stream should throw\n";
|
||||
} catch (query::stream::StreamsException &exception) {
|
||||
} catch (query::StreamsException &exception) {
|
||||
EXPECT_EQ(exception.what(), fmt::format("Stream already exists with name '{}'", stream_name));
|
||||
}
|
||||
}
|
||||
@@ -151,12 +151,12 @@ TEST_F(StreamsTest, DropNotExistingStream) {
|
||||
const auto stream_info = CreateDefaultStreamInfo();
|
||||
const auto stream_name = GetDefaultStreamName();
|
||||
const std::string not_existing_stream_name{"ThisDoesn'tExists"};
|
||||
streams_->Create<query::stream::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
streams_->Create<query::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
|
||||
try {
|
||||
streams_->Drop(not_existing_stream_name);
|
||||
FAIL() << "Dropping not existing stream should throw\n";
|
||||
} catch (query::stream::StreamsException &exception) {
|
||||
} catch (query::StreamsException &exception) {
|
||||
EXPECT_EQ(exception.what(), fmt::format("Couldn't find stream '{}'", not_existing_stream_name));
|
||||
}
|
||||
}
|
||||
@@ -187,7 +187,8 @@ TEST_F(StreamsTest, RestoreStreams) {
|
||||
|
||||
mock_cluster_.CreateTopic(stream_info.topics[0]);
|
||||
}
|
||||
|
||||
stream_check_datas[1].info.common_info.batch_interval = {};
|
||||
stream_check_datas[2].info.common_info.batch_size = {};
|
||||
stream_check_datas[3].owner = {};
|
||||
|
||||
const auto check_restore_logic = [&stream_check_datas, this]() {
|
||||
@@ -205,7 +206,7 @@ TEST_F(StreamsTest, RestoreStreams) {
|
||||
EXPECT_TRUE(streams_->GetStreamInfo().empty());
|
||||
|
||||
for (auto &check_data : stream_check_datas) {
|
||||
streams_->Create<query::stream::KafkaStream>(check_data.name, check_data.info, check_data.owner);
|
||||
streams_->Create<query::KafkaStream>(check_data.name, check_data.info, check_data.owner);
|
||||
}
|
||||
{
|
||||
SCOPED_TRACE("After streams are created");
|
||||
@@ -241,7 +242,7 @@ TEST_F(StreamsTest, RestoreStreams) {
|
||||
TEST_F(StreamsTest, CheckWithTimeout) {
|
||||
const auto stream_info = CreateDefaultStreamInfo();
|
||||
const auto stream_name = GetDefaultStreamName();
|
||||
streams_->Create<query::stream::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
streams_->Create<query::KafkaStream>(stream_name, stream_info, std::nullopt);
|
||||
|
||||
std::chrono::milliseconds timeout{3000};
|
||||
|
||||
|
||||
@@ -234,8 +234,7 @@ TEST(TemporalTest, DateParsing) {
|
||||
TEST(TemporalTest, LocalTimeParsing) {
|
||||
for (const auto &[string, local_time_parameters] : parsing_test_local_time_extended) {
|
||||
ASSERT_EQ(utils::ParseLocalTimeParameters(string).first, local_time_parameters) << ToString(local_time_parameters);
|
||||
const auto time_string = fmt::format("T{}", string);
|
||||
ASSERT_EQ(utils::ParseLocalTimeParameters(time_string).first, local_time_parameters)
|
||||
ASSERT_EQ(utils::ParseLocalTimeParameters(fmt::format("T{}", string)).first, local_time_parameters)
|
||||
<< ToString(local_time_parameters);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user