Use transformations in streams and CHECK STREAM (#185)

* Use the correct transformation result type

* Execute the result queries in streams

* Change the result type of parameters to nullable map

* Serialize transformation name

* Fix order of transformation parameters

* Use actual transformation in Streams

* Clear the Python transformations under GIL

* Add CHECK STREAM query

* Handle missing record fields properly
This commit is contained in:
János Benjamin Antal 2021-07-05 11:42:40 +02:00 committed by Antonio Andelic
parent a37755ce43
commit ad32db5168
22 changed files with 289 additions and 115 deletions

View File

@ -190,7 +190,8 @@ class Edge:
def __init__(self, edge): def __init__(self, edge):
if not isinstance(edge, _mgp.Edge): if not isinstance(edge, _mgp.Edge):
raise TypeError("Expected '_mgp.Edge', got '{}'".format(type(edge))) raise TypeError(
"Expected '_mgp.Edge', got '{}'".format(type(edge)))
self._edge = edge self._edge = edge
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
@ -268,7 +269,8 @@ class Vertex:
def __init__(self, vertex): def __init__(self, vertex):
if not isinstance(vertex, _mgp.Vertex): if not isinstance(vertex, _mgp.Vertex):
raise TypeError("Expected '_mgp.Vertex', got '{}'".format(type(vertex))) raise TypeError(
"Expected '_mgp.Vertex', got '{}'".format(type(vertex)))
self._vertex = vertex self._vertex = vertex
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
@ -404,7 +406,8 @@ class Path:
passed in edge is invalid. passed in edge is invalid.
''' '''
if not isinstance(edge, Edge): if not isinstance(edge, Edge):
raise TypeError("Expected '_mgp.Edge', got '{}'".format(type(edge))) raise TypeError(
"Expected '_mgp.Edge', got '{}'".format(type(edge)))
if not self.is_valid() or not edge.is_valid(): if not self.is_valid() or not edge.is_valid():
raise InvalidContextError() raise InvalidContextError()
self._path.expand(edge._edge) self._path.expand(edge._edge)
@ -454,7 +457,8 @@ class Vertices:
def __init__(self, graph): def __init__(self, graph):
if not isinstance(graph, _mgp.Graph): if not isinstance(graph, _mgp.Graph):
raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) raise TypeError(
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
self._graph = graph self._graph = graph
self._len = None self._len = None
@ -499,7 +503,8 @@ class Graph:
def __init__(self, graph): def __init__(self, graph):
if not isinstance(graph, _mgp.Graph): if not isinstance(graph, _mgp.Graph):
raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) raise TypeError(
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
self._graph = graph self._graph = graph
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
@ -557,7 +562,8 @@ class ProcCtx:
def __init__(self, graph): def __init__(self, graph):
if not isinstance(graph, _mgp.Graph): if not isinstance(graph, _mgp.Graph):
raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) raise TypeError(
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
self._graph = Graph(graph) self._graph = Graph(graph)
def is_valid(self) -> bool: def is_valid(self) -> bool:
@ -676,11 +682,13 @@ def _typing_to_cypher_type(type_):
type_args_as_str = parse_type_args(type_as_str) type_args_as_str = parse_type_args(type_as_str)
none_type_as_str = type(None).__name__ none_type_as_str = type(None).__name__
if none_type_as_str in type_args_as_str: if none_type_as_str in type_args_as_str:
types = tuple(t for t in type_args_as_str if t != none_type_as_str) types = tuple(
t for t in type_args_as_str if t != none_type_as_str)
if len(types) == 1: if len(types) == 1:
type_arg_as_str, = types type_arg_as_str, = types
else: else:
type_arg_as_str = 'typing.Union[' + ', '.join(types) + ']' type_arg_as_str = 'typing.Union[' + \
', '.join(types) + ']'
simple_type = get_simple_type(type_arg_as_str) simple_type = get_simple_type(type_arg_as_str)
if simple_type is not None: if simple_type is not None:
return _mgp.type_nullable(simple_type) return _mgp.type_nullable(simple_type)
@ -726,6 +734,7 @@ def raise_if_does_not_meet_requirements(func: typing.Callable[..., Record]):
if inspect.isgeneratorfunction(func): if inspect.isgeneratorfunction(func):
raise NotImplementedError("Generator functions are not supported") raise NotImplementedError("Generator functions are not supported")
def read_proc(func: typing.Callable[..., Record]): def read_proc(func: typing.Callable[..., Record]):
''' '''
Register `func` as a a read-only procedure of the current module. Register `func` as a a read-only procedure of the current module.
@ -803,17 +812,20 @@ def read_proc(func: typing.Callable[..., Record]):
mgp_proc.add_result(name, _typing_to_cypher_type(type_)) mgp_proc.add_result(name, _typing_to_cypher_type(type_))
return func return func
class InvalidMessageError(Exception): class InvalidMessageError(Exception):
'''Signals using a message instance outside of the registered transformation.''' '''Signals using a message instance outside of the registered transformation.'''
pass pass
class Message: class Message:
'''Represents a message from a stream.''' '''Represents a message from a stream.'''
__slots__ = ('_message',) __slots__ = ('_message',)
def __init__(self, message): def __init__(self, message):
if not isinstance(message, _mgp.Message): if not isinstance(message, _mgp.Message):
raise TypeError("Expected '_mgp.Message', got '{}'".format(type(message))) raise TypeError(
"Expected '_mgp.Message', got '{}'".format(type(message)))
self._message = message self._message = message
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
@ -829,34 +841,37 @@ class Message:
def payload(self) -> bytes: def payload(self) -> bytes:
if not self.is_valid(): if not self.is_valid():
raise InvalidMessageError() raise InvalidMessageError()
return self._messages._payload(_message) return self._message.payload()
def topic_name(self) -> str: def topic_name(self) -> str:
if not self.is_valid(): if not self.is_valid():
raise InvalidMessageError() raise InvalidMessageError()
return self._messages._topic_name(_message) return self._message.topic_name()
def key() -> bytes: def key(self) -> bytes:
if not self.is_valid(): if not self.is_valid():
raise InvalidMessageError() raise InvalidMessageError()
return self._messages.key(_message) return self._message.key()
def timestamp() -> int: def timestamp(self) -> int:
if not self.is_valid(): if not self.is_valid():
raise InvalidMessageError() raise InvalidMessageError()
return self._messages.timestamp(_message) return self._message.timestamp()
class InvalidMessagesError(Exception): class InvalidMessagesError(Exception):
'''Signals using a messages instance outside of the registered transformation.''' '''Signals using a messages instance outside of the registered transformation.'''
pass pass
class Messages: class Messages:
'''Represents a list of messages from a stream.''' '''Represents a list of messages from a stream.'''
__slots__ = ('_messages',) __slots__ = ('_messages',)
def __init__(self, messages): def __init__(self, messages):
if not isinstance(messages, _mgp.Messages): if not isinstance(messages, _mgp.Messages):
raise TypeError("Expected '_mgp.Messages', got '{}'".format(type(messages))) raise TypeError(
"Expected '_mgp.Messages', got '{}'".format(type(messages)))
self._messages = messages self._messages = messages
def __deepcopy__(self, memo): def __deepcopy__(self, memo):
@ -869,18 +884,19 @@ class Messages:
'''Return True if `self` is in valid context and may be used.''' '''Return True if `self` is in valid context and may be used.'''
return self._messages.is_valid() return self._messages.is_valid()
def message_at(self, id : int) -> Message: def message_at(self, id: int) -> Message:
'''Raise InvalidMessagesError if context is invalid.''' '''Raise InvalidMessagesError if context is invalid.'''
if not self.is_valid(): if not self.is_valid():
raise InvalidMessagesError() raise InvalidMessagesError()
return Message(self._messages.message_at(id)) return Message(self._messages.message_at(id))
def total_messages() -> int: def total_messages(self) -> int:
'''Raise InvalidContextError if context is invalid.''' '''Raise InvalidContextError if context is invalid.'''
if not self.is_valid(): if not self.is_valid():
raise InvalidMessagesError() raise InvalidMessagesError()
return self._messages.total_messages() return self._messages.total_messages()
class TransCtx: class TransCtx:
'''Context of a transformation being executed. '''Context of a transformation being executed.
@ -891,7 +907,8 @@ class TransCtx:
def __init__(self, graph): def __init__(self, graph):
if not isinstance(graph, _mgp.Graph): if not isinstance(graph, _mgp.Graph):
raise TypeError("Expected '_mgp.Graph', got '{}'".format(type(graph))) raise TypeError(
"Expected '_mgp.Graph', got '{}'".format(type(graph)))
self._graph = Graph(graph) self._graph = Graph(graph)
def is_valid(self) -> bool: def is_valid(self) -> bool:
@ -904,13 +921,15 @@ class TransCtx:
raise InvalidContextError() raise InvalidContextError()
return self._graph return self._graph
def transformation(func: typing.Callable[..., Record]): def transformation(func: typing.Callable[..., Record]):
raise_if_does_not_meet_requirements(func) raise_if_does_not_meet_requirements(func)
sig = inspect.signature(func) sig = inspect.signature(func)
params = tuple(sig.parameters.values()) params = tuple(sig.parameters.values())
if not params or not params[0].annotation is Messages: if not params or not params[0].annotation is Messages:
if not len(params) == 2 or not params[1].annotation is Messages: if not len(params) == 2 or not params[1].annotation is Messages:
raise NotImplementedError("Valid signatures for transformations are (TransCtx, Messages) or (Messages)") raise NotImplementedError(
"Valid signatures for transformations are (TransCtx, Messages) or (Messages)")
if params[0].annotation is TransCtx: if params[0].annotation is TransCtx:
@functools.wraps(func) @functools.wraps(func)
def wrapper(graph, messages): def wrapper(graph, messages):

View File

@ -44,6 +44,7 @@ utils::BasicResult<std::string, std::vector<Message>> GetBatch(RdKafka::KafkaCon
break; break;
default: default:
// TODO(antaljanosbenjamin): handle RD_KAFKA_RESP_ERR__MAX_POLL_EXCEEDED
auto error = msg->errstr(); auto error = msg->errstr();
spdlog::warn("Unexpected error while consuming message in consumer {}, error: {}!", info.consumer_name, spdlog::warn("Unexpected error while consuming message in consumer {}, error: {}!", info.consumer_name,
msg->errstr()); msg->errstr());
@ -307,6 +308,7 @@ void Consumer::StartConsuming() {
spdlog::warn("Error happened in consumer {} while processing a batch: {}!", info_.consumer_name, e.what()); spdlog::warn("Error happened in consumer {} while processing a batch: {}!", info_.consumer_name, e.what());
break; break;
} }
spdlog::info("Kafka consumer {} finished processing", info_.consumer_name);
} }
is_running_.store(false); is_running_.store(false);
}); });

View File

@ -23,6 +23,7 @@
#include "communication/bolt/v1/constants.hpp" #include "communication/bolt/v1/constants.hpp"
#include "helpers.hpp" #include "helpers.hpp"
#include "py/py.hpp" #include "py/py.hpp"
#include "query/discard_value_stream.hpp"
#include "query/exceptions.hpp" #include "query/exceptions.hpp"
#include "query/interpreter.hpp" #include "query/interpreter.hpp"
#include "query/plan/operator.hpp" #include "query/plan/operator.hpp"
@ -433,7 +434,7 @@ class BoltSession final : public communication::bolt::Session<communication::Inp
} }
std::map<std::string, communication::bolt::Value> Discard(std::optional<int> n, std::optional<int> qid) override { std::map<std::string, communication::bolt::Value> Discard(std::optional<int> n, std::optional<int> qid) override {
DiscardValueResultStream stream; query::DiscardValueResultStream stream;
return PullResults(stream, n, qid); return PullResults(stream, n, qid);
} }
@ -517,12 +518,6 @@ class BoltSession final : public communication::bolt::Session<communication::Inp
const storage::Storage *db_; const storage::Storage *db_;
}; };
struct DiscardValueResultStream {
void Result(const std::vector<query::TypedValue> &) {
// do nothing
}
};
// NOTE: Needed only for ToBoltValue conversions // NOTE: Needed only for ToBoltValue conversions
const storage::Storage *db_; const storage::Storage *db_;
query::Interpreter interpreter_; query::Interpreter interpreter_;

View File

@ -29,8 +29,8 @@ class EnsureGIL final {
PyGILState_STATE gil_state_; PyGILState_STATE gil_state_;
public: public:
EnsureGIL() : gil_state_(PyGILState_Ensure()) {} EnsureGIL() noexcept : gil_state_(PyGILState_Ensure()) {}
~EnsureGIL() { PyGILState_Release(gil_state_); } ~EnsureGIL() noexcept { PyGILState_Release(gil_state_); }
EnsureGIL(const EnsureGIL &) = delete; EnsureGIL(const EnsureGIL &) = delete;
EnsureGIL(EnsureGIL &&) = delete; EnsureGIL(EnsureGIL &&) = delete;
EnsureGIL &operator=(const EnsureGIL &) = delete; EnsureGIL &operator=(const EnsureGIL &) = delete;

View File

@ -0,0 +1,13 @@
#pragma once
#include <vector>
#include "query/typed_value.hpp"
namespace query {
struct DiscardValueResultStream final {
void Result(const std::vector<query::TypedValue> & /*values*/) {
// do nothing
}
};
} // namespace query

View File

@ -2476,12 +2476,15 @@ cpp<#
:slk-save #'slk-save-ast-pointer :slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression")) :slk-load (slk-load-ast-pointer "Expression"))
(batch_size "Expression *" :initval "nullptr" :scope :public (batch_size "Expression *" :initval "nullptr" :scope :public
:slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression"))
(batch_limit "Expression *" :initval "nullptr" :scope :public
:slk-save #'slk-save-ast-pointer :slk-save #'slk-save-ast-pointer
:slk-load (slk-load-ast-pointer "Expression"))) :slk-load (slk-load-ast-pointer "Expression")))
(:public (:public
(lcp:define-enum action (lcp:define-enum action
(create-stream drop-stream start-stream stop-stream start-all-streams stop-all-streams show-streams test-stream) (create-stream drop-stream start-stream stop-stream start-all-streams stop-all-streams show-streams check-stream)
(:serialize)) (:serialize))
#>cpp #>cpp
StreamQuery() = default; StreamQuery() = default;

View File

@ -472,6 +472,7 @@ antlrcpp::Any CypherMainVisitor::visitCreateStream(MemgraphCypher::CreateStreamC
auto *topic_names_ctx = ctx->topicNames(); auto *topic_names_ctx = ctx->topicNames();
MG_ASSERT(topic_names_ctx != nullptr); MG_ASSERT(topic_names_ctx != nullptr);
// TODO(antaljanosbenjamin): Add dash
auto topic_names = topic_names_ctx->symbolicNameWithDots(); auto topic_names = topic_names_ctx->symbolicNameWithDots();
MG_ASSERT(!topic_names.empty()); MG_ASSERT(!topic_names.empty());
stream_query->topic_names_.reserve(topic_names.size()); stream_query->topic_names_.reserve(topic_names.size());
@ -540,6 +541,20 @@ antlrcpp::Any CypherMainVisitor::visitShowStreams(MemgraphCypher::ShowStreamsCon
return stream_query; return stream_query;
} }
antlrcpp::Any CypherMainVisitor::visitCheckStream(MemgraphCypher::CheckStreamContext *ctx) {
auto *stream_query = storage_->Create<StreamQuery>();
stream_query->action_ = StreamQuery::Action::CHECK_STREAM;
stream_query->stream_name_ = ctx->streamName()->symbolicName()->accept(this).as<std::string>();
if (ctx->BATCH_LIMIT()) {
if (!ctx->batchLimit->numberLiteral() || !ctx->batchLimit->numberLiteral()->integerLiteral()) {
throw SemanticException("Batch limit should be an integer literal!");
}
stream_query->batch_limit_ = ctx->batchLimit->accept(this);
}
return stream_query;
}
antlrcpp::Any CypherMainVisitor::visitCypherUnion(MemgraphCypher::CypherUnionContext *ctx) { antlrcpp::Any CypherMainVisitor::visitCypherUnion(MemgraphCypher::CypherUnionContext *ctx) {
bool distinct = !ctx->ALL(); bool distinct = !ctx->ALL();
auto *cypher_union = storage_->Create<CypherUnion>(distinct); auto *cypher_union = storage_->Create<CypherUnion>(distinct);

View File

@ -288,6 +288,11 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
*/ */
antlrcpp::Any visitShowStreams(MemgraphCypher::ShowStreamsContext *ctx) override; antlrcpp::Any visitShowStreams(MemgraphCypher::ShowStreamsContext *ctx) override;
/**
* @return StreamQuery*
*/
antlrcpp::Any visitCheckStream(MemgraphCypher::CheckStreamContext *ctx) override;
/** /**
* @return CypherUnion* * @return CypherUnion*
*/ */

View File

@ -12,10 +12,11 @@ memgraphCypherKeyword : cypherKeyword
| ASYNC | ASYNC
| AUTH | AUTH
| BAD | BAD
| BATCHES
| BATCH_INTERVAL | BATCH_INTERVAL
| BATCH_LIMIT
| BATCH_SIZE | BATCH_SIZE
| BEFORE | BEFORE
| CHECK
| CLEAR | CLEAR
| COMMIT | COMMIT
| COMMITTED | COMMITTED
@ -141,7 +142,8 @@ clause : cypherMatch
| loadCsv | loadCsv
; ;
streamQuery : createStream streamQuery : checkStream
| createStream
| dropStream | dropStream
| startStream | startStream
| startAllStreams | startAllStreams
@ -289,3 +291,5 @@ stopStream : STOP STREAM streamName ;
stopAllStreams : STOP ALL STREAMS ; stopAllStreams : STOP ALL STREAMS ;
showStreams : SHOW STREAMS ; showStreams : SHOW STREAMS ;
checkStream : CHECK STREAM streamName ( BATCH_LIMIT batchLimit=literal ) ? ;

View File

@ -17,10 +17,11 @@ ALTER : A L T E R ;
ASYNC : A S Y N C ; ASYNC : A S Y N C ;
AUTH : A U T H ; AUTH : A U T H ;
BAD : B A D ; BAD : B A D ;
BATCHES : B A T C H E S ;
BATCH_INTERVAL : B A T C H UNDERSCORE I N T E R V A L ; BATCH_INTERVAL : B A T C H UNDERSCORE I N T E R V A L ;
BATCH_LIMIT : B A T C H UNDERSCORE L I M I T ;
BATCH_SIZE : B A T C H UNDERSCORE S I Z E ; BATCH_SIZE : B A T C H UNDERSCORE S I Z E ;
BEFORE : B E F O R E ; BEFORE : B E F O R E ;
CHECK : C H E C K ;
CLEAR : C L E A R ; CLEAR : C L E A R ;
COMMIT : C O M M I T ; COMMIT : C O M M I T ;
COMMITTED : C O M M I T T E D ; COMMITTED : C O M M I T T E D ;

View File

@ -127,11 +127,11 @@ const trie::Trie kKeywords = {"union", "all",
"level", "next", "level", "next",
"read", "session", "read", "session",
"snapshot", "transaction", "snapshot", "transaction",
"batches", "batch_interval", "batch_limit", "batch_interval",
"batch_size", "consumer_group", "batch_size", "consumer_group",
"start", "stream", "start", "stream",
"streams", "transform", "streams", "transform",
"topics"}; "topics", "check"};
// Unicode codepoints that are allowed at the start of the unescaped name. // Unicode codepoints that are allowed at the start of the unescaped name.
const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts( const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts(

View File

@ -490,7 +490,7 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters &paramete
.consumer_group = std::move(consumer_group), .consumer_group = std::move(consumer_group),
.batch_interval = batch_interval, .batch_interval = batch_interval,
.batch_size = batch_size, .batch_size = batch_size,
.transformation_name = "transform.trans"}); .transformation_name = transformation_name});
return std::vector<std::vector<TypedValue>>{}; return std::vector<std::vector<TypedValue>>{};
}; };
return callback; return callback;
@ -575,8 +575,14 @@ Callback HandleStreamQuery(StreamQuery *stream_query, const Parameters &paramete
return results; return results;
}; };
return callback; return callback;
case StreamQuery::Action::TEST_STREAM: }
throw std::logic_error("not implemented"); case StreamQuery::Action::CHECK_STREAM: {
callback.header = {"query", "parameters"};
callback.fn = [interpreter_context, stream_name = stream_query->stream_name_,
batch_limit = GetOptionalValue<int64_t>(stream_query->batch_limit_, evaluator)]() mutable {
return interpreter_context->streams.Test(stream_name, batch_limit);
};
return callback;
} }
} }
} }

View File

@ -3761,7 +3761,7 @@ class CallProcedureCursor : public Cursor {
std::string_view field_name(self_->result_fields_[i]); std::string_view field_name(self_->result_fields_[i]);
auto result_it = values.find(field_name); auto result_it = values.find(field_name);
if (result_it == values.end()) { if (result_it == values.end()) {
throw QueryRuntimeException("Procedure '{}' does not yield a record with '{}' field.", self_->procedure_name_, throw QueryRuntimeException("Procedure '{}' did not yield a record with '{}' field.", self_->procedure_name_,
field_name); field_name);
} }
frame[self_->result_symbols_[i]] = result_it->second; frame[self_->result_symbols_[i]] = result_it->second;

View File

@ -1372,7 +1372,7 @@ bool MgpTransAddFixedResult(mgp_trans *trans) {
if (int err = AddResultToProp(trans, "query", mgp_type_string(), false); err != 1) { if (int err = AddResultToProp(trans, "query", mgp_type_string(), false); err != 1) {
return err; return err;
} }
return AddResultToProp(trans, "parameters", mgp_type_nullable(mgp_type_list(mgp_type_any())), false); return AddResultToProp(trans, "parameters", mgp_type_nullable(mgp_type_map()), false);
} }
int mgp_proc_add_deprecated_result(mgp_proc *proc, const char *name, const mgp_type *type) { int mgp_proc_add_deprecated_result(mgp_proc *proc, const char *name, const mgp_type *type) {

View File

@ -549,7 +549,7 @@ bool IsValidIdentifierName(const char *name);
} // namespace query::procedure } // namespace query::procedure
struct mgp_message { struct mgp_message {
integrations::kafka::Message *msg; const integrations::kafka::Message *msg;
}; };
struct mgp_messages { struct mgp_messages {

View File

@ -418,11 +418,11 @@ bool PythonModule::Load(const std::filesystem::path &file_path) {
bool PythonModule::Close() { bool PythonModule::Close() {
MG_ASSERT(py_module_, "Attempting to close a module that has not been loaded..."); MG_ASSERT(py_module_, "Attempting to close a module that has not been loaded...");
spdlog::info("Closing module {}...", file_path_); spdlog::info("Closing module {}...", file_path_);
// The procedures are closures which hold references to the Python callbacks. // The procedures and transformations are closures which hold references to the Python callbacks.
// Releasing these references might result in deallocations so we need to take // Releasing these references might result in deallocations so we need to take the GIL.
// the GIL.
auto gil = py::EnsureGIL(); auto gil = py::EnsureGIL();
procedures_.clear(); procedures_.clear();
transformations_.clear();
// Delete the module from the `sys.modules` directory so that the module will // Delete the module from the `sys.modules` directory so that the module will
// be properly imported if imported again. // be properly imported if imported again.
py::Object sys(PyImport_ImportModule("sys")); py::Object sys(PyImport_ImportModule("sys"));

View File

@ -782,7 +782,7 @@ void CallPythonTransformation(const py::Object &py_cb, const mgp_messages *msgs,
}; };
auto call = [&](py::Object py_graph, py::Object py_messages) -> std::optional<py::ExceptionInfo> { auto call = [&](py::Object py_graph, py::Object py_messages) -> std::optional<py::ExceptionInfo> {
auto py_res = py_cb.Call(py_messages, py_graph); auto py_res = py_cb.Call(py_graph, py_messages);
if (!py_res) return py::FetchError(); if (!py_res) return py::FetchError();
if (PySequence_Check(py_res.Ptr())) { if (PySequence_Check(py_res.Ptr())) {
return AddMultipleRecordsFromPython(result, py_res); return AddMultipleRecordsFromPython(result, py_res);

View File

@ -6,23 +6,92 @@
#include <spdlog/spdlog.h> #include <spdlog/spdlog.h>
#include <json/json.hpp> #include <json/json.hpp>
#include "query/db_accessor.hpp"
#include "query/discard_value_stream.hpp"
#include "query/interpreter.hpp" #include "query/interpreter.hpp"
#include "query/procedure/mg_procedure_impl.hpp"
#include "query/procedure/module.hpp"
#include "query/typed_value.hpp"
#include "utils/memory.hpp"
#include "utils/on_scope_exit.hpp" #include "utils/on_scope_exit.hpp"
#include "utils/pmr/string.hpp"
namespace query { namespace query {
using Consumer = integrations::kafka::Consumer;
using ConsumerInfo = integrations::kafka::ConsumerInfo;
using Message = integrations::kafka::Message;
namespace { namespace {
constexpr auto kExpectedTransformationResultSize = 2;
const utils::pmr::string query_param_name{"query", utils::NewDeleteResource()};
const utils::pmr::string params_param_name{"parameters", utils::NewDeleteResource()};
const std::map<std::string, storage::PropertyValue> empty_parameters{};
auto GetStream(auto &map, const std::string &stream_name) { auto GetStream(auto &map, const std::string &stream_name) {
if (auto it = map.find(stream_name); it != map.end()) { if (auto it = map.find(stream_name); it != map.end()) {
return it; return it;
} }
throw StreamsException("Couldn't find stream '{}'", stream_name); throw StreamsException("Couldn't find stream '{}'", stream_name);
} }
} // namespace
using Consumer = integrations::kafka::Consumer; void CallCustomTransformation(const std::string &transformation_name, const std::vector<Message> &messages,
using ConsumerInfo = integrations::kafka::ConsumerInfo; mgp_result &result, storage::Storage::Accessor &storage_accessor,
using Message = integrations::kafka::Message; utils::MemoryResource &memory_resource, const std::string &stream_name) {
DbAccessor db_accessor{&storage_accessor};
{
auto maybe_transformation =
procedure::FindTransformation(procedure::gModuleRegistry, transformation_name, utils::NewDeleteResource());
if (!maybe_transformation) {
throw StreamsException("Couldn't find transformation {} for stream '{}'", transformation_name, stream_name);
};
const auto &trans = *maybe_transformation->second;
mgp_messages mgp_messages{mgp_messages::storage_type{&memory_resource}};
std::transform(messages.begin(), messages.end(), std::back_inserter(mgp_messages.messages),
[](const integrations::kafka::Message &message) { return mgp_message{&message}; });
mgp_graph graph{&db_accessor, storage::View::OLD, nullptr};
mgp_memory memory{&memory_resource};
result.rows.clear();
result.error_msg.reset();
result.signature = &trans.results;
MG_ASSERT(result.signature->size() == kExpectedTransformationResultSize);
MG_ASSERT(result.signature->contains(query_param_name));
MG_ASSERT(result.signature->contains(params_param_name));
spdlog::trace("Calling transformation in stream '{}'", stream_name);
trans.cb(&mgp_messages, &graph, &result, &memory);
}
if (result.error_msg.has_value()) {
throw StreamsException(result.error_msg->c_str());
}
}
std::pair<TypedValue /*query*/, TypedValue /*parameters*/> ExtractTransformationResult(
utils::pmr::map<utils::pmr::string, TypedValue> &&values, const std::string_view transformation_name,
const std::string_view stream_name) {
if (values.size() != kExpectedTransformationResultSize) {
throw StreamsException(
"Transformation '{}' in stream '{}' did not yield all fields (query, parameters) as required.",
transformation_name, stream_name);
}
auto get_value = [&](const utils::pmr::string &field_name) mutable -> TypedValue & {
auto it = values.find(field_name);
if (it == values.end()) {
throw StreamsException{"Transformation '{}' in stream '{}' did not yield a record with '{}' field.",
transformation_name, stream_name, field_name};
};
return it->second;
};
auto &query_value = get_value(query_param_name);
MG_ASSERT(query_value.IsString());
auto &params_value = get_value(params_param_name);
MG_ASSERT(params_value.IsNull() || params_value.IsMap());
return {std::move(query_value), std::move(params_value)};
}
} // namespace
// nlohmann::json doesn't support string_view access yet // nlohmann::json doesn't support string_view access yet
const std::string kStreamName{"name"}; const std::string kStreamName{"name"};
@ -31,6 +100,7 @@ const std::string kConsumerGroupKey{"consumer_group"};
const std::string kBatchIntervalKey{"batch_interval"}; const std::string kBatchIntervalKey{"batch_interval"};
const std::string kBatchSizeKey{"batch_size"}; const std::string kBatchSizeKey{"batch_size"};
const std::string kIsRunningKey{"is_running"}; const std::string kIsRunningKey{"is_running"};
const std::string kTransformationName{"transformation_name"};
void to_json(nlohmann::json &data, StreamStatus &&status) { void to_json(nlohmann::json &data, StreamStatus &&status) {
auto &info = status.info; auto &info = status.info;
@ -51,6 +121,7 @@ void to_json(nlohmann::json &data, StreamStatus &&status) {
} }
data[kIsRunningKey] = status.is_running; data[kIsRunningKey] = status.is_running;
data[kTransformationName] = status.info.transformation_name;
} }
void from_json(const nlohmann::json &data, StreamStatus &status) { void from_json(const nlohmann::json &data, StreamStatus &status) {
@ -75,6 +146,7 @@ void from_json(const nlohmann::json &data, StreamStatus &status) {
} }
data.at(kIsRunningKey).get_to(status.is_running); data.at(kIsRunningKey).get_to(status.is_running);
data.at(kTransformationName).get_to(status.info.transformation_name);
} }
Streams::Streams(InterpreterContext *interpreter_context, std::string bootstrap_servers, Streams::Streams(InterpreterContext *interpreter_context, std::string bootstrap_servers,
@ -89,8 +161,8 @@ void Streams::RestoreStreams() {
MG_ASSERT(locked_streams_map->empty(), "Cannot restore streams when some streams already exist!"); MG_ASSERT(locked_streams_map->empty(), "Cannot restore streams when some streams already exist!");
for (const auto &[stream_name, stream_data] : storage_) { for (const auto &[stream_name, stream_data] : storage_) {
const auto get_failed_message = [](const std::string_view stream_name, const std::string_view message, const auto get_failed_message = [&stream_name = stream_name](const std::string_view message,
const std::string_view nested_message) { const std::string_view nested_message) {
return fmt::format("Failed to load stream '{}', because: {} caused by {}", stream_name, message, nested_message); return fmt::format("Failed to load stream '{}', because: {} caused by {}", stream_name, message, nested_message);
}; };
@ -98,21 +170,22 @@ void Streams::RestoreStreams() {
try { try {
nlohmann::json::parse(stream_data).get_to(status); nlohmann::json::parse(stream_data).get_to(status);
} catch (const nlohmann::json::type_error &exception) { } catch (const nlohmann::json::type_error &exception) {
spdlog::warn(get_failed_message(stream_name, "invalid type conversion", exception.what())); spdlog::warn(get_failed_message("invalid type conversion", exception.what()));
continue; continue;
} catch (const nlohmann::json::out_of_range &exception) { } catch (const nlohmann::json::out_of_range &exception) {
spdlog::warn(get_failed_message(stream_name, "non existing field", exception.what())); spdlog::warn(get_failed_message("non existing field", exception.what()));
continue; continue;
} }
MG_ASSERT(status.name == stream_name, "Expected stream name is '{}', but got '{}'", stream_name, status.name); MG_ASSERT(status.name == stream_name, "Expected stream name is '{}', but got '{}'", status.name, stream_name);
try { try {
auto it = CreateConsumer(*locked_streams_map, stream_name, std::move(status.info)); auto it = CreateConsumer(*locked_streams_map, stream_name, std::move(status.info));
if (status.is_running) { if (status.is_running) {
it->second.consumer->Lock()->Start(); it->second.consumer->Lock()->Start();
} }
spdlog::info("Stream '{}' is loaded", stream_name);
} catch (const utils::BasicException &exception) { } catch (const utils::BasicException &exception) {
spdlog::warn(get_failed_message(stream_name, "unexpected error", exception.what())); spdlog::warn(get_failed_message("unexpected error", exception.what()));
} }
} }
} }
@ -200,27 +273,38 @@ std::vector<StreamStatus> Streams::GetStreamInfo() const {
} }
TransformationResult Streams::Test(const std::string &stream_name, std::optional<int64_t> batch_limit) const { TransformationResult Streams::Test(const std::string &stream_name, std::optional<int64_t> batch_limit) const {
TransformationResult result; // This depends on the fact that Drop will first acquire a write lock to the consumer, and erase it only after that
auto consumer_function = [&result](const std::vector<Message> &messages) { auto [locked_consumer,
for (const auto &message : messages) { transformation_name] = [this, &stream_name]() -> std::pair<SynchronizedConsumer::ReadLockedPtr, std::string> {
// TODO(antaljanosbenjamin) Update the logic with using the transform from modules auto locked_streams = streams_.ReadLock();
const auto payload = message.Payload(); auto it = GetStream(*locked_streams, stream_name);
const std::string_view payload_as_string_view{payload.data(), payload.size()}; return {it->second.consumer->ReadLock(), it->second.transformation_name};
spdlog::info("CREATE (n:MESSAGE {{payload: '{}'}})", payload_as_string_view); }();
result[fmt::format("CREATE (n:MESSAGE {{payload: '{}'}})", payload_as_string_view)] = "replace with params";
auto *memory_resource = utils::NewDeleteResource();
mgp_result result{nullptr, memory_resource};
TransformationResult test_result;
auto consumer_function = [interpreter_context = interpreter_context_, memory_resource, &stream_name,
&transformation_name = transformation_name, &result,
&test_result](const std::vector<Message> &messages) mutable {
auto accessor = interpreter_context->db->Access();
CallCustomTransformation(transformation_name, messages, result, accessor, *memory_resource, stream_name);
for (auto &row : result.rows) {
auto [query, parameters] = ExtractTransformationResult(std::move(row.values), transformation_name, stream_name);
std::vector<TypedValue> result_row;
result_row.reserve(kExpectedTransformationResultSize);
result_row.push_back(std::move(query));
result_row.push_back(std::move(parameters));
test_result.push_back(std::move(result_row));
} }
}; };
// This depends on the fact that Drop will first acquire a write lock to the consumer, and erase it only after that
auto locked_consumer = [this, &stream_name] {
auto locked_streams = streams_.ReadLock();
auto it = GetStream(*locked_streams, stream_name);
return it->second.consumer->ReadLock();
}();
locked_consumer->Test(batch_limit, consumer_function); locked_consumer->Test(batch_limit, consumer_function);
return result; return test_result;
} }
StreamStatus Streams::CreateStatus(const std::string &name, const std::string &transformation_name, StreamStatus Streams::CreateStatus(const std::string &name, const std::string &transformation_name,
@ -243,24 +327,37 @@ Streams::StreamsMap::iterator Streams::CreateConsumer(StreamsMap &map, const std
throw StreamsException{"Stream already exists with name '{}'", stream_name}; throw StreamsException{"Stream already exists with name '{}'", stream_name};
} }
auto consumer_function = [interpreter_context = auto *memory_resource = utils::NewDeleteResource();
interpreter_context_](const std::vector<integrations::kafka::Message> &messages) {
Interpreter interpreter = Interpreter{interpreter_context};
TransformationResult result;
for (const auto &message : messages) { auto consumer_function = [interpreter_context = interpreter_context_, memory_resource, stream_name,
// TODO(antaljanosbenjamin) Update the logic with using the transform from modules transformation_name = stream_info.transformation_name,
const auto payload = message.Payload(); interpreter = std::make_shared<Interpreter>(interpreter_context_),
const std::string_view payload_as_string_view{payload.data(), payload.size()}; result = mgp_result{nullptr, memory_resource}](
result[fmt::format("CREATE (n:MESSAGE {{payload: '{}'}})", payload_as_string_view)] = "replace with params"; const std::vector<integrations::kafka::Message> &messages) mutable {
auto accessor = interpreter_context->db->Access();
CallCustomTransformation(transformation_name, messages, result, accessor, *memory_resource, stream_name);
DiscardValueResultStream stream;
spdlog::trace("Start transaction in stream '{}'", stream_name);
interpreter->BeginTransaction();
for (auto &row : result.rows) {
spdlog::trace("Processing row in stream '{}'", stream_name);
auto [query_value, params_value] =
ExtractTransformationResult(std::move(row.values), transformation_name, stream_name);
storage::PropertyValue params_prop{params_value};
std::string query{query_value.ValueString()};
spdlog::trace("Executing query '{}' in stream '{}'", query, stream_name);
auto prepare_result =
interpreter->Prepare(query, params_prop.IsNull() ? empty_parameters : params_prop.ValueMap());
interpreter->PullAll(&stream);
} }
for (const auto &[query, params] : result) { spdlog::trace("Commit transaction in stream '{}'", stream_name);
// auto prepared_query = interpreter.Prepare(query, {}); interpreter->CommitTransaction();
spdlog::info("Executing query '{}'", query); result.rows.clear();
// TODO(antaljanosbenjamin) run the query in real life, try not to copy paste the whole execution code, but
// extract it to a function that can be called from multiple places (e.g: triggers)
}
}; };
ConsumerInfo consumer_info{ ConsumerInfo consumer_info{

View File

@ -8,6 +8,7 @@
#include "integrations/kafka/consumer.hpp" #include "integrations/kafka/consumer.hpp"
#include "kvstore/kvstore.hpp" #include "kvstore/kvstore.hpp"
#include "query/typed_value.hpp"
#include "utils/exceptions.hpp" #include "utils/exceptions.hpp"
#include "utils/rw_lock.hpp" #include "utils/rw_lock.hpp"
#include "utils/synchronized.hpp" #include "utils/synchronized.hpp"
@ -19,8 +20,7 @@ class StreamsException : public utils::BasicException {
using BasicException::BasicException; using BasicException::BasicException;
}; };
// TODO(antaljanosbenjamin) Replace this with mgp_trans related thing using TransformationResult = std::vector<std::vector<TypedValue>>;
using TransformationResult = std::map<std::string, std::string>;
using TransformFunction = std::function<TransformationResult(const std::vector<integrations::kafka::Message> &)>; using TransformFunction = std::function<TransformationResult(const std::vector<integrations::kafka::Message> &)>;
struct StreamInfo { struct StreamInfo {
@ -28,7 +28,6 @@ struct StreamInfo {
std::string consumer_group; std::string consumer_group;
std::optional<std::chrono::milliseconds> batch_interval; std::optional<std::chrono::milliseconds> batch_interval;
std::optional<int64_t> batch_size; std::optional<int64_t> batch_size;
// TODO(antaljanosbenjamin) How to reference the transformation in a better way?
std::string transformation_name; std::string transformation_name;
}; };
@ -41,9 +40,7 @@ struct StreamStatus {
using SynchronizedConsumer = utils::Synchronized<integrations::kafka::Consumer, utils::WritePrioritizedRWLock>; using SynchronizedConsumer = utils::Synchronized<integrations::kafka::Consumer, utils::WritePrioritizedRWLock>;
struct StreamData { struct StreamData {
// TODO(antaljanosbenjamin) How to reference the transformation in a better way?
std::string transformation_name; std::string transformation_name;
// TODO(antaljanosbenjamin) consider propagate_const
std::unique_ptr<SynchronizedConsumer> consumer; std::unique_ptr<SynchronizedConsumer> consumer;
}; };
@ -120,8 +117,8 @@ class Streams final {
/// @param stream_name name of the stream we want to test /// @param stream_name name of the stream we want to test
/// @param batch_limit number of batches we want to test before stopping /// @param batch_limit number of batches we want to test before stopping
/// ///
/// TODO(antaljanosbenjamin) add type of parameters /// @returns A vector of vectors of TypedValue. Each subvector contains two elements, the query string and the
/// @returns A vector of pairs consisting of the query (std::string) and its parameters ... /// nullable parameters map.
/// ///
/// @throws StreamsException if the stream doesn't exist /// @throws StreamsException if the stream doesn't exist
/// @throws ConsumerRunningException if the consumer is alredy running /// @throws ConsumerRunningException if the consumer is alredy running

View File

@ -244,12 +244,11 @@ void TriggerStore::RestoreTriggers(utils::SkipList<QueryCacheEntry> *query_cache
spdlog::info("Loading triggers..."); spdlog::info("Loading triggers...");
for (const auto &[trigger_name, trigger_data] : storage_) { for (const auto &[trigger_name, trigger_data] : storage_) {
// structured binding cannot be captured in lambda const auto get_failed_message = [&trigger_name = trigger_name](const std::string_view message) {
const auto get_failed_message = [](const std::string_view trigger_name, const std::string_view message) {
return fmt::format("Failed to load trigger '{}'. {}", trigger_name, message); return fmt::format("Failed to load trigger '{}'. {}", trigger_name, message);
}; };
const auto invalid_state_message = get_failed_message(trigger_name, "Invalid state of the trigger data."); const auto invalid_state_message = get_failed_message("Invalid state of the trigger data.");
spdlog::debug("Loading trigger '{}'", trigger_name); spdlog::debug("Loading trigger '{}'", trigger_name);
auto json_trigger_data = nlohmann::json::parse(trigger_data); auto json_trigger_data = nlohmann::json::parse(trigger_data);
@ -259,7 +258,7 @@ void TriggerStore::RestoreTriggers(utils::SkipList<QueryCacheEntry> *query_cache
continue; continue;
} }
if (json_trigger_data["version"] != kVersion) { if (json_trigger_data["version"] != kVersion) {
spdlog::warn(get_failed_message(trigger_name, "Invalid version of the trigger data.")); spdlog::warn(get_failed_message("Invalid version of the trigger data."));
continue; continue;
} }

View File

@ -3207,8 +3207,16 @@ TEST_P(CypherMainVisitorTest, CreateSnapshotQuery) {
ASSERT_TRUE(dynamic_cast<CreateSnapshotQuery *>(ast_generator.ParseQuery("CREATE SNAPSHOT"))); ASSERT_TRUE(dynamic_cast<CreateSnapshotQuery *>(ast_generator.ParseQuery("CREATE SNAPSHOT")));
} }
void CheckOptionalExpression(Base &ast_generator, Expression *expression, const std::optional<TypedValue> &expected) {
EXPECT_EQ(expression != nullptr, expected.has_value());
if (expected.has_value()) {
EXPECT_NO_FATAL_FAILURE(ast_generator.CheckLiteral(expression, *expected));
}
};
void ValidateMostlyEmptyStreamQuery(Base &ast_generator, const std::string &query_string, void ValidateMostlyEmptyStreamQuery(Base &ast_generator, const std::string &query_string,
const StreamQuery::Action action, const std::string_view stream_name) { const StreamQuery::Action action, const std::string_view stream_name,
const std::optional<TypedValue> &batch_limit = std::nullopt) {
auto *parsed_query = dynamic_cast<StreamQuery *>(ast_generator.ParseQuery(query_string)); auto *parsed_query = dynamic_cast<StreamQuery *>(ast_generator.ParseQuery(query_string));
ASSERT_NE(parsed_query, nullptr); ASSERT_NE(parsed_query, nullptr);
EXPECT_EQ(parsed_query->action_, action); EXPECT_EQ(parsed_query->action_, action);
@ -3218,6 +3226,7 @@ void ValidateMostlyEmptyStreamQuery(Base &ast_generator, const std::string &quer
EXPECT_TRUE(parsed_query->consumer_group_.empty()); EXPECT_TRUE(parsed_query->consumer_group_.empty());
EXPECT_EQ(parsed_query->batch_interval_, nullptr); EXPECT_EQ(parsed_query->batch_interval_, nullptr);
EXPECT_EQ(parsed_query->batch_size_, nullptr); EXPECT_EQ(parsed_query->batch_size_, nullptr);
EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->batch_limit_, batch_limit));
} }
TEST_P(CypherMainVisitorTest, DropStream) { TEST_P(CypherMainVisitorTest, DropStream) {
@ -3292,18 +3301,13 @@ void ValidateCreateStreamQuery(Base &ast_generator, const std::string &query_str
ASSERT_NO_THROW(parsed_query = dynamic_cast<StreamQuery *>(ast_generator.ParseQuery(query_string))) << query_string; ASSERT_NO_THROW(parsed_query = dynamic_cast<StreamQuery *>(ast_generator.ParseQuery(query_string))) << query_string;
ASSERT_NE(parsed_query, nullptr); ASSERT_NE(parsed_query, nullptr);
EXPECT_EQ(parsed_query->stream_name_, stream_name); EXPECT_EQ(parsed_query->stream_name_, stream_name);
auto check_expression = [&](Expression *expression, const std::optional<TypedValue> &expected) {
EXPECT_EQ(expression != nullptr, expected.has_value());
if (expected.has_value()) {
EXPECT_NO_FATAL_FAILURE(ast_generator.CheckLiteral(expression, *expected));
}
};
EXPECT_EQ(parsed_query->topic_names_, topic_names); EXPECT_EQ(parsed_query->topic_names_, topic_names);
EXPECT_EQ(parsed_query->transform_name_, transform_name); EXPECT_EQ(parsed_query->transform_name_, transform_name);
EXPECT_EQ(parsed_query->consumer_group_, consumer_group); EXPECT_EQ(parsed_query->consumer_group_, consumer_group);
EXPECT_NO_FATAL_FAILURE(check_expression(parsed_query->batch_interval_, batch_interval)); EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->batch_interval_, batch_interval));
EXPECT_NO_FATAL_FAILURE(check_expression(parsed_query->batch_size_, batch_size)); EXPECT_NO_FATAL_FAILURE(CheckOptionalExpression(ast_generator, parsed_query->batch_size_, batch_size));
EXPECT_EQ(parsed_query->batch_limit_, nullptr);
} }
TEST_P(CypherMainVisitorTest, CreateStream) { TEST_P(CypherMainVisitorTest, CreateStream) {
@ -3380,4 +3384,22 @@ TEST_P(CypherMainVisitorTest, CreateStream) {
EXPECT_NO_FATAL_FAILURE(check_topic_names({topic_name1, topic_name2})); EXPECT_NO_FATAL_FAILURE(check_topic_names({topic_name1, topic_name2}));
} }
TEST_P(CypherMainVisitorTest, CheckStream) {
auto &ast_generator = *GetParam();
TestInvalidQuery("CHECK STREAM", ast_generator);
TestInvalidQuery("CHECK STREAMS", ast_generator);
TestInvalidQuery("CHECK STREAMS something", ast_generator);
TestInvalidQuery("CHECK STREAM something,something", ast_generator);
TestInvalidQuery("CHECK STREAM something BATCH LIMIT 1", ast_generator);
TestInvalidQuery("CHECK STREAM something BATCH_LIMIT", ast_generator);
TestInvalidQuery<SemanticException>("CHECK STREAM something BATCH_LIMIT 'it should be an integer'", ast_generator);
TestInvalidQuery<SemanticException>("CHECK STREAM something BATCH_LIMIT 2.5", ast_generator);
ValidateMostlyEmptyStreamQuery(ast_generator, "CHECK STREAM checkedStream", StreamQuery::Action::CHECK_STREAM,
"checkedStream");
ValidateMostlyEmptyStreamQuery(ast_generator, "CHECK STREAM checkedStream bAtCH_LIMIT 42",
StreamQuery::Action::CHECK_STREAM, "checkedStream", TypedValue(42));
}
} // namespace } // namespace

View File

@ -32,8 +32,7 @@ StreamInfo CreateDefaultStreamInfo() {
.consumer_group = "ConsumerGroup " + GetDefaultStreamName(), .consumer_group = "ConsumerGroup " + GetDefaultStreamName(),
.batch_interval = std::nullopt, .batch_interval = std::nullopt,
.batch_size = std::nullopt, .batch_size = std::nullopt,
// TODO(antaljanosbenjamin) Add proper reference once Streams supports that .transformation_name = "not used in the tests",
.transformation_name = "not yet used",
}; };
} }
@ -79,8 +78,7 @@ class StreamsTest : public ::testing::Test {
EXPECT_EQ(check_data.info.consumer_group, status.info.consumer_group); EXPECT_EQ(check_data.info.consumer_group, status.info.consumer_group);
EXPECT_EQ(check_data.info.batch_interval, status.info.batch_interval); EXPECT_EQ(check_data.info.batch_interval, status.info.batch_interval);
EXPECT_EQ(check_data.info.batch_size, status.info.batch_size); EXPECT_EQ(check_data.info.batch_size, status.info.batch_size);
// TODO(antaljanosbenjamin) Add proper reference once Streams supports that EXPECT_EQ(check_data.info.transformation_name, status.info.transformation_name);
// EXPECT_EQ(check_data.info.transformation_name, status.info.transformation_name);
EXPECT_EQ(check_data.is_running, status.is_running); EXPECT_EQ(check_data.is_running, status.is_running);
} }
@ -227,5 +225,3 @@ TEST_F(StreamsTest, RestoreStreams) {
check_restore_logic(); check_restore_logic();
} }
} }
// TODO(antaljanosbenjamin) Add tests for Streams::Test method and transformation