Compare commits
10 Commits
T610-FL-Ad
...
T0958-MG-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
309d03287d | ||
|
|
3dd2657320 | ||
|
|
6fe474282a | ||
|
|
7fc0fb6520 | ||
|
|
063e297e1e | ||
|
|
86b1688192 | ||
|
|
f629de7e60 | ||
|
|
b737e53456 | ||
|
|
10ca68bb2a | ||
|
|
bfbd8538d4 |
4
.github/workflows/package_all.yaml
vendored
4
.github/workflows/package_all.yaml
vendored
@@ -174,5 +174,5 @@ jobs:
|
||||
- name: "Upload package"
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: debian-11
|
||||
path: build/output/debian-11/memgraph*.deb
|
||||
name: debian-11-arm
|
||||
path: build/output/debian-11-arm/memgraph*.deb
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Release CentOS 8
|
||||
name: Release CentOS 9
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
community_build:
|
||||
name: "Community build"
|
||||
runs-on: [self-hosted, Linux, X64, CentOS8]
|
||||
runs-on: [self-hosted, Linux, X64, CentOS9]
|
||||
env:
|
||||
THREADS: 24
|
||||
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
coverage_build:
|
||||
name: "Coverage build"
|
||||
runs-on: [self-hosted, Linux, X64, CentOS8]
|
||||
runs-on: [self-hosted, Linux, X64, CentOS9]
|
||||
env:
|
||||
THREADS: 24
|
||||
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
|
||||
debug_build:
|
||||
name: "Debug build"
|
||||
runs-on: [self-hosted, Linux, X64, CentOS8]
|
||||
runs-on: [self-hosted, Linux, X64, CentOS9]
|
||||
env:
|
||||
THREADS: 24
|
||||
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}
|
||||
@@ -180,7 +180,7 @@ jobs:
|
||||
|
||||
release_build:
|
||||
name: "Release build"
|
||||
runs-on: [self-hosted, Linux, X64, CentOS8]
|
||||
runs-on: [self-hosted, Linux, X64, CentOS9]
|
||||
env:
|
||||
THREADS: 24
|
||||
MEMGRAPH_ENTERPRISE_LICENSE: ${{ secrets.MEMGRAPH_ENTERPRISE_LICENSE }}
|
||||
22
.github/workflows/release_docker.yaml
vendored
22
.github/workflows/release_docker.yaml
vendored
@@ -4,8 +4,12 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: "Memgraph binary version to publish on Dockerhub."
|
||||
description: "Memgraph binary version to publish on DockerHub."
|
||||
required: true
|
||||
force_release:
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
|
||||
jobs:
|
||||
docker_publish:
|
||||
@@ -36,6 +40,22 @@ jobs:
|
||||
curl -L https://download.memgraph.com/memgraph/v${{ github.event.inputs.version }}/debian-11/memgraph_${{ github.event.inputs.version }}-1_amd64.deb > memgraph-amd64.deb
|
||||
curl -L https://download.memgraph.com/memgraph/v${{ github.event.inputs.version }}/debian-11-aarch64/memgraph_${{ github.event.inputs.version }}-1_arm64.deb > memgraph-arm64.deb
|
||||
|
||||
- name: Check if specified version is already pushed
|
||||
run: |
|
||||
EXISTS=$(docker manifest inspect $DOCKER_ORGANIZATION_NAME/$DOCKER_REPOSITORY_NAME:${{ github.event.inputs.version }} > /dev/null; echo $?)
|
||||
echo $EXISTS
|
||||
if [[ ${EXISTS} -eq 0 ]]; then
|
||||
echo 'The specified version has been already released to DockerHub.'
|
||||
if [[ ${{ github.event.inputs.force_release }} = true ]]; then
|
||||
echo 'Forcing the release!'
|
||||
else
|
||||
echo 'Stopping the release!'
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo 'All good the specified version has not been release to DockerHub.'
|
||||
fi
|
||||
|
||||
- name: Build & push docker images
|
||||
run: |
|
||||
cd release/docker
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -9,7 +9,6 @@
|
||||
*.swn
|
||||
*.swo
|
||||
*.swp
|
||||
|
||||
*~
|
||||
.DS_Store
|
||||
.gdb_history
|
||||
@@ -27,7 +26,6 @@ src/query/frontend/opencypher/generated/
|
||||
tags
|
||||
ve/
|
||||
ve3/
|
||||
.cache/
|
||||
perf.data*
|
||||
TAGS
|
||||
*.apollo_measurements
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
* @antaljanosbenjamin @kostasrim
|
||||
@@ -18,16 +18,14 @@ WIDTH = 80
|
||||
|
||||
def wrap_text(s, initial_indent="# "):
|
||||
return "\n#\n".join(
|
||||
map(
|
||||
lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent, subsequent_indent="# "),
|
||||
s.split("\n"),
|
||||
)
|
||||
)
|
||||
map(lambda x: textwrap.fill(x, WIDTH, initial_indent=initial_indent,
|
||||
subsequent_indent="# "), s.split("\n")))
|
||||
|
||||
|
||||
def extract_flags(binary_path):
|
||||
ret = {}
|
||||
data = subprocess.run([binary_path, "--help-xml"], stdout=subprocess.PIPE).stdout.decode("utf-8")
|
||||
data = subprocess.run([binary_path, "--help-xml"],
|
||||
stdout=subprocess.PIPE).stdout.decode("utf-8")
|
||||
root = ET.fromstring(data)
|
||||
for child in root:
|
||||
if child.tag == "usage" and child.text.lower().count("warning"):
|
||||
@@ -48,7 +46,8 @@ def apply_config_to_flags(config, flags):
|
||||
for modification in config["modifications"]:
|
||||
name = modification["name"]
|
||||
if name not in flags:
|
||||
print("WARNING: Flag '" + name + "' missing from binary!", file=sys.stderr)
|
||||
print("WARNING: Flag '" + name + "' missing from binary!",
|
||||
file=sys.stderr)
|
||||
continue
|
||||
flags[name]["default"] = modification["value"]
|
||||
flags[name]["override"] = modification["override"]
|
||||
@@ -76,9 +75,8 @@ def extract_sections(flags):
|
||||
else:
|
||||
sections.append((current_section, current_flags))
|
||||
sections.append(("other", other))
|
||||
assert set(sum(map(lambda x: x[1], sections), [])) == set(
|
||||
flags.keys()
|
||||
), "The section extraction algorithm lost some flags!"
|
||||
assert set(sum(map(lambda x: x[1], sections), [])) == set(flags.keys()), \
|
||||
"The section extraction algorithm lost some flags!"
|
||||
return sections
|
||||
|
||||
|
||||
@@ -91,7 +89,8 @@ def generate_config_file(sections, flags):
|
||||
helpstr = flag["meaning"] + " [" + flag["type"] + "]"
|
||||
ret += wrap_text(helpstr) + "\n"
|
||||
prefix = "# " if not flag["override"] else ""
|
||||
ret += prefix + "--" + flag["name"].replace("_", "-") + "=" + flag["default"] + "\n\n"
|
||||
ret += prefix + "--" + flag["name"].replace("_", "-") + \
|
||||
"=" + flag["default"] + "\n\n"
|
||||
ret += "\n"
|
||||
ret += wrap_text(config["footer"])
|
||||
return ret.strip() + "\n"
|
||||
@@ -99,16 +98,13 @@ def generate_config_file(sections, flags):
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("memgraph_binary", help="path to Memgraph binary")
|
||||
parser.add_argument(
|
||||
"output_file",
|
||||
help="path where to store the generated Memgraph " "configuration file",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--config-file",
|
||||
default=CONFIG_FILE,
|
||||
help="path to generator configuration file",
|
||||
)
|
||||
parser.add_argument("memgraph_binary",
|
||||
help="path to Memgraph binary")
|
||||
parser.add_argument("output_file",
|
||||
help="path where to store the generated Memgraph "
|
||||
"configuration file")
|
||||
parser.add_argument("--config-file", default=CONFIG_FILE,
|
||||
help="path to generator configuration file")
|
||||
|
||||
args = parser.parse_args()
|
||||
flags = extract_flags(args.memgraph_binary)
|
||||
|
||||
686
include/mgp.py
686
include/mgp.py
File diff suppressed because it is too large
Load Diff
3
init
3
init
@@ -139,3 +139,6 @@ done;
|
||||
# Install precommit hook
|
||||
python3 -m pip install pre-commit
|
||||
python3 -m pre_commit install
|
||||
|
||||
# Link `include/mgp.py` with `release/mgp/mgp.py`
|
||||
ln -v -f include/mgp.py release/mgp/mgp.py
|
||||
|
||||
@@ -7,11 +7,13 @@ import copy
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def procedure(
|
||||
context: mgp.ProcCtx,
|
||||
required_arg: mgp.Nullable[mgp.Any],
|
||||
optional_arg: mgp.Nullable[mgp.Any] = None,
|
||||
) -> mgp.Record(args=list, vertex_count=int, avg_degree=mgp.Number, props=mgp.Nullable[mgp.Map]):
|
||||
def procedure(context: mgp.ProcCtx,
|
||||
required_arg: mgp.Nullable[mgp.Any],
|
||||
optional_arg: mgp.Nullable[mgp.Any] = None
|
||||
) -> mgp.Record(args=list,
|
||||
vertex_count=int,
|
||||
avg_degree=mgp.Number,
|
||||
props=mgp.Nullable[mgp.Map]):
|
||||
"""
|
||||
This example procedure returns 4 fields.
|
||||
|
||||
@@ -35,7 +37,7 @@ def procedure(
|
||||
if isinstance(required_arg, (mgp.Edge, mgp.Vertex)):
|
||||
props = dict(required_arg.properties.items())
|
||||
elif isinstance(required_arg, mgp.Path):
|
||||
(start_vertex,) = required_arg.vertices
|
||||
start_vertex, = required_arg.vertices
|
||||
props = dict(start_vertex.properties.items())
|
||||
# Count the vertices and edges in the database; this may take a while.
|
||||
vertex_count = 0
|
||||
@@ -49,13 +51,15 @@ def procedure(
|
||||
# Copy the received arguments to make it equivalent to the C example.
|
||||
args_copy = [copy.deepcopy(required_arg), copy.deepcopy(optional_arg)]
|
||||
# Multiple rows can be produced by returning an iterable of mgp.Record.
|
||||
return mgp.Record(args=args_copy, vertex_count=vertex_count, avg_degree=avg_degree, props=props)
|
||||
return mgp.Record(args=args_copy, vertex_count=vertex_count,
|
||||
avg_degree=avg_degree, props=props)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def write_procedure(
|
||||
context: mgp.ProcCtx, property_name: str, property_value: mgp.Nullable[mgp.Any]
|
||||
) -> mgp.Record(created_vertex=mgp.Vertex):
|
||||
def write_procedure(context: mgp.ProcCtx,
|
||||
property_name: str,
|
||||
property_value: mgp.Nullable[mgp.Any]
|
||||
) -> mgp.Record(created_vertex=mgp.Vertex):
|
||||
"""
|
||||
This example procedure creates a new vertex with the specified property
|
||||
and connects it to all existing vertex which has the same property with
|
||||
|
||||
@@ -4,17 +4,15 @@ from collections import OrderedDict
|
||||
from itertools import chain, repeat
|
||||
from inspect import cleandoc
|
||||
from typing import List, Tuple
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write(
|
||||
(
|
||||
"\n"
|
||||
"NOTE: Please install networkx to be able to use graph_analyzer "
|
||||
"module. Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
)
|
||||
sys.stderr.write((
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use graph_analyzer '
|
||||
'module. Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n'))
|
||||
raise import_error
|
||||
# Imported last because it also depends on networkx.
|
||||
from mgp_networkx import MemgraphMultiDiGraph # noqa E402
|
||||
@@ -25,14 +23,16 @@ _MAX_LIST_SIZE = 10
|
||||
|
||||
@mgp.read_proc
|
||||
def help() -> mgp.Record(name=str, value=str):
|
||||
"""Shows manual page for graph_analyzer."""
|
||||
'''Shows manual page for graph_analyzer.'''
|
||||
records = []
|
||||
|
||||
def make_records(name, doc):
|
||||
return (mgp.Record(name=n, value=v) for n, v in zip(chain([name], repeat("")), cleandoc(doc).splitlines()))
|
||||
return (mgp.Record(name=n, value=v) for n, v in
|
||||
zip(chain([name], repeat('')), cleandoc(doc).splitlines()))
|
||||
|
||||
for func in (help, analyze, analyze_subgraph):
|
||||
records.extend(make_records("Procedure '{}'".format(func.__name__), func.__doc__))
|
||||
records.extend(make_records("Procedure '{}'".format(func.__name__),
|
||||
func.__doc__))
|
||||
|
||||
for m, v in _get_analysis_mapping().items():
|
||||
records.extend(make_records("Analysis '{}'".format(m), v.__doc__))
|
||||
@@ -41,8 +41,10 @@ def help() -> mgp.Record(name=str, value=str):
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def analyze(context: mgp.ProcCtx, analyses: mgp.Nullable[List[str]] = None) -> mgp.Record(name=str, value=str):
|
||||
"""
|
||||
def analyze(context: mgp.ProcCtx,
|
||||
analyses: mgp.Nullable[List[str]] = None
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
'''
|
||||
Shows graph information.
|
||||
|
||||
In case of multiple results, only the first 10 will be shown.
|
||||
@@ -55,20 +57,19 @@ def analyze(context: mgp.ProcCtx, analyses: mgp.Nullable[List[str]] = None) -> m
|
||||
|
||||
Example call (with parameter):
|
||||
CALL graph_analyzer.analyze(['nodes', 'edges']) YIELD *;
|
||||
"""
|
||||
'''
|
||||
g = MemgraphMultiDiGraph(ctx=context)
|
||||
recs = _analyze_graph(context, g, analyses)
|
||||
return [mgp.Record(name=name, value=value) for name, value in recs]
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def analyze_subgraph(
|
||||
context: mgp.ProcCtx,
|
||||
vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge],
|
||||
analyses: mgp.Nullable[List[str]] = None,
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
"""
|
||||
def analyze_subgraph(context: mgp.ProcCtx,
|
||||
vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge],
|
||||
analyses: mgp.Nullable[List[str]] = None
|
||||
) -> mgp.Record(name=str, value=str):
|
||||
'''
|
||||
Shows subgraph information.
|
||||
|
||||
In case of multiple results, only the first 10 will be shown.
|
||||
@@ -90,40 +91,36 @@ def analyze_subgraph(
|
||||
CALL graph_analyzer.analyze_subgraph(nodes, edges, ['nodes', 'edges'])
|
||||
YIELD *
|
||||
RETURN name, value;
|
||||
"""
|
||||
'''
|
||||
vertices, edges = map(set, [vertices, edges])
|
||||
g = nx.subgraph_view(
|
||||
MemgraphMultiDiGraph(ctx=context),
|
||||
lambda n: n in vertices,
|
||||
lambda n1, n2, e: e in edges,
|
||||
)
|
||||
lambda n1, n2, e: e in edges)
|
||||
recs = _analyze_graph(context, g, analyses)
|
||||
return [mgp.Record(name=name, value=value) for name, value in recs]
|
||||
|
||||
|
||||
def _get_analysis_mapping():
|
||||
return OrderedDict(
|
||||
[
|
||||
("nodes", _number_of_nodes),
|
||||
("edges", _number_of_edges),
|
||||
("bridges", _bridges),
|
||||
("articulation_points", _articulation_points),
|
||||
("avg_degree", _avg_degree),
|
||||
("sorted_nodes_degree", _sorted_nodes_degree),
|
||||
("self_loops", _self_loops),
|
||||
("is_bipartite", _is_bipartite),
|
||||
("is_planar", _is_planar),
|
||||
("is_biconnected: ", _is_biconnected),
|
||||
("is_weakly_connected", _is_weakly_connected),
|
||||
("number_of_weakly_components", _weakly_components),
|
||||
("is_strongly_connected", _is_strongly_connected),
|
||||
("strongly_components", _strongly_components),
|
||||
("is_dag", _is_dag),
|
||||
("is_eulerian", _is_eulerian),
|
||||
("is_forest", _is_forest),
|
||||
("is_tree", _is_tree),
|
||||
]
|
||||
)
|
||||
return OrderedDict([
|
||||
('nodes', _number_of_nodes),
|
||||
('edges', _number_of_edges),
|
||||
('bridges', _bridges),
|
||||
('articulation_points', _articulation_points),
|
||||
('avg_degree', _avg_degree),
|
||||
('sorted_nodes_degree', _sorted_nodes_degree),
|
||||
('self_loops', _self_loops),
|
||||
('is_bipartite', _is_bipartite),
|
||||
('is_planar', _is_planar),
|
||||
('is_biconnected: ', _is_biconnected),
|
||||
('is_weakly_connected', _is_weakly_connected),
|
||||
('number_of_weakly_components', _weakly_components),
|
||||
('is_strongly_connected', _is_strongly_connected),
|
||||
('strongly_components', _strongly_components),
|
||||
('is_dag', _is_dag),
|
||||
('is_eulerian', _is_eulerian),
|
||||
('is_forest', _is_forest),
|
||||
('is_tree', _is_tree)])
|
||||
|
||||
|
||||
def _get_analysis_func(name: str):
|
||||
@@ -135,15 +132,20 @@ def _get_analysis_funcs():
|
||||
return _get_analysis_mapping().values()
|
||||
|
||||
|
||||
def _analyze_graph(context: mgp.ProcCtx, g: nx.MultiDiGraph, analyses: List[str]) -> List[Tuple[str, str]]:
|
||||
def _analyze_graph(context: mgp.ProcCtx,
|
||||
g: nx.MultiDiGraph,
|
||||
analyses: List[str]
|
||||
) -> List[Tuple[str, str]]:
|
||||
|
||||
functions = _get_analysis_funcs() if analyses is None else [_get_analysis_func(name) for name in analyses]
|
||||
functions = (_get_analysis_funcs() if analyses is None
|
||||
else [_get_analysis_func(name) for name in analyses])
|
||||
|
||||
records = []
|
||||
for index, f in enumerate(functions):
|
||||
context.check_must_abort()
|
||||
if f is None:
|
||||
raise KeyError("Graph analysis is not supported: " + analyses[index])
|
||||
raise KeyError('Graph analysis is not supported: ' +
|
||||
analyses[index])
|
||||
name, value = f(g)
|
||||
if isinstance(value, (list, set, tuple)):
|
||||
value = list(value)[:_MAX_LIST_SIZE]
|
||||
@@ -153,120 +155,126 @@ def _analyze_graph(context: mgp.ProcCtx, g: nx.MultiDiGraph, analyses: List[str]
|
||||
|
||||
|
||||
def _number_of_nodes(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
"""Returns number of nodes."""
|
||||
return "Number of nodes", nx.number_of_nodes(g)
|
||||
'''Returns number of nodes.'''
|
||||
return 'Number of nodes', nx.number_of_nodes(g)
|
||||
|
||||
|
||||
def _number_of_edges(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
"""Returns number of edges."""
|
||||
return "Number of edges", nx.number_of_edges(g)
|
||||
'''Returns number of edges.'''
|
||||
return 'Number of edges', nx.number_of_edges(g)
|
||||
|
||||
|
||||
def _avg_degree(g: nx.MultiDiGraph) -> Tuple[str, float]:
|
||||
"""Returns average degree."""
|
||||
'''Returns average degree.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
_, number_of_edges = _number_of_edges(g)
|
||||
avg_degree = 0 if number_of_nodes == 0 else number_of_edges / number_of_nodes
|
||||
return "Average degree", avg_degree
|
||||
avg_degree = (0 if number_of_nodes == 0
|
||||
else number_of_edges / number_of_nodes)
|
||||
return 'Average degree', avg_degree
|
||||
|
||||
|
||||
def _sorted_nodes_degree(g: nx.MultiDiGraph) -> Tuple[str, List[int]]:
|
||||
"""Returns list of sorted nodes degree. [(node_id, degree), ...]"""
|
||||
'''Returns list of sorted nodes degree. [(node_id, degree), ...]'''
|
||||
nodes_degree = [(n, g.degree(n)) for n in g.nodes()]
|
||||
nodes_degree.sort(key=lambda x: x[1], reverse=True)
|
||||
return "Sorted nodes degree", nodes_degree
|
||||
return 'Sorted nodes degree', nodes_degree
|
||||
|
||||
|
||||
def _self_loops(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
"""Returns number of self loops."""
|
||||
return "Self loops", sum((1 if e[0] == e[1] else 0 for e in g.edges()))
|
||||
'''Returns number of self loops.'''
|
||||
return 'Self loops', sum((1 if e[0] == e[1] else 0 for e in g.edges()))
|
||||
|
||||
|
||||
def _is_bipartite(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Checks if graph is bipartite."""
|
||||
'''Checks if graph is bipartite.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.bipartite.basic.is_bipartite(g)
|
||||
return "Is bipartite", ret
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.bipartite.basic.is_bipartite(g))
|
||||
return 'Is bipartite', ret
|
||||
|
||||
|
||||
def _is_planar(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Checks if graph is planar."""
|
||||
'''Checks if graph is planar.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.planarity.check_planarity(g)[0]
|
||||
return "Is planar", ret
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.planarity.check_planarity(g)[0])
|
||||
return 'Is planar', ret
|
||||
|
||||
|
||||
def _is_biconnected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Check if graph is biconnected."""
|
||||
'''Check if graph is biconnected.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g))
|
||||
return "Is biconnected", ret
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.is_biconnected(nx.MultiDiGraph.to_undirected(g)))
|
||||
return 'Is biconnected', ret
|
||||
|
||||
|
||||
def _is_weakly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Check if graph is weakly connected."""
|
||||
'''Check if graph is weakly connected.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.is_weakly_connected(g)
|
||||
return "Is weakly connected", ret
|
||||
return 'Is weakly connected', ret
|
||||
|
||||
|
||||
def _is_strongly_connected(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Checks if graph is strongly connected."""
|
||||
'''Checks if graph is strongly connected.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.is_strongly_connected(g)
|
||||
return "Is strongly connected", ret
|
||||
return 'Is strongly connected', ret
|
||||
|
||||
|
||||
def _is_dag(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Check if graph is directed acyclic graph (DAG)"""
|
||||
'''Check if graph is directed acyclic graph (DAG)'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.dag.is_directed_acyclic_graph(g)
|
||||
return "Is DAG", ret
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.dag.is_directed_acyclic_graph(g))
|
||||
return 'Is DAG', ret
|
||||
|
||||
|
||||
def _is_eulerian(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Checks if graph is Eulerian."""
|
||||
'''Checks if graph is Eulerian.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.euler.is_eulerian(g)
|
||||
return "Is eulerian", ret
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.euler.is_eulerian(g))
|
||||
return 'Is eulerian', ret
|
||||
|
||||
|
||||
def _is_forest(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Checks if graph is forest, all components must be trees."""
|
||||
'''Checks if graph is forest, all components must be trees.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_forest(g)
|
||||
return "Is forest", ret
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.tree.recognition.is_forest(g))
|
||||
return 'Is forest', ret
|
||||
|
||||
|
||||
def _is_tree(g: nx.MultiDiGraph) -> Tuple[str, bool]:
|
||||
"""Checks if graph is tree."""
|
||||
'''Checks if graph is tree.'''
|
||||
_, number_of_nodes = _number_of_nodes(g)
|
||||
ret = False if number_of_nodes == 0 else nx.algorithms.tree.recognition.is_tree(g)
|
||||
return "Is tree", ret
|
||||
ret = (False if number_of_nodes == 0
|
||||
else nx.algorithms.tree.recognition.is_tree(g))
|
||||
return 'Is tree', ret
|
||||
|
||||
|
||||
def _bridges(g: nx.MultiDiGraph) -> Tuple[str, int]:
|
||||
"""Returns number of bridges, multiple edges between same nodes are
|
||||
mapped to one edge."""
|
||||
return "Number of bridges", sum(1 for _ in nx.bridges(nx.Graph(g)))
|
||||
'''Returns number of bridges, multiple edges between same nodes are
|
||||
mapped to one edge.'''
|
||||
return 'Number of bridges', sum(1 for _ in nx.bridges(nx.Graph(g)))
|
||||
|
||||
|
||||
def _articulation_points(g: nx.MultiDiGraph):
|
||||
"""Returns number of articulation points."""
|
||||
'''Returns number of articulation points.'''
|
||||
undirected = nx.MultiDiGraph.to_undirected(g)
|
||||
return (
|
||||
"Number of articulation points",
|
||||
sum(1 for _ in nx.articulation_points(undirected)),
|
||||
)
|
||||
return ('Number of articulation points',
|
||||
sum(1 for _ in nx.articulation_points(undirected)))
|
||||
|
||||
|
||||
def _weakly_components(g: nx.MultiDiGraph):
|
||||
"""Returns number of weakly components."""
|
||||
'''Returns number of weakly components.'''
|
||||
comps = nx.algorithms.components.number_weakly_connected_components(g)
|
||||
return "Number of weakly connected components", comps
|
||||
return 'Number of weakly connected components', comps
|
||||
|
||||
|
||||
def _strongly_components(g: nx.MultiDiGraph):
|
||||
"""Returns number of strongly connected components."""
|
||||
'''Returns number of strongly connected components.'''
|
||||
comps = nx.algorithms.components.number_strongly_connected_components(g)
|
||||
return "Number of strongly connected components", comps
|
||||
return 'Number of strongly connected components', comps
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
import sys
|
||||
import mgp
|
||||
import collections
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write(
|
||||
(
|
||||
"\n"
|
||||
"NOTE: Please install networkx to be able to use Memgraph NetworkX "
|
||||
"wrappers. Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
)
|
||||
sys.stderr.write((
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use Memgraph NetworkX '
|
||||
'wrappers. Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n'))
|
||||
raise import_error
|
||||
|
||||
|
||||
class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
__slots__ = ("_ctx", "_succ", "_multi")
|
||||
__slots__ = ('_ctx', '_succ', '_multi')
|
||||
|
||||
def __init__(self, ctx, succ=True, multi=True):
|
||||
self._ctx = ctx
|
||||
@@ -26,7 +24,8 @@ class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
def __getitem__(self, key):
|
||||
if key not in self:
|
||||
raise KeyError
|
||||
return MemgraphAdjlistInnerDict(key, succ=self._succ, multi=self._multi)
|
||||
return MemgraphAdjlistInnerDict(key, succ=self._succ,
|
||||
multi=self._multi)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._ctx.graph.vertices)
|
||||
@@ -41,7 +40,7 @@ class MemgraphAdjlistOuterDict(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphAdjlistInnerDict(collections.abc.Mapping):
|
||||
__slots__ = ("_node", "_succ", "_multi", "_neighbors")
|
||||
__slots__ = ('_node', '_succ', '_multi', '_neighbors')
|
||||
|
||||
def __init__(self, node, succ=True, multi=True):
|
||||
self._node = node
|
||||
@@ -72,26 +71,31 @@ class MemgraphAdjlistInnerDict(collections.abc.Mapping):
|
||||
def _get_neighbors(self):
|
||||
if not self._neighbors:
|
||||
if self._succ:
|
||||
self._neighbors = set(e.to_vertex for e in self._node.out_edges)
|
||||
self._neighbors = set(
|
||||
e.to_vertex for e in self._node.out_edges)
|
||||
else:
|
||||
self._neighbors = set(e.from_vertex for e in self._node.in_edges)
|
||||
self._neighbors = set(
|
||||
e.from_vertex for e in self._node.in_edges)
|
||||
return self._neighbors
|
||||
|
||||
def _get_edge(self, neighbor):
|
||||
if self._succ:
|
||||
edge = list(filter(lambda e: e.to_vertex == neighbor, self._node.out_edges))
|
||||
edge = list(filter(lambda e: e.to_vertex == neighbor,
|
||||
self._node.out_edges))
|
||||
else:
|
||||
edge = list(filter(lambda e: e.from_vertex == neighbor, self._node.in_edges))
|
||||
edge = list(filter(lambda e: e.from_vertex == neighbor,
|
||||
self._node.in_edges))
|
||||
|
||||
assert len(edge) >= 1
|
||||
if len(edge) > 1:
|
||||
raise RuntimeError("Graph contains multiedges but " "is of non-multigraph type: {}".format(edge))
|
||||
raise RuntimeError('Graph contains multiedges but '
|
||||
'is of non-multigraph type: {}'.format(edge))
|
||||
|
||||
return edge[0]
|
||||
|
||||
|
||||
class MemgraphEdgeKeyDict(collections.abc.Mapping):
|
||||
__slots__ = ("_node", "_neighbor", "_succ", "_edges")
|
||||
__slots__ = ('_node', '_neighbor', '_succ', '_edges')
|
||||
|
||||
def __init__(self, node, neighbor, succ=True):
|
||||
self._node = node
|
||||
@@ -118,14 +122,18 @@ class MemgraphEdgeKeyDict(collections.abc.Mapping):
|
||||
def _get_edges(self):
|
||||
if not self._edges:
|
||||
if self._succ:
|
||||
self._edges = list(filter(lambda e: e.to_vertex == self._neighbor, self._node.out_edges))
|
||||
self._edges = list(filter(
|
||||
lambda e: e.to_vertex == self._neighbor,
|
||||
self._node.out_edges))
|
||||
else:
|
||||
self._edges = list(filter(lambda e: e.from_vertex == self._neighbor, self._node.in_edges))
|
||||
self._edges = list(filter(
|
||||
lambda e: e.from_vertex == self._neighbor,
|
||||
self._node.in_edges))
|
||||
return self._edges
|
||||
|
||||
|
||||
class UnhashableProperties(collections.abc.Mapping):
|
||||
__slots__ = "_properties"
|
||||
__slots__ = ('_properties')
|
||||
|
||||
def __init__(self, properties):
|
||||
self._properties = properties
|
||||
@@ -147,7 +155,7 @@ class UnhashableProperties(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphNodeDict(collections.abc.Mapping):
|
||||
__slots__ = ("_ctx",)
|
||||
__slots__ = ('_ctx',)
|
||||
|
||||
def __init__(self, ctx):
|
||||
self._ctx = ctx
|
||||
@@ -179,7 +187,8 @@ class MemgraphNodeDict(collections.abc.Mapping):
|
||||
|
||||
|
||||
class MemgraphDiGraphBase:
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, multi=True, **kwargs):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, multi=True,
|
||||
**kwargs):
|
||||
# NOTE: We assume that our graph will never be given any initial data
|
||||
# because we already pull our data from the Memgraph database. This
|
||||
# assert is triggered by certain NetworkX procedures because they
|
||||
@@ -192,30 +201,23 @@ class MemgraphDiGraphBase:
|
||||
# modify the graph's internal attributes and don't try to populate it
|
||||
# with initial data or modify it.
|
||||
|
||||
self.node_dict_factory = lambda: MemgraphNodeDict(ctx) if ctx else self._error
|
||||
self.node_dict_factory = lambda: MemgraphNodeDict(ctx) \
|
||||
if ctx else self._error
|
||||
self.node_attr_dict_factory = self._error
|
||||
|
||||
self.adjlist_outer_dict_factory = lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) if ctx else self._error
|
||||
self.adjlist_outer_dict_factory = \
|
||||
lambda: MemgraphAdjlistOuterDict(ctx, multi=multi) \
|
||||
if ctx else self._error
|
||||
self.adjlist_inner_dict_factory = self._error
|
||||
self.edge_key_dict_factory = self._error
|
||||
self.edge_attr_dict_factory = self._error
|
||||
|
||||
# NOTE: We forbid any mutating operations because our graph is
|
||||
# immutable and pulls its data from the Memgraph database.
|
||||
for f in [
|
||||
"add_node",
|
||||
"add_nodes_from",
|
||||
"remove_node",
|
||||
"remove_nodes_from",
|
||||
"add_edge",
|
||||
"add_edges_from",
|
||||
"add_weighted_edges_from",
|
||||
"new_edge_key",
|
||||
"remove_edge",
|
||||
"remove_edges_from",
|
||||
"update",
|
||||
"clear",
|
||||
]:
|
||||
for f in ['add_node', 'add_nodes_from', 'remove_node',
|
||||
'remove_nodes_from', 'add_edge', 'add_edges_from',
|
||||
'add_weighted_edges_from', 'new_edge_key', 'remove_edge',
|
||||
'remove_edges_from', 'update', 'clear']:
|
||||
setattr(self, f, lambda *args, **kwargs: self._error())
|
||||
|
||||
super().__init__(None, **kwargs)
|
||||
@@ -229,29 +231,33 @@ class MemgraphDiGraphBase:
|
||||
self._pred = MemgraphAdjlistOuterDict(ctx, succ=False, multi=multi)
|
||||
|
||||
def _error(self):
|
||||
raise RuntimeError("Modification operations are not supported")
|
||||
raise RuntimeError('Modification operations are not supported')
|
||||
|
||||
|
||||
class MemgraphMultiDiGraph(MemgraphDiGraphBase, nx.MultiDiGraph):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
|
||||
super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=True, **kwargs)
|
||||
super().__init__(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, multi=True, **kwargs)
|
||||
|
||||
|
||||
def MemgraphMultiGraph(incoming_graph_data=None, ctx=None, **kwargs):
|
||||
return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
return MemgraphMultiDiGraph(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
|
||||
|
||||
class MemgraphDiGraph(MemgraphDiGraphBase, nx.DiGraph):
|
||||
def __init__(self, incoming_graph_data=None, ctx=None, **kwargs):
|
||||
super().__init__(incoming_graph_data=incoming_graph_data, ctx=ctx, multi=False, **kwargs)
|
||||
super().__init__(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, multi=False, **kwargs)
|
||||
|
||||
|
||||
def MemgraphGraph(incoming_graph_data=None, ctx=None, **kwargs):
|
||||
return MemgraphDiGraph(incoming_graph_data=incoming_graph_data, ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
return MemgraphDiGraph(incoming_graph_data=incoming_graph_data,
|
||||
ctx=ctx, **kwargs).to_undirected(as_view=True)
|
||||
|
||||
|
||||
class PropertiesDictionary(collections.abc.Mapping):
|
||||
__slots__ = ("_ctx", "_prop", "_len")
|
||||
__slots__ = ('_ctx', '_prop', '_len')
|
||||
|
||||
def __init__(self, ctx, prop):
|
||||
self._ctx = ctx
|
||||
@@ -264,7 +270,8 @@ class PropertiesDictionary(collections.abc.Mapping):
|
||||
try:
|
||||
return vertex.properties[self._prop]
|
||||
except KeyError:
|
||||
raise KeyError(("{} doesn\t have the required " + "property '{}'").format(vertex, self._prop))
|
||||
raise KeyError(("{} doesn\t have the required " +
|
||||
"property '{}'").format(vertex, self._prop))
|
||||
|
||||
def __iter__(self):
|
||||
for v in self._ctx.graph.vertices:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,20 +1,23 @@
|
||||
import sys
|
||||
import mgp
|
||||
|
||||
try:
|
||||
import networkx as nx
|
||||
except ImportError as import_error:
|
||||
sys.stderr.write(
|
||||
"\n" "NOTE: Please install networkx to be able to use wcc module.\n" "Using Python:\n" + sys.version + "\n"
|
||||
)
|
||||
'\n'
|
||||
'NOTE: Please install networkx to be able to use wcc module.\n'
|
||||
'Using Python:\n'
|
||||
+ sys.version +
|
||||
'\n')
|
||||
raise import_error
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def get_components(
|
||||
vertices: mgp.List[mgp.Vertex], edges: mgp.List[mgp.Edge]
|
||||
) -> mgp.Record(n_components=int, components=mgp.List[mgp.List[mgp.Vertex]]):
|
||||
"""
|
||||
def get_components(vertices: mgp.List[mgp.Vertex],
|
||||
edges: mgp.List[mgp.Edge]
|
||||
) -> mgp.Record(n_components=int,
|
||||
components=mgp.List[mgp.List[mgp.Vertex]]):
|
||||
'''
|
||||
This procedure finds weakly connected components of a given subgraph of a
|
||||
directed graph.
|
||||
|
||||
@@ -38,7 +41,7 @@ def get_components(
|
||||
WITH collect(n) AS nodes, collect(e) AS edges
|
||||
CALL wcc.get_components(nodes, edges) YIELD *
|
||||
RETURN n_components, components;
|
||||
"""
|
||||
'''
|
||||
g = nx.DiGraph()
|
||||
g.add_nodes_from(vertices)
|
||||
g.add_edges_from([(edge.from_vertex, edge.to_vertex) for edge in edges])
|
||||
|
||||
@@ -104,9 +104,7 @@ def retry(retry_limit, timeout=100):
|
||||
except Exception:
|
||||
time.sleep(timeout)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return inner_func
|
||||
|
||||
|
||||
@@ -165,15 +163,8 @@ def format_version(variant, version, offering, distance=None, shorthash=None, su
|
||||
|
||||
# Parse arguments.
|
||||
parser = argparse.ArgumentParser(description="Get the current version of Memgraph.")
|
||||
parser.add_argument(
|
||||
"--open-source",
|
||||
action="store_true",
|
||||
help="set the current offering to 'open-source'",
|
||||
)
|
||||
parser.add_argument(
|
||||
"version",
|
||||
help="manual version override, if supplied the version isn't " "determined using git",
|
||||
)
|
||||
parser.add_argument("--open-source", action="store_true", help="set the current offering to 'open-source'")
|
||||
parser.add_argument("version", help="manual version override, if supplied the version isn't " "determined using git")
|
||||
parser.add_argument("suffix", help="custom suffix for the current version being built")
|
||||
parser.add_argument(
|
||||
"--variant",
|
||||
@@ -182,9 +173,7 @@ parser.add_argument(
|
||||
help="which variant of the version string should be generated",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--memgraph-root-dir",
|
||||
help="The root directory of the checked out " "Memgraph repository.",
|
||||
default=".",
|
||||
"--memgraph-root-dir", help="The root directory of the checked out " "Memgraph repository.", default="."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
@@ -267,27 +256,14 @@ for version in versions:
|
||||
if current_version is None:
|
||||
raise Exception("You are attempting to determine the version for a very " "old version of Memgraph!")
|
||||
version, branch, master_branch_merge = current_version
|
||||
distance = int(
|
||||
get_output(
|
||||
"git",
|
||||
"rev-list",
|
||||
"--count",
|
||||
"--first-parent",
|
||||
master_branch_merge + ".." + current_hash,
|
||||
)
|
||||
)
|
||||
distance = int(get_output("git", "rev-list", "--count", "--first-parent", master_branch_merge + ".." + current_hash))
|
||||
version_str = ".".join(map(str, version)) + ".0"
|
||||
if distance == 0:
|
||||
print(format_version(args.variant, version_str, offering, suffix=args.suffix), end="")
|
||||
else:
|
||||
print(
|
||||
format_version(
|
||||
args.variant,
|
||||
version_str,
|
||||
offering,
|
||||
distance=distance,
|
||||
shorthash=current_hash_short,
|
||||
suffix=args.suffix,
|
||||
args.variant, version_str, offering, distance=distance, shorthash=current_hash_short, suffix=args.suffix
|
||||
),
|
||||
end="",
|
||||
)
|
||||
|
||||
3
release/mgp/.gitignore
vendored
Normal file
3
release/mgp/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
.venv
|
||||
dist
|
||||
mgp.py
|
||||
201
release/mgp/LICENSE
Normal file
201
release/mgp/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
4
release/mgp/README.md
Normal file
4
release/mgp/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# mgp
|
||||
|
||||
PyPi package used for type hinting when creating MAGE modules. The get started
|
||||
using MAGE repository checkout the repository here: https://github.com/memgraph/mage.
|
||||
255
release/mgp/_mgp.py
Normal file
255
release/mgp/_mgp.py
Normal file
@@ -0,0 +1,255 @@
|
||||
from typing import Any
|
||||
|
||||
|
||||
class MgpIterable:
|
||||
def get() -> Any:
|
||||
pass
|
||||
|
||||
def next() -> Any:
|
||||
pass
|
||||
|
||||
|
||||
class Vertex:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def underlying_graph_is_mutable() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def iter_properties() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
def get_property(self, property_name: str) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def set_property(self, property_name: str, value: Any) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def get_id() -> "VertexId": # type: ignore
|
||||
pass
|
||||
|
||||
def label_at(self, index: int) -> "Label": # type: ignore
|
||||
pass
|
||||
|
||||
def labels_count() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
def add_label(self, label: Any):
|
||||
pass
|
||||
|
||||
def remove_label(self, label: Any):
|
||||
pass
|
||||
|
||||
def iter_in_edges() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
def iter_out_edges() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Edge:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def underlying_graph_is_mutable() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def iter_properties() -> MgpIterable: # type: ignore
|
||||
pass
|
||||
|
||||
def get_property(self, property_name: str) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def set_property(self, property_name: str, valuse: Any) -> "Property": # type: ignore
|
||||
pass
|
||||
|
||||
def get_type_name() -> str: # type: ignore
|
||||
pass
|
||||
|
||||
def get_id() -> "EdgeId": # type: ignore
|
||||
pass
|
||||
|
||||
def from_vertex() -> Vertex: # type: ignore
|
||||
pass
|
||||
|
||||
def to_vertex() -> Vertex: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Path:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def make_with_start(vertex: Vertex) -> "Path": # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Graph:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class CypherType:
|
||||
pass
|
||||
|
||||
|
||||
class Message:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def source_type() -> str: # type: ignore
|
||||
pass
|
||||
|
||||
def topic_name() -> str: # type: ignore
|
||||
pass
|
||||
|
||||
def key() -> bytes: # type: ignore
|
||||
pass
|
||||
|
||||
def timestamp() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
def offset() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
def payload() -> bytes: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class Messages:
|
||||
def is_valid() -> bool: # type: ignore
|
||||
pass
|
||||
|
||||
def message_at(self, id: int) -> Message: # type: ignore
|
||||
pass
|
||||
|
||||
def total_messages() -> int: # type: ignore
|
||||
pass
|
||||
|
||||
|
||||
class UnknownError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnableToAllocateError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InsufficientBufferError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class OutOfRangeError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class LogicErrorError(Exception):
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class DeletedObjectError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidArgumentError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class KeyAlreadyExistsError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ImmutableObjectError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ValueConversionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SerializationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def type_nullable(elem: Any):
|
||||
pass
|
||||
|
||||
|
||||
def type_list(elem: Any):
|
||||
pass
|
||||
|
||||
|
||||
def type_bool():
|
||||
pass
|
||||
|
||||
|
||||
def type_string():
|
||||
pass
|
||||
|
||||
|
||||
def type_int():
|
||||
pass
|
||||
|
||||
|
||||
def type_float():
|
||||
pass
|
||||
|
||||
|
||||
def type_number():
|
||||
pass
|
||||
|
||||
|
||||
def type_map():
|
||||
pass
|
||||
|
||||
|
||||
def type_node():
|
||||
pass
|
||||
|
||||
|
||||
def type_relationship():
|
||||
pass
|
||||
|
||||
|
||||
def type_path():
|
||||
pass
|
||||
|
||||
|
||||
def type_date():
|
||||
pass
|
||||
|
||||
|
||||
def type_local_time():
|
||||
pass
|
||||
|
||||
|
||||
def type_local_date_time():
|
||||
pass
|
||||
|
||||
|
||||
def type_duration():
|
||||
pass
|
||||
|
||||
|
||||
def type_any():
|
||||
pass
|
||||
|
||||
|
||||
class _MODULE:
|
||||
@staticmethod
|
||||
def add_read_procedure(wrapper):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_write_procedure(wrapper):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_transformation(wrapper):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def add_function(wrapper):
|
||||
pass
|
||||
22
release/mgp/instructions.md
Normal file
22
release/mgp/instructions.md
Normal file
@@ -0,0 +1,22 @@
|
||||
# How to publish new versions
|
||||
## Prerequisites
|
||||
1. Installed poetry
|
||||
```
|
||||
pip install poetry
|
||||
```
|
||||
2. Set up [API tokens](https://pypi.org/help/#apitoken)
|
||||
3. Be a collaborator on [pypi](https://pypi.org/project/mgp/)
|
||||
|
||||
## Making changes
|
||||
1. Make changes to the package
|
||||
2. Bump version in `pyproject.tml`
|
||||
3. `poetry build`
|
||||
4. `poetry publish`
|
||||
|
||||
## Why is this not automatized?
|
||||
|
||||
Because someone always has to manually bump up the version in `pyproject.toml`
|
||||
|
||||
## Why does `_mgp.py` exists?
|
||||
Because we are mocking here all the types that are created by Memgraph
|
||||
in order to fix typing errors in `mgp.py`.
|
||||
23
release/mgp/pyproject.toml
Normal file
23
release/mgp/pyproject.toml
Normal file
@@ -0,0 +1,23 @@
|
||||
[tool.poetry]
|
||||
name = "mgp"
|
||||
version = "1.0.0"
|
||||
description = "Memgraph's module for developing MAGE modules. Used only for type hinting!"
|
||||
authors = [
|
||||
"MasterMedo <mislav.vuletic@gmail.com>",
|
||||
"jbajic <jure.bajic@memgraph.io>",
|
||||
"katarinasupe <katarina.supe@memgraph.io>",
|
||||
"antejavor <ante.javor@memgraph.io>",
|
||||
"antaljanosbenjamin <benjamin.antal@memgraph.io>",
|
||||
]
|
||||
license = "Apache-2.0"
|
||||
readme = "README.md"
|
||||
include = ["mgp.py", "_mgp.py"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.7"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
@@ -84,8 +84,6 @@ std::string PermissionToString(Permission permission) {
|
||||
return "MODULE_WRITE";
|
||||
case Permission::WEBSOCKET:
|
||||
return "WEBSOCKET";
|
||||
case Permission::LABELS:
|
||||
return "LABELS";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,107 +183,19 @@ bool operator==(const Permissions &first, const Permissions &second) {
|
||||
|
||||
bool operator!=(const Permissions &first, const Permissions &second) { return !(first == second); }
|
||||
|
||||
LabelPermissions::LabelPermissions(const std::unordered_set<std::string> &grants,
|
||||
const std::unordered_set<std::string> &denies)
|
||||
: grants_(grants), denies_(denies) {}
|
||||
|
||||
PermissionLevel LabelPermissions::Has(const std::string &permission) const {
|
||||
if (denies_.find(permission) != denies_.end()) {
|
||||
return PermissionLevel::DENY;
|
||||
}
|
||||
|
||||
if (grants_.find(permission) != denies_.end()) {
|
||||
return PermissionLevel::GRANT;
|
||||
}
|
||||
|
||||
return PermissionLevel::NEUTRAL;
|
||||
}
|
||||
|
||||
void LabelPermissions::Grant(const std::string &permission) {
|
||||
auto deniedPermissionIter = denies_.find(permission);
|
||||
|
||||
if (deniedPermissionIter != denies_.end()) {
|
||||
denies_.erase(deniedPermissionIter);
|
||||
}
|
||||
|
||||
if (grants_.find(permission) == grants_.end()) {
|
||||
grants_.insert(permission);
|
||||
}
|
||||
}
|
||||
|
||||
void LabelPermissions::Revoke(const std::string &permission) {
|
||||
auto deniedPermissionIter = denies_.find(permission);
|
||||
auto grantedPermissionIter = grants_.find(permission);
|
||||
|
||||
if (deniedPermissionIter != denies_.end()) {
|
||||
denies_.erase(deniedPermissionIter);
|
||||
}
|
||||
|
||||
if (grantedPermissionIter != grants_.end()) {
|
||||
grants_.erase(grantedPermissionIter);
|
||||
}
|
||||
}
|
||||
|
||||
void LabelPermissions::Deny(const std::string &permission) {
|
||||
auto grantedPermissionIter = grants_.find(permission);
|
||||
|
||||
if (grantedPermissionIter != grants_.end()) {
|
||||
grants_.erase(grantedPermissionIter);
|
||||
}
|
||||
|
||||
if (denies_.find(permission) == denies_.end()) {
|
||||
denies_.insert(permission);
|
||||
}
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> LabelPermissions::GetGrants() const { return grants_; }
|
||||
|
||||
std::unordered_set<std::string> LabelPermissions::GetDenies() const { return denies_; }
|
||||
|
||||
nlohmann::json LabelPermissions::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["grants"] = grants_;
|
||||
data["denies"] = denies_;
|
||||
return data;
|
||||
}
|
||||
|
||||
LabelPermissions LabelPermissions::Deserialize(const nlohmann::json &data) {
|
||||
if (!data.is_object()) {
|
||||
throw AuthException("Couldn't load permissions data!");
|
||||
}
|
||||
|
||||
return {LabelPermissions(data["grants"], data["denies"])};
|
||||
}
|
||||
|
||||
std::unordered_set<std::string> LabelPermissions::grants() const { return grants_; }
|
||||
std::unordered_set<std::string> LabelPermissions::denies() const { return denies_; }
|
||||
|
||||
bool operator==(const LabelPermissions &first, const LabelPermissions &second) {
|
||||
return first.grants() == second.grants() && first.denies() == second.denies();
|
||||
}
|
||||
|
||||
bool operator!=(const LabelPermissions &first, const LabelPermissions &second) { return !(first == second); }
|
||||
|
||||
Role::Role(const std::string &rolename) : rolename_(utils::ToLowerCase(rolename)) {}
|
||||
|
||||
Role::Role(const std::string &rolename, const Permissions &permissions)
|
||||
: rolename_(utils::ToLowerCase(rolename)), permissions_(permissions) {}
|
||||
|
||||
Role::Role(const std::string &rolename, const Permissions &permissions, const LabelPermissions &labelPermissions)
|
||||
: rolename_(utils::ToLowerCase(rolename)), permissions_(permissions), labelPermissions_(labelPermissions) {}
|
||||
|
||||
const std::string &Role::rolename() const { return rolename_; }
|
||||
const Permissions &Role::permissions() const { return permissions_; }
|
||||
Permissions &Role::permissions() { return permissions_; }
|
||||
|
||||
LabelPermissions &Role::labelPermissions() { return labelPermissions_; }
|
||||
|
||||
nlohmann::json Role::Serialize() const {
|
||||
nlohmann::json data = nlohmann::json::object();
|
||||
data["rolename"] = rolename_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
data["labelPermissions"] = labelPermissions_.Serialize();
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -297,9 +207,7 @@ Role Role::Deserialize(const nlohmann::json &data) {
|
||||
throw AuthException("Couldn't load role data!");
|
||||
}
|
||||
auto permissions = Permissions::Deserialize(data["permissions"]);
|
||||
auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
|
||||
|
||||
return {data["rolename"], permissions, labelPermissions};
|
||||
return {data["rolename"], permissions};
|
||||
}
|
||||
|
||||
bool operator==(const Role &first, const Role &second) {
|
||||
@@ -311,13 +219,6 @@ User::User(const std::string &username) : username_(utils::ToLowerCase(username)
|
||||
User::User(const std::string &username, const std::string &password_hash, const Permissions &permissions)
|
||||
: username_(utils::ToLowerCase(username)), password_hash_(password_hash), permissions_(permissions) {}
|
||||
|
||||
User::User(const std::string &username, const std::string &password_hash, const Permissions &permissions,
|
||||
const LabelPermissions &labelPermissions)
|
||||
: username_(utils::ToLowerCase(username)),
|
||||
password_hash_(password_hash),
|
||||
permissions_(permissions),
|
||||
labelPermissions_(labelPermissions) {}
|
||||
|
||||
bool User::CheckPassword(const std::string &password) {
|
||||
if (password_hash_.empty()) return true;
|
||||
return VerifyPassword(password, password_hash_);
|
||||
@@ -370,8 +271,6 @@ const std::string &User::username() const { return username_; }
|
||||
const Permissions &User::permissions() const { return permissions_; }
|
||||
Permissions &User::permissions() { return permissions_; }
|
||||
|
||||
LabelPermissions &User::labelPermissions() { return labelPermissions_; }
|
||||
|
||||
const Role *User::role() const {
|
||||
if (role_.has_value()) {
|
||||
return &role_.value();
|
||||
@@ -384,7 +283,6 @@ nlohmann::json User::Serialize() const {
|
||||
data["username"] = username_;
|
||||
data["password_hash"] = password_hash_;
|
||||
data["permissions"] = permissions_.Serialize();
|
||||
data["labelPermissions"] = labelPermissions_.Serialize();
|
||||
// The role shouldn't be serialized here, it is stored as a foreign key.
|
||||
return data;
|
||||
}
|
||||
@@ -397,14 +295,11 @@ User User::Deserialize(const nlohmann::json &data) {
|
||||
throw AuthException("Couldn't load user data!");
|
||||
}
|
||||
auto permissions = Permissions::Deserialize(data["permissions"]);
|
||||
auto labelPermissions = LabelPermissions::Deserialize(data["labelPermissions"]);
|
||||
|
||||
return {data["username"], data["password_hash"], permissions, labelPermissions};
|
||||
return {data["username"], data["password_hash"], permissions};
|
||||
}
|
||||
|
||||
bool operator==(const User &first, const User &second) {
|
||||
return first.username_ == second.username_ && first.password_hash_ == second.password_hash_ &&
|
||||
first.permissions_ == second.permissions_ && first.role_ == second.role_;
|
||||
}
|
||||
|
||||
} // namespace memgraph::auth
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include <string>
|
||||
|
||||
#include <json/json.hpp>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace memgraph::auth {
|
||||
// These permissions must have values that are applicable for usage in a
|
||||
@@ -39,8 +38,7 @@ enum class Permission : uint64_t {
|
||||
STREAM = 1U << 17U,
|
||||
MODULE_READ = 1U << 18U,
|
||||
MODULE_WRITE = 1U << 19U,
|
||||
WEBSOCKET = 1U << 20U,
|
||||
LABELS = 1U << 21U
|
||||
WEBSOCKET = 1U << 20U
|
||||
};
|
||||
// clang-format on
|
||||
|
||||
@@ -90,52 +88,16 @@ bool operator==(const Permissions &first, const Permissions &second);
|
||||
|
||||
bool operator!=(const Permissions &first, const Permissions &second);
|
||||
|
||||
class LabelPermissions final {
|
||||
public:
|
||||
LabelPermissions(const std::unordered_set<std::string> &grants = {},
|
||||
const std::unordered_set<std::string> &denies = {});
|
||||
|
||||
PermissionLevel Has(const std::string &permission) const;
|
||||
|
||||
void Grant(const std::string &permission);
|
||||
|
||||
void Revoke(const std::string &permission);
|
||||
|
||||
void Deny(const std::string &permission);
|
||||
|
||||
std::unordered_set<std::string> GetGrants() const;
|
||||
std::unordered_set<std::string> GetDenies() const;
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
static LabelPermissions Deserialize(const nlohmann::json &data);
|
||||
|
||||
std::unordered_set<std::string> grants() const;
|
||||
std::unordered_set<std::string> denies() const;
|
||||
|
||||
private:
|
||||
std::unordered_set<std::string> grants_{};
|
||||
std::unordered_set<std::string> denies_{};
|
||||
};
|
||||
|
||||
bool operator==(const LabelPermissions &first, const LabelPermissions &second);
|
||||
|
||||
bool operator!=(const LabelPermissions &first, const LabelPermissions &second);
|
||||
class Role final {
|
||||
public:
|
||||
Role(const std::string &rolename);
|
||||
|
||||
Role(const std::string &rolename, const Permissions &permissions);
|
||||
|
||||
Role(const std::string &rolename, const Permissions &permissions, const LabelPermissions &labelPermissions);
|
||||
|
||||
const std::string &rolename() const;
|
||||
const Permissions &permissions() const;
|
||||
Permissions &permissions();
|
||||
|
||||
LabelPermissions &labelPermissions();
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
@@ -146,7 +108,6 @@ class Role final {
|
||||
private:
|
||||
std::string rolename_;
|
||||
Permissions permissions_;
|
||||
LabelPermissions labelPermissions_;
|
||||
};
|
||||
|
||||
bool operator==(const Role &first, const Role &second);
|
||||
@@ -158,9 +119,6 @@ class User final {
|
||||
|
||||
User(const std::string &username, const std::string &password_hash, const Permissions &permissions);
|
||||
|
||||
User(const std::string &username, const std::string &password_hash, const Permissions &permissions,
|
||||
const LabelPermissions &labelPermissions);
|
||||
|
||||
/// @throw AuthException if unable to verify the password.
|
||||
bool CheckPassword(const std::string &password);
|
||||
|
||||
@@ -180,8 +138,6 @@ class User final {
|
||||
|
||||
const Role *role() const;
|
||||
|
||||
LabelPermissions &labelPermissions();
|
||||
|
||||
nlohmann::json Serialize() const;
|
||||
|
||||
/// @throw AuthException if unable to deserialize.
|
||||
@@ -194,9 +150,7 @@ class User final {
|
||||
std::string password_hash_;
|
||||
Permissions permissions_;
|
||||
std::optional<Role> role_;
|
||||
LabelPermissions labelPermissions_;
|
||||
};
|
||||
|
||||
bool operator==(const User &first, const User &second);
|
||||
|
||||
} // namespace memgraph::auth
|
||||
|
||||
@@ -18,24 +18,19 @@ roles_config = config["roles"]
|
||||
# Initialize LDAP server.
|
||||
tls = None
|
||||
if server_config["encryption"] != "disabled":
|
||||
cert_file = server_config["cert_file"] if server_config["cert_file"] else None
|
||||
cert_file = server_config["cert_file"] if server_config["cert_file"] \
|
||||
else None
|
||||
key_file = server_config["key_file"] if server_config["key_file"] else None
|
||||
ca_file = server_config["ca_file"] if server_config["ca_file"] else None
|
||||
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] else ssl.CERT_NONE
|
||||
tls = ldap3.Tls(
|
||||
local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
ca_certs_file=ca_file,
|
||||
validate=validate,
|
||||
)
|
||||
validate = ssl.CERT_REQUIRED if server_config["validate_cert"] \
|
||||
else ssl.CERT_NONE
|
||||
tls = ldap3.Tls(local_private_key_file=key_file,
|
||||
local_certificate_file=cert_file,
|
||||
ca_certs_file=ca_file,
|
||||
validate=validate)
|
||||
use_ssl = server_config["encryption"] == "ssl"
|
||||
server = ldap3.Server(
|
||||
server_config["host"],
|
||||
port=server_config["port"],
|
||||
tls=tls,
|
||||
use_ssl=use_ssl,
|
||||
get_info=ldap3.ALL,
|
||||
)
|
||||
server = ldap3.Server(server_config["host"], port=server_config["port"],
|
||||
tls=tls, use_ssl=use_ssl, get_info=ldap3.ALL)
|
||||
|
||||
|
||||
# Main authentication/authorization function.
|
||||
@@ -45,12 +40,14 @@ def authenticate(username, password):
|
||||
return {"authenticated": False, "role": ""}
|
||||
|
||||
# Create the DN of the user
|
||||
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + users_config["suffix"]
|
||||
dn = users_config["prefix"] + ldap3.utils.dn.escape_rdn(username) + \
|
||||
users_config["suffix"]
|
||||
|
||||
# Bind to the server
|
||||
conn = ldap3.Connection(server, dn, password)
|
||||
if server_config["encryption"] == "starttls" and not conn.start_tls():
|
||||
print("ERROR: Couldn't issue STARTTLS to the LDAP server!", file=sys.stderr)
|
||||
print("ERROR: Couldn't issue STARTTLS to the LDAP server!",
|
||||
file=sys.stderr)
|
||||
return {"authenticated": False, "role": ""}
|
||||
if not conn.bind():
|
||||
return {"authenticated": False, "role": ""}
|
||||
@@ -59,32 +56,25 @@ def authenticate(username, password):
|
||||
if roles_config["root_dn"] != "":
|
||||
# search for role
|
||||
search_filter = "(&(objectclass={objclass})({attr}={value}))".format(
|
||||
objclass=roles_config["root_objectclass"],
|
||||
attr=roles_config["user_attribute"],
|
||||
value=ldap3.utils.conv.escape_filter_chars(dn),
|
||||
)
|
||||
succ = conn.search(
|
||||
roles_config["root_dn"],
|
||||
search_filter,
|
||||
search_scope=ldap3.LEVEL,
|
||||
attributes=[roles_config["role_attribute"]],
|
||||
)
|
||||
objclass=roles_config["root_objectclass"],
|
||||
attr=roles_config["user_attribute"],
|
||||
value=ldap3.utils.conv.escape_filter_chars(dn))
|
||||
succ = conn.search(roles_config["root_dn"], search_filter,
|
||||
search_scope=ldap3.LEVEL,
|
||||
attributes=[roles_config["role_attribute"]])
|
||||
if not succ or len(conn.entries) == 0:
|
||||
return {"authenticated": True, "role": ""}
|
||||
if len(conn.entries) > 1:
|
||||
roles = list(map(lambda x: x[roles_config["role_attribute"]].value, conn.entries))
|
||||
roles = list(map(lambda x: x[roles_config["role_attribute"]].value,
|
||||
conn.entries))
|
||||
# Because we don't know exactly which role the user should have
|
||||
# we authorize the user with an empty role.
|
||||
print(
|
||||
"WARNING: Found more than one role for " "user '" + username + "':",
|
||||
", ".join(roles) + "!",
|
||||
file=sys.stderr,
|
||||
)
|
||||
print("WARNING: Found more than one role for "
|
||||
"user '" + username + "':", ", ".join(roles) + "!",
|
||||
file=sys.stderr)
|
||||
return {"authenticated": True, "role": ""}
|
||||
return {
|
||||
"authenticated": True,
|
||||
"role": conn.entries[0][roles_config["role_attribute"]].value,
|
||||
}
|
||||
return {"authenticated": True,
|
||||
"role": conn.entries[0][roles_config["role_attribute"]].value}
|
||||
else:
|
||||
return {"authenticated": True, "role": ""}
|
||||
|
||||
|
||||
@@ -57,8 +57,6 @@ auth::Permission PrivilegeToPermission(query::AuthQuery::Privilege privilege) {
|
||||
return auth::Permission::MODULE_WRITE;
|
||||
case query::AuthQuery::Privilege::WEBSOCKET:
|
||||
return auth::Permission::WEBSOCKET;
|
||||
case query::AuthQuery::Privilege::LABELS:
|
||||
return auth::Permission::LABELS;
|
||||
}
|
||||
}
|
||||
} // namespace memgraph::glue
|
||||
|
||||
@@ -216,6 +216,11 @@ DEFINE_bool(telemetry_enabled, false,
|
||||
"the database runtime (vertex and edge counts and resource usage) "
|
||||
"to allow for easier improvement of the product.");
|
||||
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_bool(storage_restore_replicas_on_startup, true,
|
||||
"Controls replicas should be restored automatically."); // TODO(42jeremy) this must be removed once T0835
|
||||
// is implemented.
|
||||
|
||||
// Streams flags
|
||||
// NOLINTNEXTLINE (cppcoreguidelines-avoid-non-const-global-variables)
|
||||
DEFINE_uint32(
|
||||
@@ -501,7 +506,7 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
|
||||
if (first_user) {
|
||||
spdlog::info("{} is first created user. Granting all privileges.", username);
|
||||
GrantPrivilege(username, memgraph::query::kPrivilegesAll, {"*"});
|
||||
GrantPrivilege(username, memgraph::query::kPrivilegesAll);
|
||||
}
|
||||
|
||||
return user_added;
|
||||
@@ -747,9 +752,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void GrantPrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) override {
|
||||
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
// TODO (mferencevic): should we first check that the
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -758,9 +762,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void DenyPrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) override {
|
||||
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
// TODO (mferencevic): should we first check that the
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -769,9 +772,8 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
}
|
||||
|
||||
void RevokePrivilege(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) override {
|
||||
EditPermissions(user_or_role, privileges, labels, [](auto *permissions, const auto &permission) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges) override {
|
||||
EditPermissions(user_or_role, privileges, [](auto *permissions, const auto &permission) {
|
||||
// TODO (mferencevic): should we first check that the
|
||||
// privilege is granted/denied/revoked before
|
||||
// unconditionally granting/denying/revoking it?
|
||||
@@ -782,8 +784,7 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
private:
|
||||
template <class TEditFun>
|
||||
void EditPermissions(const std::string &user_or_role,
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels, const TEditFun &edit_fun) {
|
||||
const std::vector<memgraph::query::AuthQuery::Privilege> &privileges, const TEditFun &edit_fun) {
|
||||
if (!std::regex_match(user_or_role, name_regex_)) {
|
||||
throw memgraph::query::QueryRuntimeException("Invalid user or role name.");
|
||||
}
|
||||
@@ -803,17 +804,11 @@ class AuthQueryHandler final : public memgraph::query::AuthQueryHandler {
|
||||
for (const auto &permission : permissions) {
|
||||
edit_fun(&user->permissions(), permission);
|
||||
}
|
||||
for (const auto &label : labels) {
|
||||
edit_fun(&user->labelPermissions(), label);
|
||||
}
|
||||
locked_auth->SaveUser(*user);
|
||||
} else {
|
||||
for (const auto &permission : permissions) {
|
||||
edit_fun(&role->permissions(), permission);
|
||||
}
|
||||
for (const auto &label : labels) {
|
||||
edit_fun(&role->labelPermissions(), label);
|
||||
}
|
||||
locked_auth->SaveRole(*role);
|
||||
}
|
||||
} catch (const memgraph::auth::AuthException &e) {
|
||||
@@ -1205,7 +1200,8 @@ int main(int argc, char **argv) {
|
||||
.snapshot_retention_count = FLAGS_storage_snapshot_retention_count,
|
||||
.wal_file_size_kibibytes = FLAGS_storage_wal_file_size_kib,
|
||||
.wal_file_flush_every_n_tx = FLAGS_storage_wal_file_flush_every_n_tx,
|
||||
.snapshot_on_exit = FLAGS_storage_snapshot_on_exit},
|
||||
.snapshot_on_exit = FLAGS_storage_snapshot_on_exit,
|
||||
.restore_replicas_on_startup = FLAGS_storage_restore_replicas_on_startup},
|
||||
.transaction = {.isolation_level = ParseIsolationLevel()}};
|
||||
if (FLAGS_storage_snapshot_interval_sec == 0) {
|
||||
if (FLAGS_storage_wal_enabled) {
|
||||
|
||||
@@ -2239,11 +2239,9 @@ cpp<#
|
||||
(user "std::string" :scope :public)
|
||||
(role "std::string" :scope :public)
|
||||
(user-or-role "std::string" :scope :public)
|
||||
|
||||
(password "Expression *" :initval "nullptr" :scope :public
|
||||
:slk-save #'slk-save-ast-pointer
|
||||
:slk-load (slk-load-ast-pointer "Expression"))
|
||||
(labels "std::vector<std::string>" :scope :public)
|
||||
(privileges "std::vector<Privilege>" :scope :public))
|
||||
(:public
|
||||
(lcp:define-enum action
|
||||
@@ -2255,7 +2253,7 @@ cpp<#
|
||||
(lcp:define-enum privilege
|
||||
(create delete match merge set remove index stats auth constraint
|
||||
dump replication durability read_file free_memory trigger config stream module_read module_write
|
||||
websocket labels)
|
||||
websocket)
|
||||
(:serialize))
|
||||
#>cpp
|
||||
AuthQuery() = default;
|
||||
@@ -2266,14 +2264,13 @@ cpp<#
|
||||
#>cpp
|
||||
AuthQuery(Action action, std::string user, std::string role,
|
||||
std::string user_or_role, Expression *password,
|
||||
std::vector<std::string> labels ,std::vector<Privilege> privileges)
|
||||
std::vector<Privilege> privileges)
|
||||
: action_(action),
|
||||
user_(user),
|
||||
role_(role),
|
||||
user_or_role_(user_or_role),
|
||||
password_(password),
|
||||
labels_(labels),
|
||||
privileges_(privileges){}
|
||||
privileges_(privileges) {}
|
||||
cpp<#)
|
||||
(:private
|
||||
#>cpp
|
||||
@@ -2298,8 +2295,7 @@ const std::vector<AuthQuery::Privilege> kPrivilegesAll = {
|
||||
AuthQuery::Privilege::FREE_MEMORY, AuthQuery::Privilege::TRIGGER,
|
||||
AuthQuery::Privilege::CONFIG, AuthQuery::Privilege::STREAM,
|
||||
AuthQuery::Privilege::MODULE_READ, AuthQuery::Privilege::MODULE_WRITE,
|
||||
AuthQuery::Privilege::WEBSOCKET,
|
||||
AuthQuery::Privilege::LABELS};
|
||||
AuthQuery::Privilege::WEBSOCKET};
|
||||
cpp<#
|
||||
|
||||
(lcp:define-class info-query (query)
|
||||
@@ -2379,10 +2375,7 @@ cpp<#
|
||||
(port "Expression *" :initval "nullptr" :scope :public
|
||||
:slk-save #'slk-save-ast-pointer
|
||||
:slk-load (slk-load-ast-pointer "Expression"))
|
||||
(sync_mode "SyncMode" :scope :public)
|
||||
(timeout "Expression *" :initval "nullptr" :scope :public
|
||||
:slk-save #'slk-save-ast-pointer
|
||||
:slk-load (slk-load-ast-pointer "Expression")))
|
||||
(sync_mode "SyncMode" :scope :public))
|
||||
|
||||
(:public
|
||||
(lcp:define-enum action
|
||||
|
||||
@@ -275,18 +275,7 @@ antlrcpp::Any CypherMainVisitor::visitRegisterReplica(MemgraphCypher::RegisterRe
|
||||
replication_query->replica_name_ = ctx->replicaName()->symbolicName()->accept(this).as<std::string>();
|
||||
if (ctx->SYNC()) {
|
||||
replication_query->sync_mode_ = memgraph::query::ReplicationQuery::SyncMode::SYNC;
|
||||
if (ctx->WITH() && ctx->TIMEOUT()) {
|
||||
if (ctx->timeout->numberLiteral()) {
|
||||
// we accept both double and integer literals
|
||||
replication_query->timeout_ = ctx->timeout->accept(this);
|
||||
} else {
|
||||
throw SemanticException("Timeout should be a integer or double literal!");
|
||||
}
|
||||
}
|
||||
} else if (ctx->ASYNC()) {
|
||||
if (ctx->WITH() && ctx->TIMEOUT()) {
|
||||
throw SyntaxException("Timeout can be set only for the SYNC replication mode!");
|
||||
}
|
||||
replication_query->sync_mode_ = memgraph::query::ReplicationQuery::SyncMode::ASYNC;
|
||||
}
|
||||
|
||||
@@ -1285,11 +1274,7 @@ antlrcpp::Any CypherMainVisitor::visitGrantPrivilege(MemgraphCypher::GrantPrivil
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
for (auto *privilege : ctx->privilegeList()->privilege()) {
|
||||
if (privilege->LABELS()) {
|
||||
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
|
||||
} else {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
} else {
|
||||
/* grant all privileges */
|
||||
@@ -1307,11 +1292,7 @@ antlrcpp::Any CypherMainVisitor::visitDenyPrivilege(MemgraphCypher::DenyPrivileg
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
for (auto *privilege : ctx->privilegeList()->privilege()) {
|
||||
if (privilege->LABELS()) {
|
||||
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
|
||||
} else {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
} else {
|
||||
/* deny all privileges */
|
||||
@@ -1329,11 +1310,7 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
|
||||
auth->user_or_role_ = ctx->userOrRole->accept(this).as<std::string>();
|
||||
if (ctx->privilegeList()) {
|
||||
for (auto *privilege : ctx->privilegeList()->privilege()) {
|
||||
if (privilege->LABELS()) {
|
||||
auth->labels_ = privilege->labelList()->accept(this).as<std::vector<std::string>>();
|
||||
} else {
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
auth->privileges_.push_back(privilege->accept(this));
|
||||
}
|
||||
} else {
|
||||
/* revoke all privileges */
|
||||
@@ -1342,22 +1319,6 @@ antlrcpp::Any CypherMainVisitor::visitRevokePrivilege(MemgraphCypher::RevokePriv
|
||||
return auth;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthQuery*
|
||||
*/
|
||||
antlrcpp::Any CypherMainVisitor::visitLabelList(MemgraphCypher::LabelListContext *ctx) {
|
||||
std::vector<std::string> labels;
|
||||
for (auto *label : ctx->label()) {
|
||||
if (label->ASTERISK()) {
|
||||
labels.push_back("*");
|
||||
} else {
|
||||
labels.push_back(label->symbolicName()->accept(this).as<std::string>());
|
||||
}
|
||||
}
|
||||
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AuthQuery::Privilege
|
||||
*/
|
||||
@@ -1383,10 +1344,6 @@ antlrcpp::Any CypherMainVisitor::visitPrivilege(MemgraphCypher::PrivilegeContext
|
||||
if (ctx->MODULE_READ()) return AuthQuery::Privilege::MODULE_READ;
|
||||
if (ctx->MODULE_WRITE()) return AuthQuery::Privilege::MODULE_WRITE;
|
||||
if (ctx->WEBSOCKET()) return AuthQuery::Privilege::WEBSOCKET;
|
||||
if (ctx->LABELS()) {
|
||||
// fill labels in authquery
|
||||
return AuthQuery::Privilege::LABELS;
|
||||
}
|
||||
LOG_FATAL("Should not get here - unknown privilege!");
|
||||
}
|
||||
|
||||
|
||||
@@ -473,11 +473,6 @@ class CypherMainVisitor : public antlropencypher::MemgraphCypherBaseVisitor {
|
||||
*/
|
||||
antlrcpp::Any visitPrivilege(MemgraphCypher::PrivilegeContext *ctx) override;
|
||||
|
||||
/**
|
||||
* @return AuthQuery::LabelList
|
||||
*/
|
||||
antlrcpp::Any visitLabelList(MemgraphCypher::LabelListContext *ctx) override;
|
||||
|
||||
/**
|
||||
* @return AuthQuery*
|
||||
*/
|
||||
|
||||
@@ -56,7 +56,6 @@ memgraphCypherKeyword : cypherKeyword
|
||||
| IDENTIFIED
|
||||
| ISOLATION
|
||||
| KAFKA
|
||||
| LABELS
|
||||
| LEVEL
|
||||
| LOAD
|
||||
| LOCK
|
||||
@@ -255,15 +254,10 @@ privilege : CREATE
|
||||
| MODULE_READ
|
||||
| MODULE_WRITE
|
||||
| WEBSOCKET
|
||||
| LABELS labels=labelList
|
||||
;
|
||||
|
||||
privilegeList : privilege ( ',' privilege )* ;
|
||||
|
||||
labelList : COLON label ( ',' COLON label )* ;
|
||||
|
||||
label : ( '*' | symbolicName ) ;
|
||||
|
||||
showPrivileges : SHOW PRIVILEGES FOR userOrRole=userOrRoleName ;
|
||||
|
||||
showRoleForUser : SHOW ROLE FOR user=userOrRoleName ;
|
||||
@@ -282,7 +276,6 @@ replicaName : symbolicName ;
|
||||
socketAddress : literal ;
|
||||
|
||||
registerReplica : REGISTER REPLICA replicaName ( SYNC | ASYNC )
|
||||
( WITH TIMEOUT timeout=literal ) ?
|
||||
TO socketAddress ;
|
||||
|
||||
dropReplica : DROP REPLICA replicaName ;
|
||||
|
||||
@@ -66,7 +66,6 @@ IDENTIFIED : I D E N T I F I E D ;
|
||||
IGNORE : I G N O R E ;
|
||||
ISOLATION : I S O L A T I O N ;
|
||||
KAFKA : K A F K A ;
|
||||
LABELS : L A B E L S ;
|
||||
LEVEL : L E V E L ;
|
||||
LOAD : L O A D ;
|
||||
LOCK : L O C K ;
|
||||
|
||||
@@ -204,9 +204,8 @@ const trie::Trie kKeywords = {"union",
|
||||
"pulsar",
|
||||
"service_url",
|
||||
"version",
|
||||
"websocket",
|
||||
"foreach",
|
||||
"labels"};
|
||||
"websocket"
|
||||
"foreach"};
|
||||
|
||||
// Unicode codepoints that are allowed at the start of the unescaped name.
|
||||
const std::bitset<kBitsetSize> kUnescapedNameAllowedStarts(
|
||||
|
||||
@@ -882,21 +882,36 @@ TypedValue Id(const TypedValue *args, int64_t nargs, const FunctionContext &ctx)
|
||||
}
|
||||
|
||||
TypedValue ToString(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
||||
FType<Or<Null, String, Number, Bool>>("toString", args, nargs);
|
||||
FType<Or<Null, String, Number, Date, LocalTime, LocalDateTime, Duration, Bool>>("toString", args, nargs);
|
||||
const auto &arg = args[0];
|
||||
if (arg.IsNull()) {
|
||||
return TypedValue(ctx.memory);
|
||||
} else if (arg.IsString()) {
|
||||
}
|
||||
if (arg.IsString()) {
|
||||
return TypedValue(arg, ctx.memory);
|
||||
} else if (arg.IsInt()) {
|
||||
}
|
||||
if (arg.IsInt()) {
|
||||
// TODO: This is making a pointless copy of std::string, we may want to
|
||||
// use a different conversion to string
|
||||
return TypedValue(std::to_string(arg.ValueInt()), ctx.memory);
|
||||
} else if (arg.IsDouble()) {
|
||||
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
|
||||
} else {
|
||||
return TypedValue(arg.ValueBool() ? "true" : "false", ctx.memory);
|
||||
}
|
||||
if (arg.IsDouble()) {
|
||||
return TypedValue(std::to_string(arg.ValueDouble()), ctx.memory);
|
||||
}
|
||||
if (arg.IsDate()) {
|
||||
return TypedValue(arg.ValueDate().ToString(), ctx.memory);
|
||||
}
|
||||
if (arg.IsLocalTime()) {
|
||||
return TypedValue(arg.ValueLocalTime().ToString(), ctx.memory);
|
||||
}
|
||||
if (arg.IsLocalDateTime()) {
|
||||
return TypedValue(arg.ValueLocalDateTime().ToString(), ctx.memory);
|
||||
}
|
||||
if (arg.IsDuration()) {
|
||||
return TypedValue(arg.ValueDuration().ToString(), ctx.memory);
|
||||
}
|
||||
|
||||
return TypedValue(arg.ValueBool() ? "true" : "false", ctx.memory);
|
||||
}
|
||||
|
||||
TypedValue Timestamp(const TypedValue *args, int64_t nargs, const FunctionContext &ctx) {
|
||||
|
||||
@@ -111,7 +111,7 @@ class ExpressionEvaluator : public ExpressionVisitor<TypedValue> {
|
||||
TypedValue Visit(IfOperator &if_operator) override {
|
||||
auto condition = if_operator.condition_->Accept(*this);
|
||||
if (condition.IsNull()) {
|
||||
return if_operator.then_expression_->Accept(*this);
|
||||
return if_operator.else_expression_->Accept(*this);
|
||||
}
|
||||
if (condition.type() != TypedValue::Type::Bool) {
|
||||
// At the moment IfOperator is used only in CASE construct.
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include "query/trigger.hpp"
|
||||
#include "query/typed_value.hpp"
|
||||
#include "storage/v2/property_value.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
#include "utils/algorithm.hpp"
|
||||
#include "utils/csv_parsing.hpp"
|
||||
#include "utils/event_counter.hpp"
|
||||
@@ -160,7 +161,7 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
void RegisterReplica(const std::string &name, const std::string &socket_address,
|
||||
const ReplicationQuery::SyncMode sync_mode, const std::optional<double> timeout,
|
||||
const ReplicationQuery::SyncMode sync_mode,
|
||||
const std::chrono::seconds replica_check_frequency) override {
|
||||
if (db_->GetReplicationRole() == storage::ReplicationRole::REPLICA) {
|
||||
// replica can't register another replica
|
||||
@@ -183,9 +184,9 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
|
||||
io::network::Endpoint::ParseSocketOrIpAddress(socket_address, query::kDefaultReplicationPort);
|
||||
if (maybe_ip_and_port) {
|
||||
auto [ip, port] = *maybe_ip_and_port;
|
||||
auto ret = db_->RegisterReplica(
|
||||
name, {std::move(ip), port}, repl_mode,
|
||||
{.timeout = timeout, .replica_check_frequency = replica_check_frequency, .ssl = std::nullopt});
|
||||
auto ret = db_->RegisterReplica(name, {std::move(ip), port}, repl_mode,
|
||||
storage::replication::RegistrationMode::MUST_BE_INSTANTLY_VALID,
|
||||
{.replica_check_frequency = replica_check_frequency, .ssl = std::nullopt});
|
||||
if (ret.HasError()) {
|
||||
throw QueryRuntimeException(fmt::format("Couldn't register replica '{}'!", name));
|
||||
}
|
||||
@@ -228,9 +229,6 @@ class ReplQueryHandler final : public query::ReplicationQueryHandler {
|
||||
replica.sync_mode = ReplicationQuery::SyncMode::ASYNC;
|
||||
break;
|
||||
}
|
||||
if (repl_info.timeout) {
|
||||
replica.timeout = *repl_info.timeout;
|
||||
}
|
||||
|
||||
replica.current_timestamp_of_replica = repl_info.timestamp_info.current_timestamp_of_replica;
|
||||
replica.current_number_of_timestamp_behind_master =
|
||||
@@ -282,8 +280,6 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
std::string rolename = auth_query->role_;
|
||||
std::string user_or_role = auth_query->user_or_role_;
|
||||
std::vector<AuthQuery::Privilege> privileges = auth_query->privileges_;
|
||||
std::vector<std::string> labels = auth_query->labels_;
|
||||
// std::vector<storage::LabelId> labels = NamesToLabels(labels, db_accessor);
|
||||
auto password = EvaluateOptionalExpression(auth_query->password_, &evaluator);
|
||||
|
||||
Callback callback;
|
||||
@@ -298,8 +294,7 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
|
||||
if (license_check_result.HasError() && enterprise_only_methods.contains(auth_query->action_)) {
|
||||
throw utils::BasicException(
|
||||
utils::license::LicenseCheckErrorToString(license_check_result.GetError(), "advanced authentication
|
||||
features"));
|
||||
utils::license::LicenseCheckErrorToString(license_check_result.GetError(), "advanced authentication features"));
|
||||
}
|
||||
|
||||
switch (auth_query->action_) {
|
||||
@@ -314,7 +309,7 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
// If the license is not valid we create users with admin access
|
||||
if (!valid_enterprise_license) {
|
||||
spdlog::warn("Granting all the privileges to {}.", username);
|
||||
auth->GrantPrivilege(username, kPrivilegesAll, {});
|
||||
auth->GrantPrivilege(username, kPrivilegesAll);
|
||||
}
|
||||
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
@@ -389,20 +384,20 @@ Callback HandleAuthQuery(AuthQuery *auth_query, AuthQueryHandler *auth, const Pa
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::GRANT_PRIVILEGE:
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->GrantPrivilege(user_or_role, privileges, labels);
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->GrantPrivilege(user_or_role, privileges);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::DENY_PRIVILEGE:
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->DenyPrivilege(user_or_role, privileges, labels);
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->DenyPrivilege(user_or_role, privileges);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
case AuthQuery::Action::REVOKE_PRIVILEGE: {
|
||||
callback.fn = [auth, user_or_role, privileges, labels] {
|
||||
auth->RevokePrivilege(user_or_role, privileges, labels);
|
||||
callback.fn = [auth, user_or_role, privileges] {
|
||||
auth->RevokePrivilege(user_or_role, privileges);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
return callback;
|
||||
@@ -490,22 +485,11 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
const auto &name = repl_query->replica_name_;
|
||||
const auto &sync_mode = repl_query->sync_mode_;
|
||||
auto socket_address = repl_query->socket_address_->Accept(evaluator);
|
||||
auto timeout = EvaluateOptionalExpression(repl_query->timeout_, &evaluator);
|
||||
const auto replica_check_frequency = interpreter_context->config.replication_replica_check_frequency;
|
||||
std::optional<double> maybe_timeout;
|
||||
if (timeout.IsDouble()) {
|
||||
maybe_timeout = timeout.ValueDouble();
|
||||
} else if (timeout.IsInt()) {
|
||||
maybe_timeout = static_cast<double>(timeout.ValueInt());
|
||||
}
|
||||
if (maybe_timeout && *maybe_timeout <= 0.0) {
|
||||
throw utils::BasicException("Parameter TIMEOUT must be strictly greater than 0.");
|
||||
}
|
||||
|
||||
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, name, socket_address, sync_mode,
|
||||
maybe_timeout, replica_check_frequency]() mutable {
|
||||
handler.RegisterReplica(name, std::string(socket_address.ValueString()), sync_mode, maybe_timeout,
|
||||
replica_check_frequency);
|
||||
replica_check_frequency]() mutable {
|
||||
handler.RegisterReplica(name, std::string(socket_address.ValueString()), sync_mode, replica_check_frequency);
|
||||
return std::vector<std::vector<TypedValue>>();
|
||||
};
|
||||
notifications->emplace_back(SeverityLevel::INFO, NotificationCode::REGISTER_REPLICA,
|
||||
@@ -525,13 +509,9 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
}
|
||||
|
||||
case ReplicationQuery::Action::SHOW_REPLICAS: {
|
||||
callback.header = {"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state"};
|
||||
callback.header = {
|
||||
"name", "socket_address", "sync_mode", "current_timestamp_of_replica", "number_of_timestamp_behind_master",
|
||||
"state"};
|
||||
callback.fn = [handler = ReplQueryHandler{interpreter_context->db}, replica_nfields = callback.header.size()] {
|
||||
const auto &replicas = handler.ShowReplicas();
|
||||
auto typed_replicas = std::vector<std::vector<TypedValue>>{};
|
||||
@@ -552,12 +532,6 @@ Callback HandleReplicationQuery(ReplicationQuery *repl_query, const Parameters &
|
||||
break;
|
||||
}
|
||||
|
||||
if (replica.timeout) {
|
||||
typed_replica.emplace_back(TypedValue(*replica.timeout));
|
||||
} else {
|
||||
typed_replica.emplace_back(TypedValue());
|
||||
}
|
||||
|
||||
typed_replica.emplace_back(TypedValue(static_cast<int64_t>(replica.current_timestamp_of_replica)));
|
||||
typed_replica.emplace_back(
|
||||
TypedValue(static_cast<int64_t>(replica.current_number_of_timestamp_behind_master)));
|
||||
|
||||
@@ -99,16 +99,14 @@ class AuthQueryHandler {
|
||||
virtual std::vector<std::vector<TypedValue>> GetPrivileges(const std::string &user_or_role) = 0;
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
virtual void GrantPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) = 0;
|
||||
virtual void GrantPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0;
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) = 0;
|
||||
virtual void DenyPrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges) = 0;
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
virtual void RevokePrivilege(const std::string &user_or_role, const std::vector<AuthQuery::Privilege> &privileges,
|
||||
const std::vector<std::string> &labels) = 0;
|
||||
virtual void RevokePrivilege(const std::string &user_or_role,
|
||||
const std::vector<AuthQuery::Privilege> &privileges) = 0;
|
||||
};
|
||||
|
||||
enum class QueryHandlerResult { COMMIT, ABORT, NOTHING };
|
||||
@@ -142,7 +140,7 @@ class ReplicationQueryHandler {
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
virtual void RegisterReplica(const std::string &name, const std::string &socket_address,
|
||||
const ReplicationQuery::SyncMode sync_mode, const std::optional<double> timeout,
|
||||
ReplicationQuery::SyncMode sync_mode,
|
||||
const std::chrono::seconds replica_check_frequency) = 0;
|
||||
|
||||
/// @throw QueryRuntimeException if an error ocurred.
|
||||
|
||||
@@ -619,10 +619,10 @@ void Streams::Drop(const std::string &stream_name) {
|
||||
// no running Test function for this consumer, therefore it can be erased.
|
||||
std::visit([&](const auto &stream_data) { stream_data.stream_source->Lock(); }, it->second);
|
||||
|
||||
locked_streams->erase(it);
|
||||
if (!storage_.Delete(stream_name)) {
|
||||
throw StreamsException("Couldn't delete stream '{}' from persistent store!", stream_name);
|
||||
}
|
||||
locked_streams->erase(it);
|
||||
|
||||
// TODO(antaljanosbenjamin) Release the transformation
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ class Streams final {
|
||||
void Persist(StreamStatus<TStream> &&status) {
|
||||
const std::string stream_name = status.name;
|
||||
if (!storage_.Put(stream_name, nlohmann::json(std::move(status)).dump())) {
|
||||
throw StreamsException{"Couldn't persist steam data for stream '{}'", stream_name};
|
||||
throw StreamsException{"Couldn't persist stream data for stream '{}'", stream_name};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ set(storage_v2_src_files
|
||||
storage.cpp)
|
||||
|
||||
##### Replication #####
|
||||
|
||||
define_add_lcp(add_lcp_storage lcp_storage_cpp_files generated_lcp_storage_files)
|
||||
|
||||
add_lcp_storage(replication/rpc.lcp SLK_SERIALIZE)
|
||||
@@ -26,10 +25,10 @@ set(storage_v2_src_files
|
||||
replication/replication_server.cpp
|
||||
replication/serialization.cpp
|
||||
replication/slk.cpp
|
||||
replication/replication_persistence_helper.cpp
|
||||
${lcp_storage_cpp_files})
|
||||
|
||||
#######################
|
||||
|
||||
find_package(gflags REQUIRED)
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ struct Config {
|
||||
uint64_t wal_file_flush_every_n_tx{100000};
|
||||
|
||||
bool snapshot_on_exit{false};
|
||||
|
||||
bool restore_replicas_on_startup{false};
|
||||
} durability;
|
||||
|
||||
struct Transaction {
|
||||
|
||||
@@ -22,6 +22,7 @@ static const std::string kSnapshotDirectory{"snapshots"};
|
||||
static const std::string kWalDirectory{"wal"};
|
||||
static const std::string kBackupDirectory{".backup"};
|
||||
static const std::string kLockFile{".lock"};
|
||||
static const std::string kReplicationDirectory{"replication"};
|
||||
|
||||
// This is the prefix used for Snapshot and WAL filenames. It is a timestamp
|
||||
// format that equals to: YYYYmmddHHMMSSffffff
|
||||
|
||||
@@ -10,12 +10,13 @@
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace memgraph::storage::replication {
|
||||
struct ReplicationClientConfig {
|
||||
std::optional<double> timeout;
|
||||
// The default delay between main checking/pinging replicas is 1s because
|
||||
// that seems like a reasonable timeframe in which main should notice a
|
||||
// replica is down.
|
||||
@@ -24,6 +25,8 @@ struct ReplicationClientConfig {
|
||||
struct SSL {
|
||||
std::string key_file = "";
|
||||
std::string cert_file = "";
|
||||
|
||||
friend bool operator==(const SSL &, const SSL &) = default;
|
||||
};
|
||||
|
||||
std::optional<SSL> ssl;
|
||||
|
||||
@@ -16,4 +16,6 @@ namespace memgraph::storage::replication {
|
||||
enum class ReplicationMode : std::uint8_t { SYNC, ASYNC };
|
||||
|
||||
enum class ReplicaState : std::uint8_t { READY, REPLICATING, RECOVERY, INVALID };
|
||||
|
||||
enum class RegistrationMode : std::uint8_t { MUST_BE_INSTANTLY_VALID, CAN_BE_INVALID };
|
||||
} // namespace memgraph::storage::replication
|
||||
|
||||
@@ -43,12 +43,6 @@ Storage::ReplicationClient::ReplicationClient(std::string name, Storage *storage
|
||||
rpc_client_.emplace(endpoint, &*rpc_context_);
|
||||
TryInitializeClientSync();
|
||||
|
||||
if (config.timeout && replica_state_ != replication::ReplicaState::INVALID) {
|
||||
MG_ASSERT(*config.timeout > 0);
|
||||
timeout_.emplace(*config.timeout);
|
||||
timeout_dispatcher_.emplace();
|
||||
}
|
||||
|
||||
// Help the user to get the most accurate replica state possible.
|
||||
if (config.replica_check_frequency > std::chrono::seconds(0)) {
|
||||
replica_checker_.Run("Replica Checker", config.replica_check_frequency, [&] { FrequentCheck(); });
|
||||
@@ -239,41 +233,6 @@ void Storage::ReplicationClient::FinalizeTransactionReplication() {
|
||||
|
||||
if (mode_ == replication::ReplicationMode::ASYNC) {
|
||||
thread_pool_.AddTask([this] { this->FinalizeTransactionReplicationInternal(); });
|
||||
} else if (timeout_) {
|
||||
MG_ASSERT(mode_ == replication::ReplicationMode::SYNC, "Only SYNC replica can have a timeout.");
|
||||
MG_ASSERT(timeout_dispatcher_, "Timeout thread is missing");
|
||||
timeout_dispatcher_->WaitForTaskToFinish();
|
||||
|
||||
timeout_dispatcher_->active = true;
|
||||
thread_pool_.AddTask([&, this] {
|
||||
this->FinalizeTransactionReplicationInternal();
|
||||
std::unique_lock main_guard(timeout_dispatcher_->main_lock);
|
||||
// TimerThread can finish waiting for timeout
|
||||
timeout_dispatcher_->active = false;
|
||||
// Notify the main thread
|
||||
timeout_dispatcher_->main_cv.notify_one();
|
||||
});
|
||||
|
||||
timeout_dispatcher_->StartTimeoutTask(*timeout_);
|
||||
|
||||
// Wait until one of the threads notifies us that they finished executing
|
||||
// Both threads should first set the active flag to false
|
||||
{
|
||||
std::unique_lock main_guard(timeout_dispatcher_->main_lock);
|
||||
timeout_dispatcher_->main_cv.wait(main_guard, [&] { return !timeout_dispatcher_->active.load(); });
|
||||
}
|
||||
|
||||
// TODO (antonio2368): Document and/or polish SEMI-SYNC to ASYNC fallback.
|
||||
if (replica_state_ == replication::ReplicaState::REPLICATING) {
|
||||
mode_ = replication::ReplicationMode::ASYNC;
|
||||
timeout_.reset();
|
||||
// This can only happen if we timeouted so we are sure that
|
||||
// Timeout task finished
|
||||
// We need to delete timeout dispatcher AFTER the replication
|
||||
// finished because it tries to acquire the timeout lock
|
||||
// and acces the `active` variable`
|
||||
thread_pool_.AddTask([this] { timeout_dispatcher_.reset(); });
|
||||
}
|
||||
} else {
|
||||
FinalizeTransactionReplicationInternal();
|
||||
}
|
||||
@@ -566,30 +525,6 @@ Storage::TimestampInfo Storage::ReplicationClient::GetTimestampInfo() {
|
||||
return info;
|
||||
}
|
||||
|
||||
////// TimeoutDispatcher //////
|
||||
void Storage::ReplicationClient::TimeoutDispatcher::WaitForTaskToFinish() {
|
||||
// Wait for the previous timeout task to finish
|
||||
std::unique_lock main_guard(main_lock);
|
||||
main_cv.wait(main_guard, [&] { return finished; });
|
||||
}
|
||||
|
||||
void Storage::ReplicationClient::TimeoutDispatcher::StartTimeoutTask(const double timeout) {
|
||||
timeout_pool.AddTask([timeout, this] {
|
||||
finished = false;
|
||||
using std::chrono::steady_clock;
|
||||
const auto timeout_duration =
|
||||
std::chrono::duration_cast<steady_clock::duration>(std::chrono::duration<double>(timeout));
|
||||
const auto end_time = steady_clock::now() + timeout_duration;
|
||||
while (active && (steady_clock::now() < end_time)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
std::unique_lock main_guard(main_lock);
|
||||
finished = true;
|
||||
active = false;
|
||||
main_cv.notify_one();
|
||||
});
|
||||
}
|
||||
////// ReplicaStream //////
|
||||
Storage::ReplicationClient::ReplicaStream::ReplicaStream(ReplicationClient *self,
|
||||
const uint64_t previous_commit_timestamp,
|
||||
|
||||
@@ -120,8 +120,6 @@ class Storage::ReplicationClient {
|
||||
|
||||
auto Mode() const { return mode_; }
|
||||
|
||||
auto Timeout() const { return timeout_; }
|
||||
|
||||
const auto &Endpoint() const { return rpc_client_->Endpoint(); }
|
||||
|
||||
Storage::TimestampInfo GetTimestampInfo();
|
||||
@@ -158,30 +156,6 @@ class Storage::ReplicationClient {
|
||||
std::optional<ReplicaStream> replica_stream_;
|
||||
replication::ReplicationMode mode_{replication::ReplicationMode::SYNC};
|
||||
|
||||
// Dispatcher class for timeout tasks
|
||||
struct TimeoutDispatcher {
|
||||
explicit TimeoutDispatcher(){};
|
||||
|
||||
void WaitForTaskToFinish();
|
||||
|
||||
void StartTimeoutTask(double timeout);
|
||||
|
||||
// If the Timeout task should continue waiting
|
||||
std::atomic<bool> active{false};
|
||||
|
||||
std::mutex main_lock;
|
||||
std::condition_variable main_cv;
|
||||
|
||||
private:
|
||||
// if the Timeout task finished executing
|
||||
bool finished{true};
|
||||
|
||||
utils::ThreadPool timeout_pool{1};
|
||||
};
|
||||
|
||||
std::optional<double> timeout_;
|
||||
std::optional<TimeoutDispatcher> timeout_dispatcher_;
|
||||
|
||||
utils::SpinLock client_lock_;
|
||||
// This thread pool is used for background tasks so we don't
|
||||
// block the main storage thread
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#include "storage/v2/replication/replication_persistence_helper.hpp"
|
||||
#include "utils/logging.hpp"
|
||||
|
||||
namespace {
|
||||
const std::string kReplicaName = "replica_name";
|
||||
const std::string kIpAddress = "replica_ip_address";
|
||||
const std::string kPort = "replica_port";
|
||||
const std::string kSyncMode = "replica_sync_mode";
|
||||
const std::string kCheckFrequency = "replica_check_frequency";
|
||||
const std::string kSSLKeyFile = "replica_ssl_key_file";
|
||||
const std::string kSSLCertFile = "replica_ssl_cert_file";
|
||||
} // namespace
|
||||
|
||||
namespace memgraph::storage::replication {
|
||||
|
||||
nlohmann::json ReplicaStatusToJSON(ReplicaStatus &&status) {
|
||||
auto data = nlohmann::json::object();
|
||||
|
||||
data[kReplicaName] = std::move(status.name);
|
||||
data[kIpAddress] = std::move(status.ip_address);
|
||||
data[kPort] = status.port;
|
||||
data[kSyncMode] = status.sync_mode;
|
||||
|
||||
data[kCheckFrequency] = status.replica_check_frequency.count();
|
||||
|
||||
if (status.ssl.has_value()) {
|
||||
data[kSSLKeyFile] = std::move(status.ssl->key_file);
|
||||
data[kSSLCertFile] = std::move(status.ssl->cert_file);
|
||||
} else {
|
||||
data[kSSLKeyFile] = nullptr;
|
||||
data[kSSLCertFile] = nullptr;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
std::optional<ReplicaStatus> JSONToReplicaStatus(nlohmann::json &&data) {
|
||||
ReplicaStatus replica_status;
|
||||
|
||||
const auto get_failed_message = [](const std::string_view message, const std::string_view nested_message) {
|
||||
return fmt::format("Failed to deserialize replica's configuration: {} : {}", message, nested_message);
|
||||
};
|
||||
|
||||
try {
|
||||
data.at(kReplicaName).get_to(replica_status.name);
|
||||
data.at(kIpAddress).get_to(replica_status.ip_address);
|
||||
data.at(kPort).get_to(replica_status.port);
|
||||
data.at(kSyncMode).get_to(replica_status.sync_mode);
|
||||
|
||||
replica_status.replica_check_frequency = std::chrono::seconds(data.at(kCheckFrequency));
|
||||
|
||||
const auto &key_file = data.at(kSSLKeyFile);
|
||||
const auto &cert_file = data.at(kSSLCertFile);
|
||||
|
||||
MG_ASSERT(key_file.is_null() == cert_file.is_null());
|
||||
|
||||
if (!key_file.is_null()) {
|
||||
replica_status.ssl = replication::ReplicationClientConfig::SSL{};
|
||||
data.at(kSSLKeyFile).get_to(replica_status.ssl->key_file);
|
||||
data.at(kSSLCertFile).get_to(replica_status.ssl->cert_file);
|
||||
}
|
||||
} catch (const nlohmann::json::type_error &exception) {
|
||||
spdlog::error(get_failed_message("Invalid type conversion", exception.what()));
|
||||
return std::nullopt;
|
||||
} catch (const nlohmann::json::out_of_range &exception) {
|
||||
spdlog::error(get_failed_message("Non existing field", exception.what()));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return replica_status;
|
||||
}
|
||||
} // namespace memgraph::storage::replication
|
||||
@@ -0,0 +1,40 @@
|
||||
// Copyright 2022 Memgraph Ltd.
|
||||
//
|
||||
// Use of this software is governed by the Business Source License
|
||||
// included in the file licenses/BSL.txt; by using this file, you agree to be bound by the terms of the Business Source
|
||||
// License, and you may not use this file except in compliance with the Business Source License.
|
||||
//
|
||||
// As of the Change Date specified in that file, in accordance with
|
||||
// the Business Source License, use of this software will be governed
|
||||
// by the Apache License, Version 2.0, included in the file
|
||||
// licenses/APL.txt.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <compare>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <json/json.hpp>
|
||||
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
|
||||
namespace memgraph::storage::replication {
|
||||
|
||||
struct ReplicaStatus {
|
||||
std::string name;
|
||||
std::string ip_address;
|
||||
uint16_t port;
|
||||
ReplicationMode sync_mode;
|
||||
std::chrono::seconds replica_check_frequency;
|
||||
std::optional<ReplicationClientConfig::SSL> ssl;
|
||||
|
||||
friend bool operator==(const ReplicaStatus &, const ReplicaStatus &) = default;
|
||||
};
|
||||
|
||||
nlohmann::json ReplicaStatusToJSON(ReplicaStatus &&status);
|
||||
|
||||
std::optional<ReplicaStatus> JSONToReplicaStatus(nlohmann::json &&data);
|
||||
} // namespace memgraph::storage::replication
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <variant>
|
||||
|
||||
#include <gflags/gflags.h>
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
#include "io/network/endpoint.hpp"
|
||||
#include "storage/v2/durability/durability.hpp"
|
||||
@@ -28,6 +29,8 @@
|
||||
#include "storage/v2/indices.hpp"
|
||||
#include "storage/v2/mvcc.hpp"
|
||||
#include "storage/v2/replication/config.hpp"
|
||||
#include "storage/v2/replication/enums.hpp"
|
||||
#include "storage/v2/replication/replication_persistence_helper.hpp"
|
||||
#include "storage/v2/transaction.hpp"
|
||||
#include "storage/v2/vertex_accessor.hpp"
|
||||
#include "utils/file.hpp"
|
||||
@@ -50,6 +53,19 @@ using OOMExceptionEnabler = utils::MemoryTracker::OutOfMemoryExceptionEnabler;
|
||||
|
||||
namespace {
|
||||
inline constexpr uint16_t kEpochHistoryRetention = 1000;
|
||||
|
||||
std::string RegisterReplicaErrorToString(Storage::RegisterReplicaError error) {
|
||||
switch (error) {
|
||||
case Storage::RegisterReplicaError::NAME_EXISTS:
|
||||
return "NAME_EXISTS";
|
||||
case Storage::RegisterReplicaError::END_POINT_EXISTS:
|
||||
return "END_POINT_EXISTS";
|
||||
case Storage::RegisterReplicaError::CONNECTION_FAILED:
|
||||
return "CONNECTION_FAILED";
|
||||
case Storage::RegisterReplicaError::COULD_NOT_BE_PERSISTED:
|
||||
return "COULD_NOT_BE_PERSISTED";
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
auto AdvanceToVisibleVertex(utils::SkipList<Vertex>::Iterator it, utils::SkipList<Vertex>::Iterator end,
|
||||
@@ -400,6 +416,16 @@ Storage::Storage(Config config)
|
||||
} else {
|
||||
commit_log_.emplace(timestamp_);
|
||||
}
|
||||
|
||||
if (config_.durability.restore_replicas_on_startup) {
|
||||
spdlog::info("Replica's configuration will be stored and will be automatically restored in case of a crash.");
|
||||
utils::EnsureDirOrDie(config_.durability.storage_directory / durability::kReplicationDirectory);
|
||||
storage_ =
|
||||
std::make_unique<kvstore::KVStore>(config_.durability.storage_directory / durability::kReplicationDirectory);
|
||||
RestoreReplicas();
|
||||
} else {
|
||||
spdlog::warn("Replicas' configuration will NOT be stored. When the server restarts, replicas will be forgotten.");
|
||||
}
|
||||
}
|
||||
|
||||
Storage::~Storage() {
|
||||
@@ -1882,7 +1908,7 @@ bool Storage::SetMainReplicationRole() {
|
||||
|
||||
utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
|
||||
std::string name, io::network::Endpoint endpoint, const replication::ReplicationMode replication_mode,
|
||||
const replication::ReplicationClientConfig &config) {
|
||||
const replication::RegistrationMode registration_mode, const replication::ReplicationClientConfig &config) {
|
||||
MG_ASSERT(replication_role_.load() == ReplicationRole::MAIN, "Only main instance can register a replica!");
|
||||
|
||||
const bool name_exists = replication_clients_.WithLock([&](auto &clients) {
|
||||
@@ -1902,12 +1928,28 @@ utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
|
||||
return RegisterReplicaError::END_POINT_EXISTS;
|
||||
}
|
||||
|
||||
MG_ASSERT(replication_mode == replication::ReplicationMode::SYNC || !config.timeout,
|
||||
"Only SYNC mode can have a timeout set");
|
||||
if (ShouldStoreAndRestoreReplicas()) {
|
||||
auto data = replication::ReplicaStatusToJSON(
|
||||
replication::ReplicaStatus{.name = name,
|
||||
.ip_address = endpoint.address,
|
||||
.port = endpoint.port,
|
||||
.sync_mode = replication_mode,
|
||||
.replica_check_frequency = config.replica_check_frequency,
|
||||
.ssl = config.ssl});
|
||||
if (!storage_->Put(name, data.dump())) {
|
||||
spdlog::error("Error when saving replica {} in settings.", name);
|
||||
return RegisterReplicaError::COULD_NOT_BE_PERSISTED;
|
||||
}
|
||||
}
|
||||
|
||||
auto client = std::make_unique<ReplicationClient>(std::move(name), this, endpoint, replication_mode, config);
|
||||
|
||||
if (client->State() == replication::ReplicaState::INVALID) {
|
||||
return RegisterReplicaError::CONNECTION_FAILED;
|
||||
if (replication::RegistrationMode::CAN_BE_INVALID != registration_mode) {
|
||||
return RegisterReplicaError::CONNECTION_FAILED;
|
||||
}
|
||||
|
||||
spdlog::warn("Connection failed when registering replica {}. Replica will still be registered.", client->Name());
|
||||
}
|
||||
|
||||
return replication_clients_.WithLock([&](auto &clients) -> utils::BasicResult<Storage::RegisterReplicaError> {
|
||||
@@ -1928,8 +1970,15 @@ utils::BasicResult<Storage::RegisterReplicaError> Storage::RegisterReplica(
|
||||
});
|
||||
}
|
||||
|
||||
bool Storage::UnregisterReplica(const std::string_view name) {
|
||||
bool Storage::UnregisterReplica(const std::string &name) {
|
||||
MG_ASSERT(replication_role_.load() == ReplicationRole::MAIN, "Only main instance can unregister a replica!");
|
||||
if (ShouldStoreAndRestoreReplicas()) {
|
||||
if (!storage_->Delete(name)) {
|
||||
spdlog::error("Error when removing replica {} from settings.", name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return replication_clients_.WithLock([&](auto &clients) {
|
||||
return std::erase_if(clients, [&](const auto &client) { return client->Name() == name; });
|
||||
});
|
||||
@@ -1952,11 +2001,10 @@ std::vector<Storage::ReplicaInfo> Storage::ReplicasInfo() {
|
||||
return replication_clients_.WithLock([](auto &clients) {
|
||||
std::vector<Storage::ReplicaInfo> replica_info;
|
||||
replica_info.reserve(clients.size());
|
||||
std::transform(clients.begin(), clients.end(), std::back_inserter(replica_info),
|
||||
[](const auto &client) -> ReplicaInfo {
|
||||
return {client->Name(), client->Mode(), client->Timeout(),
|
||||
client->Endpoint(), client->State(), client->GetTimestampInfo()};
|
||||
});
|
||||
std::transform(
|
||||
clients.begin(), clients.end(), std::back_inserter(replica_info), [](const auto &client) -> ReplicaInfo {
|
||||
return {client->Name(), client->Mode(), client->Endpoint(), client->State(), client->GetTimestampInfo()};
|
||||
});
|
||||
return replica_info;
|
||||
});
|
||||
}
|
||||
@@ -1966,4 +2014,41 @@ void Storage::SetIsolationLevel(IsolationLevel isolation_level) {
|
||||
isolation_level_ = isolation_level;
|
||||
}
|
||||
|
||||
void Storage::RestoreReplicas() {
|
||||
MG_ASSERT(memgraph::storage::ReplicationRole::MAIN == GetReplicationRole());
|
||||
if (!ShouldStoreAndRestoreReplicas()) {
|
||||
return;
|
||||
}
|
||||
spdlog::info("Restoring replicas.");
|
||||
|
||||
for (const auto &[replica_name, replica_data] : *storage_) {
|
||||
spdlog::info("Restoring replica {}.", replica_name);
|
||||
|
||||
const auto maybe_replica_status = replication::JSONToReplicaStatus(nlohmann::json::parse(replica_data));
|
||||
if (!maybe_replica_status.has_value()) {
|
||||
LOG_FATAL("Cannot parse previously saved configuration of replica {}.", replica_name);
|
||||
}
|
||||
|
||||
auto replica_status = *maybe_replica_status;
|
||||
MG_ASSERT(replica_status.name == replica_name, "Expected replica name is '{}', but got '{}'", replica_status.name,
|
||||
replica_name);
|
||||
|
||||
auto ret =
|
||||
RegisterReplica(std::move(replica_status.name), {std::move(replica_status.ip_address), replica_status.port},
|
||||
replica_status.sync_mode, replication::RegistrationMode::CAN_BE_INVALID,
|
||||
{
|
||||
.replica_check_frequency = replica_status.replica_check_frequency,
|
||||
.ssl = replica_status.ssl,
|
||||
});
|
||||
|
||||
if (ret.HasError()) {
|
||||
MG_ASSERT(RegisterReplicaError::CONNECTION_FAILED != ret.GetError());
|
||||
LOG_FATAL("Failure when restoring replica {}: {}.", replica_name, RegisterReplicaErrorToString(ret.GetError()));
|
||||
}
|
||||
spdlog::info("Replica {} restored.", replica_name);
|
||||
}
|
||||
}
|
||||
|
||||
bool Storage::ShouldStoreAndRestoreReplicas() const { return nullptr != storage_; }
|
||||
|
||||
} // namespace memgraph::storage
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include <variant>
|
||||
|
||||
#include "io/network/endpoint.hpp"
|
||||
#include "kvstore/kvstore.hpp"
|
||||
#include "storage/v2/commit_log.hpp"
|
||||
#include "storage/v2/config.hpp"
|
||||
#include "storage/v2/constraints.hpp"
|
||||
@@ -411,15 +412,20 @@ class Storage final {
|
||||
|
||||
bool SetMainReplicationRole();
|
||||
|
||||
enum class RegisterReplicaError : uint8_t { NAME_EXISTS, END_POINT_EXISTS, CONNECTION_FAILED };
|
||||
enum class RegisterReplicaError : uint8_t {
|
||||
NAME_EXISTS,
|
||||
END_POINT_EXISTS,
|
||||
CONNECTION_FAILED,
|
||||
COULD_NOT_BE_PERSISTED
|
||||
};
|
||||
|
||||
/// @pre The instance should have a MAIN role
|
||||
/// @pre Timeout can only be set for SYNC replication
|
||||
utils::BasicResult<RegisterReplicaError, void> RegisterReplica(
|
||||
std::string name, io::network::Endpoint endpoint, replication::ReplicationMode replication_mode,
|
||||
const replication::ReplicationClientConfig &config = {});
|
||||
replication::RegistrationMode registration_mode, const replication::ReplicationClientConfig &config = {});
|
||||
/// @pre The instance should have a MAIN role
|
||||
bool UnregisterReplica(std::string_view name);
|
||||
bool UnregisterReplica(const std::string &name);
|
||||
|
||||
std::optional<replication::ReplicaState> GetReplicaState(std::string_view name);
|
||||
|
||||
@@ -433,7 +439,6 @@ class Storage final {
|
||||
struct ReplicaInfo {
|
||||
std::string name;
|
||||
replication::ReplicationMode mode;
|
||||
std::optional<double> timeout;
|
||||
io::network::Endpoint endpoint;
|
||||
replication::ReplicaState state;
|
||||
TimestampInfo timestamp_info;
|
||||
@@ -475,6 +480,10 @@ class Storage final {
|
||||
|
||||
uint64_t CommitTimestamp(std::optional<uint64_t> desired_commit_timestamp = {});
|
||||
|
||||
void RestoreReplicas();
|
||||
|
||||
bool ShouldStoreAndRestoreReplicas() const;
|
||||
|
||||
// Main storage lock.
|
||||
//
|
||||
// Accessors take a shared lock when starting, so it is possible to block
|
||||
@@ -535,6 +544,7 @@ class Storage final {
|
||||
std::filesystem::path wal_directory_;
|
||||
std::filesystem::path lock_file_path_;
|
||||
utils::OutputFile lock_file_handle_;
|
||||
std::unique_ptr<kvstore::KVStore> storage_;
|
||||
|
||||
utils::Scheduler snapshot_runner_;
|
||||
utils::SpinLock snapshot_lock_;
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#include <chrono>
|
||||
#include <ctime>
|
||||
#include <limits>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
#include "utils/exceptions.hpp"
|
||||
@@ -175,6 +176,10 @@ int64_t Date::MicrosecondsSinceEpoch() const {
|
||||
|
||||
int64_t Date::DaysSinceEpoch() const { return utils::DaysSinceEpoch(year, month, day).count(); }
|
||||
|
||||
std::string Date::ToString() const {
|
||||
return fmt::format("{:0>4}-{:0>2}-{:0>2}", year, static_cast<int>(month), static_cast<int>(day));
|
||||
}
|
||||
|
||||
size_t DateHash::operator()(const Date &date) const {
|
||||
utils::HashCombine<uint64_t, uint64_t> hasher;
|
||||
size_t result = hasher(0, date.year);
|
||||
@@ -377,6 +382,15 @@ int64_t LocalTime::NanosecondsSinceEpoch() const {
|
||||
return chrono::duration_cast<chrono::nanoseconds>(SumLocalTimeParts()).count();
|
||||
}
|
||||
|
||||
std::string LocalTime::ToString() const {
|
||||
using milli = std::chrono::milliseconds;
|
||||
using micro = std::chrono::microseconds;
|
||||
const auto subseconds = milli(millisecond) + micro(microsecond);
|
||||
|
||||
return fmt::format("{:0>2}:{:0>2}:{:0>2}.{:0>6}", static_cast<int>(hour), static_cast<int>(minute),
|
||||
static_cast<int>(second), subseconds.count());
|
||||
}
|
||||
|
||||
size_t LocalTimeHash::operator()(const LocalTime &local_time) const {
|
||||
utils::HashCombine<uint64_t, uint64_t> hasher;
|
||||
size_t result = hasher(0, local_time.hour);
|
||||
@@ -486,6 +500,8 @@ int64_t LocalDateTime::SubSecondsAsNanoseconds() const {
|
||||
return (milli_as_nanos + micros_as_nanos).count();
|
||||
}
|
||||
|
||||
std::string LocalDateTime::ToString() const { return date.ToString() + 'T' + local_time.ToString(); }
|
||||
|
||||
LocalDateTime::LocalDateTime(const DateParameters &date_parameters, const LocalTimeParameters &local_time_parameters)
|
||||
: date(date_parameters), local_time(local_time_parameters) {}
|
||||
|
||||
@@ -699,6 +715,23 @@ int64_t Duration::SubSecondsAsNanoseconds() const {
|
||||
return chrono::duration_cast<chrono::nanoseconds>(micros - secs).count();
|
||||
}
|
||||
|
||||
std::string Duration::ToString() const {
|
||||
// Format [nD]T[nH]:[nM]:[nS].
|
||||
namespace chrono = std::chrono;
|
||||
auto micros = chrono::microseconds(microseconds);
|
||||
const auto dd = GetAndSubtractDuration<chrono::days>(micros);
|
||||
const auto h = GetAndSubtractDuration<chrono::hours>(micros);
|
||||
const auto m = GetAndSubtractDuration<chrono::minutes>(micros);
|
||||
const auto s = GetAndSubtractDuration<chrono::seconds>(micros);
|
||||
|
||||
auto first_half = fmt::format("P{}DT{}H{}M", dd, h, m);
|
||||
auto second_half = fmt::format("{}.{:0>6}S", s, std::abs(micros.count()));
|
||||
if (s == 0 && micros.count() < 0) {
|
||||
return first_half + '-' + second_half;
|
||||
}
|
||||
return first_half + second_half;
|
||||
}
|
||||
|
||||
Duration Duration::operator-() const {
|
||||
if (microseconds == std::numeric_limits<decltype(microseconds)>::min()) [[unlikely]] {
|
||||
throw temporal::InvalidArgumentException("Duration arithmetic overflows");
|
||||
|
||||
@@ -87,20 +87,9 @@ struct Duration {
|
||||
int64_t SubDaysAsNanoseconds() const;
|
||||
int64_t SubSecondsAsNanoseconds() const;
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, const Duration &dur) {
|
||||
// Format [nD]T[nH]:[nM]:[nS].
|
||||
namespace chrono = std::chrono;
|
||||
auto micros = chrono::microseconds(dur.microseconds);
|
||||
const auto dd = GetAndSubtractDuration<chrono::days>(micros);
|
||||
const auto h = GetAndSubtractDuration<chrono::hours>(micros);
|
||||
const auto m = GetAndSubtractDuration<chrono::minutes>(micros);
|
||||
const auto s = GetAndSubtractDuration<chrono::seconds>(micros);
|
||||
os << fmt::format("P{}DT{}H{}M", dd, h, m);
|
||||
if (s == 0 && micros.count() < 0) {
|
||||
os << '-';
|
||||
}
|
||||
return os << fmt::format("{}.{:0>6}S", s, std::abs(micros.count()));
|
||||
}
|
||||
std::string ToString() const;
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, const Duration &dur) { return os << dur.ToString(); }
|
||||
|
||||
Duration operator-() const;
|
||||
|
||||
@@ -155,13 +144,11 @@ struct Date {
|
||||
explicit Date(int64_t microseconds);
|
||||
explicit Date(const DateParameters &date_parameters);
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, const Date &date) {
|
||||
return os << fmt::format("{:0>2}-{:0>2}-{:0>2}", date.year, static_cast<int>(date.month),
|
||||
static_cast<int>(date.day));
|
||||
}
|
||||
friend std::ostream &operator<<(std::ostream &os, const Date &date) { return os << date.ToString(); }
|
||||
|
||||
int64_t MicrosecondsSinceEpoch() const;
|
||||
int64_t DaysSinceEpoch() const;
|
||||
std::string ToString() const;
|
||||
|
||||
friend Date operator+(const Date &date, const Duration &dur) {
|
||||
namespace chrono = std::chrono;
|
||||
@@ -217,17 +204,11 @@ struct LocalTime {
|
||||
// Epoch means the start of the day, i,e, midnight
|
||||
int64_t MicrosecondsSinceEpoch() const;
|
||||
int64_t NanosecondsSinceEpoch() const;
|
||||
std::string ToString() const;
|
||||
|
||||
auto operator<=>(const LocalTime &) const = default;
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, const LocalTime <) {
|
||||
namespace chrono = std::chrono;
|
||||
using milli = chrono::milliseconds;
|
||||
using micro = chrono::microseconds;
|
||||
const auto subseconds = milli(lt.millisecond) + micro(lt.microsecond);
|
||||
return os << fmt::format("{:0>2}:{:0>2}:{:0>2}.{:0>6}", static_cast<int>(lt.hour), static_cast<int>(lt.minute),
|
||||
static_cast<int>(lt.second), subseconds.count());
|
||||
}
|
||||
friend std::ostream &operator<<(std::ostream &os, const LocalTime <) { return os << lt.ToString(); }
|
||||
|
||||
friend LocalTime operator+(const LocalTime &local_time, const Duration &dur) {
|
||||
namespace chrono = std::chrono;
|
||||
@@ -279,13 +260,11 @@ struct LocalDateTime {
|
||||
int64_t MicrosecondsSinceEpoch() const;
|
||||
int64_t SecondsSinceEpoch() const; // seconds since epoch
|
||||
int64_t SubSecondsAsNanoseconds() const;
|
||||
std::string ToString() const;
|
||||
|
||||
auto operator<=>(const LocalDateTime &) const = default;
|
||||
|
||||
friend std::ostream &operator<<(std::ostream &os, const LocalDateTime &ldt) {
|
||||
os << ldt.date << 'T' << ldt.local_time;
|
||||
return os;
|
||||
}
|
||||
friend std::ostream &operator<<(std::ostream &os, const LocalDateTime &ldt) { return os << ldt.ToString(); }
|
||||
|
||||
friend LocalDateTime operator+(const LocalDateTime &dt, const Duration &dur) {
|
||||
const auto local_date_time_as_duration = Duration(dt.MicrosecondsSinceEpoch());
|
||||
|
||||
@@ -15,31 +15,33 @@
|
||||
import sys
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
|
||||
driver = GraphDatabase.driver('bolt://localhost:7687',
|
||||
auth=basic_auth('', ''),
|
||||
encrypted=False)
|
||||
session = driver.session()
|
||||
|
||||
session.run("MATCH (n) DETACH DELETE n").consume()
|
||||
print("Database cleared.")
|
||||
session.run('MATCH (n) DETACH DELETE n').consume()
|
||||
print('Database cleared.')
|
||||
|
||||
session.run('CREATE (alice:Person {name: "Alice", age: 22})').consume()
|
||||
print("Record created.")
|
||||
print('Record created.')
|
||||
|
||||
node = session.run("MATCH (n) RETURN n").single()["n"]
|
||||
print("Record matched.")
|
||||
node = session.run('MATCH (n) RETURN n').single()['n']
|
||||
print('Record matched.')
|
||||
|
||||
label = list(node.labels)[0]
|
||||
name = node["name"]
|
||||
age = node["age"]
|
||||
name = node['name']
|
||||
age = node['age']
|
||||
|
||||
if label != "Person" or name != "Alice" or age != 22:
|
||||
print("Data does not match")
|
||||
if label != 'Person' or name != 'Alice' or age != 22:
|
||||
print('Data does not match')
|
||||
sys.exit(1)
|
||||
|
||||
print("Label: %s" % label)
|
||||
print("name: %s" % name)
|
||||
print("age: %s" % age)
|
||||
print('Label: %s' % label)
|
||||
print('name: %s' % name)
|
||||
print('age: %s' % age)
|
||||
|
||||
session.close()
|
||||
driver.close()
|
||||
|
||||
print("All ok!")
|
||||
print('All ok!')
|
||||
|
||||
@@ -14,7 +14,9 @@
|
||||
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False)
|
||||
driver = GraphDatabase.driver("bolt://localhost:7687",
|
||||
auth=basic_auth("", ""),
|
||||
encrypted=False)
|
||||
|
||||
query_template = 'CREATE (n {name:"%s"})'
|
||||
template_size = len(query_template) - 2 # because of %s
|
||||
@@ -24,11 +26,10 @@ max_len = 1000000
|
||||
# binary search because we have to find the maximum size (in number of chars)
|
||||
# of a query that can be executed via driver
|
||||
while True:
|
||||
assert min_len > 0 and max_len > 0, (
|
||||
"The lengths have to be positive values! If this happens something"
|
||||
" is terrible wrong with min & max lengths OR the database"
|
||||
assert min_len > 0 and max_len > 0, \
|
||||
"The lengths have to be positive values! If this happens something" \
|
||||
" is terrible wrong with min & max lengths OR the database" \
|
||||
" isn't available."
|
||||
)
|
||||
property_size = (max_len + min_len) // 2
|
||||
try:
|
||||
driver.session().run(query_template % ("a" * property_size)).consume()
|
||||
@@ -41,7 +42,8 @@ while True:
|
||||
|
||||
assert property_size == max_len, "max_len probably has to be increased!"
|
||||
|
||||
print("\nThe max length of a query from Python driver is: %s\n" % (template_size + property_size))
|
||||
print("\nThe max length of a query from Python driver is: %s\n" %
|
||||
(template_size + property_size))
|
||||
|
||||
# sessions are not closed bacause all sessions that are
|
||||
# executed with wrong query size might be broken
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
from neo4j import GraphDatabase, basic_auth
|
||||
from neo4j.exceptions import ClientError, TransientError
|
||||
|
||||
|
||||
def tx_error(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
print(a[0])
|
||||
@@ -23,19 +22,17 @@ def tx_error(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_good(tx, name, name2):
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name).value()
|
||||
print(a[0])
|
||||
a = tx.run("CREATE (a:Person {name: $name}) RETURN a", name=name2).value()
|
||||
print(a[0])
|
||||
|
||||
|
||||
def tx_too_long(tx):
|
||||
tx.run("MATCH (a), (b), (c), (d), (e), (f) RETURN COUNT(*) AS cnt")
|
||||
|
||||
|
||||
with GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""), encrypted=False) as driver:
|
||||
with GraphDatabase.driver("bolt://localhost:7687", auth=basic_auth("", ""),
|
||||
encrypted=False) as driver:
|
||||
|
||||
def add_person(f, name, name2):
|
||||
with driver.session() as session:
|
||||
|
||||
@@ -36,6 +36,7 @@ import os
|
||||
import subprocess
|
||||
from argparse import ArgumentParser
|
||||
from pathlib import Path
|
||||
import tempfile
|
||||
import time
|
||||
import sys
|
||||
from inspect import signature
|
||||
@@ -67,7 +68,7 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
|
||||
"log_file": "main.log",
|
||||
"setup_queries": [
|
||||
"REGISTER REPLICA replica1 SYNC TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
|
||||
"REGISTER REPLICA replica2 SYNC TO '127.0.0.1:10002'",
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -103,7 +104,7 @@ def is_port_in_use(port: int) -> bool:
|
||||
return s.connect_ex(("localhost", port)) == 0
|
||||
|
||||
|
||||
def _start_instance(name, args, log_file, queries, use_ssl, procdir):
|
||||
def _start_instance(name, args, log_file, queries, use_ssl, procdir, data_directory):
|
||||
assert (
|
||||
name not in MEMGRAPH_INSTANCES.keys()
|
||||
), "If this raises, you are trying to start an instance with the same name than one already running."
|
||||
@@ -113,7 +114,8 @@ def _start_instance(name, args, log_file, queries, use_ssl, procdir):
|
||||
mg_instance = MemgraphInstanceRunner(MEMGRAPH_BINARY, use_ssl)
|
||||
MEMGRAPH_INSTANCES[name] = mg_instance
|
||||
log_file_path = os.path.join(BUILD_DIR, "logs", log_file)
|
||||
binary_args = args + ["--log-file", log_file_path]
|
||||
data_directory_path = os.path.join(BUILD_DIR, data_directory)
|
||||
binary_args = args + ["--log-file", log_file_path] + ["--data-directory", data_directory_path]
|
||||
|
||||
if len(procdir) != 0:
|
||||
binary_args.append("--query-modules-directory=" + procdir)
|
||||
@@ -175,8 +177,13 @@ def start_instance(context, name, procdir):
|
||||
if "ssl" in value:
|
||||
use_ssl = bool(value["ssl"])
|
||||
value.pop("ssl")
|
||||
data_directory = ""
|
||||
if "data_directory" in value:
|
||||
data_directory = value["data_directory"]
|
||||
else:
|
||||
data_directory = tempfile.TemporaryDirectory().name
|
||||
|
||||
instance = _start_instance(name, args, log_file, queries, use_ssl, procdir)
|
||||
instance = _start_instance(name, args, log_file, queries, use_ssl, procdir, data_directory)
|
||||
mg_instances[name] = instance
|
||||
|
||||
assert len(mg_instances) == 1
|
||||
|
||||
@@ -106,7 +106,6 @@ def test_try_to_write(connection, function_type):
|
||||
f"MATCH (n) RETURN {function_type}_write.try_to_write(n, 'property', 1);",
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("function_type", ["py", "c"])
|
||||
def test_case_sensitivity(connection, function_type):
|
||||
cursor = connection.cursor()
|
||||
|
||||
@@ -76,11 +76,8 @@ class MemgraphInstanceRunner:
|
||||
self.stop()
|
||||
self.args = copy.deepcopy(args)
|
||||
self.args = [replace_paths(arg) for arg in self.args]
|
||||
self.data_directory = tempfile.TemporaryDirectory()
|
||||
args_mg = [
|
||||
self.binary_path,
|
||||
"--data-directory",
|
||||
self.data_directory.name,
|
||||
"--storage-wal-enabled",
|
||||
"--storage-snapshot-interval-sec",
|
||||
"300",
|
||||
|
||||
16
tests/e2e/mg_utils.py
Normal file
16
tests/e2e/mg_utils.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import time
|
||||
|
||||
|
||||
def mg_sleep_and_assert(expected_value, function_to_retrieve_data, max_duration=20, time_between_attempt=0.05):
|
||||
result = function_to_retrieve_data()
|
||||
start_time = time.time()
|
||||
while result != expected_value:
|
||||
current_time = time.time()
|
||||
duration = current_time - start_time
|
||||
if duration > max_duration:
|
||||
assert False, " mg_sleep_and_assert has tried for too long and did not get the expected result!"
|
||||
|
||||
time.sleep(time_between_attempt)
|
||||
result = function_to_retrieve_data()
|
||||
|
||||
return result
|
||||
@@ -5,7 +5,7 @@ monitoring_port: &monitoring_port "7444"
|
||||
template_cluster: &template_cluster
|
||||
cluster:
|
||||
monitoring:
|
||||
args: ["--bolt-port=7687", "--log-level=TRACE", "--"]
|
||||
args: ["--bolt-port=7687", "--log-level=TRACE"]
|
||||
log_file: "monitoring-websocket-e2e.log"
|
||||
template_cluster_ssl: &template_cluster_ssl
|
||||
cluster:
|
||||
@@ -21,7 +21,6 @@ template_cluster_ssl: &template_cluster_ssl
|
||||
*cert_file,
|
||||
"--bolt-key-file",
|
||||
*key_file,
|
||||
"--",
|
||||
]
|
||||
log_file: "monitoring-websocket-ssl-e2e.log"
|
||||
ssl: true
|
||||
|
||||
@@ -12,3 +12,4 @@ copy_e2e_python_files(replication_show show.py)
|
||||
copy_e2e_python_files(replication_show show_while_creating_invalid_state.py)
|
||||
copy_e2e_python_files_from_parent_folder(replication_show ".." memgraph.py)
|
||||
copy_e2e_python_files_from_parent_folder(replication_show ".." interactive_mg_runner.py)
|
||||
copy_e2e_python_files_from_parent_folder(replication_show ".." mg_utils.py)
|
||||
|
||||
@@ -15,6 +15,7 @@ import pytest
|
||||
import time
|
||||
|
||||
from common import execute_and_fetch_all
|
||||
from mg_utils import mg_sleep_and_assert
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -36,20 +37,19 @@ def test_show_replicas(connection):
|
||||
"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state",
|
||||
}
|
||||
actual_column_names = {x.name for x in cursor.description}
|
||||
assert expected_column_names == actual_column_names
|
||||
assert actual_column_names == expected_column_names
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
assert actual_data == expected_data
|
||||
|
||||
|
||||
def test_show_replicas_while_inserting_data(connection):
|
||||
@@ -68,43 +68,43 @@ def test_show_replicas_while_inserting_data(connection):
|
||||
"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state",
|
||||
}
|
||||
actual_column_names = {x.name for x in cursor.description}
|
||||
assert expected_column_names == actual_column_names
|
||||
assert actual_column_names == expected_column_names
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
assert actual_data == expected_data
|
||||
|
||||
# 1/
|
||||
execute_and_fetch_all(cursor, "CREATE (n1:Number {name: 'forty_two', value:42});")
|
||||
time.sleep(1)
|
||||
|
||||
# 2/
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 4, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 4, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 4, 0, "ready"),
|
||||
("replica_1", "127.0.0.1:10001", "sync", 4, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 4, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 4, 0, "ready"),
|
||||
}
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
print("actual_data=" + str(actual_data))
|
||||
print("expected_data=" + str(expected_data))
|
||||
assert expected_data == actual_data
|
||||
|
||||
def retrieve_data():
|
||||
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
actual_data = mg_sleep_and_assert(expected_data, retrieve_data)
|
||||
assert actual_data == expected_data
|
||||
|
||||
# 3/
|
||||
res = execute_and_fetch_all(cursor, "MATCH (node) return node;")
|
||||
assert 1 == len(res)
|
||||
assert len(res) == 1
|
||||
|
||||
# 4/
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
assert expected_data == actual_data
|
||||
assert actual_data == expected_data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -13,11 +13,12 @@ import sys
|
||||
|
||||
import os
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from common import execute_and_fetch_all
|
||||
from mg_utils import mg_sleep_and_assert
|
||||
import interactive_mg_runner
|
||||
import mgclient
|
||||
import tempfile
|
||||
|
||||
interactive_mg_runner.SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
interactive_mg_runner.PROJECT_DIR = os.path.normpath(
|
||||
@@ -51,8 +52,8 @@ MEMGRAPH_INSTANCES_DESCRIPTION = {
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
|
||||
"log_file": "main.log",
|
||||
"setup_queries": [
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001';",
|
||||
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002';",
|
||||
"REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001';",
|
||||
"REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';",
|
||||
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003';",
|
||||
"REGISTER REPLICA replica_4 ASYNC TO '127.0.0.1:10004';",
|
||||
],
|
||||
@@ -78,32 +79,31 @@ def test_show_replicas(connection):
|
||||
"name",
|
||||
"socket_address",
|
||||
"sync_mode",
|
||||
"timeout",
|
||||
"current_timestamp_of_replica",
|
||||
"number_of_timestamp_behind_master",
|
||||
"state",
|
||||
}
|
||||
|
||||
actual_column_names = {x.name for x in cursor.description}
|
||||
assert EXPECTED_COLUMN_NAMES == actual_column_names
|
||||
assert actual_column_names == EXPECTED_COLUMN_NAMES
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2, 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 1.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "ready"),
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
assert actual_data == expected_data
|
||||
|
||||
# 2/
|
||||
execute_and_fetch_all(cursor, "DROP REPLICA replica_2")
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "ready"),
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 0, 0, "ready"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
assert actual_data == expected_data
|
||||
|
||||
# 3/
|
||||
interactive_mg_runner.kill(MEMGRAPH_INSTANCES_DESCRIPTION, "replica_1")
|
||||
@@ -111,53 +111,296 @@ def test_show_replicas(connection):
|
||||
interactive_mg_runner.stop(MEMGRAPH_INSTANCES_DESCRIPTION, "replica_4")
|
||||
|
||||
# We leave some time for the main to realise the replicas are down.
|
||||
time.sleep(2)
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
def retrieve_data():
|
||||
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2.0, 0, 0, "invalid"),
|
||||
("replica_3", "127.0.0.1:10003", "async", None, 0, 0, "invalid"),
|
||||
("replica_4", "127.0.0.1:10004", "async", None, 0, 0, "invalid"),
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "invalid"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 0, 0, "invalid"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 0, 0, "invalid"),
|
||||
}
|
||||
assert expected_data == actual_data
|
||||
actual_data = mg_sleep_and_assert(expected_data, retrieve_data)
|
||||
assert actual_data == expected_data
|
||||
|
||||
|
||||
def test_add_replica_invalid_timeout(connection):
|
||||
# Goal of this test is to check the registration of replica with invalid timeout raises an exception
|
||||
def test_basic_recovery(connection):
|
||||
# Goal of this test is to check the recovery of main.
|
||||
# 0/ We start all replicas manually: we want to be able to kill them ourselves without relying on external tooling to kill processes.
|
||||
# 1/ We check that all replicas have the correct state: they should all be ready.
|
||||
# 2/ We kill main.
|
||||
# 3/ We re-start main.
|
||||
# 4/ We check that all replicas have the correct state: they should all be ready.
|
||||
# 5/ Drop one replica.
|
||||
# 6/ We add some data to main, then kill it and restart.
|
||||
# 7/ We check that all replicas but one have the expected data.
|
||||
# 8/ We kill another replica.
|
||||
# 9/ We add some data to main.
|
||||
# 10/ We re-add the two replicas droped/killed and check the data.
|
||||
# 11/ We kill another replica.
|
||||
# 12/ Add some more data to main.
|
||||
# 13/ Check the states of replicas.
|
||||
|
||||
# 0/
|
||||
data_directory = tempfile.TemporaryDirectory()
|
||||
CONFIGURATION = {
|
||||
"replica_1": {
|
||||
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
|
||||
"log_file": "replica1.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"],
|
||||
},
|
||||
"replica_2": {
|
||||
"args": ["--bolt-port", "7689", "--log-level=TRACE"],
|
||||
"log_file": "replica2.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
|
||||
},
|
||||
"replica_3": {
|
||||
"args": ["--bolt-port", "7690", "--log-level=TRACE"],
|
||||
"log_file": "replica3.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10003;"],
|
||||
},
|
||||
"replica_4": {
|
||||
"args": ["--bolt-port", "7691", "--log-level=TRACE"],
|
||||
"log_file": "replica4.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10004;"],
|
||||
},
|
||||
"main": {
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
|
||||
"log_file": "main.log",
|
||||
"setup_queries": [],
|
||||
"data_directory": f"{data_directory.name}",
|
||||
},
|
||||
}
|
||||
|
||||
interactive_mg_runner.start_all(CONFIGURATION)
|
||||
cursor = connection(7687, "main").cursor()
|
||||
|
||||
# We want to execute manually and not via the configuration, otherwise re-starting main would also execute these registration.
|
||||
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001';")
|
||||
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';")
|
||||
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003';")
|
||||
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_4 ASYNC TO '127.0.0.1:10004';")
|
||||
|
||||
# 1/
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 0, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 0, 0, "ready"),
|
||||
}
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
assert actual_data == expected_data
|
||||
|
||||
def check_roles():
|
||||
assert "main" == interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICATION ROLE;")[0][0]
|
||||
for index in range(1, 4):
|
||||
assert (
|
||||
"replica"
|
||||
== interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query("SHOW REPLICATION ROLE;")[0][0]
|
||||
)
|
||||
|
||||
check_roles()
|
||||
|
||||
# 2/
|
||||
interactive_mg_runner.kill(CONFIGURATION, "main")
|
||||
|
||||
# 3/
|
||||
interactive_mg_runner.start(CONFIGURATION, "main")
|
||||
cursor = connection(7687, "main").cursor()
|
||||
check_roles()
|
||||
|
||||
# 4/
|
||||
def retrieve_data():
|
||||
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
actual_data = mg_sleep_and_assert(expected_data, retrieve_data)
|
||||
assert actual_data == expected_data
|
||||
|
||||
# 5/
|
||||
execute_and_fetch_all(cursor, "DROP REPLICA replica_2;")
|
||||
|
||||
# 6/
|
||||
execute_and_fetch_all(cursor, "CREATE (p1:Number {name:'Magic', value:42})")
|
||||
interactive_mg_runner.kill(CONFIGURATION, "main")
|
||||
interactive_mg_runner.start(CONFIGURATION, "main")
|
||||
cursor = connection(7687, "main").cursor()
|
||||
check_roles()
|
||||
|
||||
# 7/
|
||||
QUERY_TO_CHECK = "MATCH (node) return node;"
|
||||
res_from_main = execute_and_fetch_all(cursor, QUERY_TO_CHECK)
|
||||
assert len(res_from_main) == 1
|
||||
for index in (1, 3, 4):
|
||||
assert res_from_main == interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query(QUERY_TO_CHECK)
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 2, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 2, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 2, 0, "ready"),
|
||||
}
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
assert actual_data == expected_data
|
||||
|
||||
# Replica_2 was dropped, we check it does not have the data from main.
|
||||
assert len(interactive_mg_runner.MEMGRAPH_INSTANCES["replica_2"].query(QUERY_TO_CHECK)) == 0
|
||||
|
||||
# 8/
|
||||
interactive_mg_runner.kill(CONFIGURATION, "replica_3")
|
||||
|
||||
# 9/
|
||||
execute_and_fetch_all(cursor, "CREATE (p1:Number {name:'Magic_again', value:43})")
|
||||
res_from_main = execute_and_fetch_all(cursor, QUERY_TO_CHECK)
|
||||
assert len(res_from_main) == 2
|
||||
|
||||
# 10/
|
||||
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';")
|
||||
interactive_mg_runner.start(CONFIGURATION, "replica_3")
|
||||
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 6, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 6, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 6, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 6, 0, "ready"),
|
||||
}
|
||||
|
||||
def retrieve_data2():
|
||||
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
actual_data = mg_sleep_and_assert(expected_data, retrieve_data2)
|
||||
|
||||
assert actual_data == expected_data
|
||||
for index in (1, 2, 3, 4):
|
||||
assert interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query(QUERY_TO_CHECK) == res_from_main
|
||||
|
||||
# 11/
|
||||
interactive_mg_runner.kill(CONFIGURATION, "replica_1")
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "invalid"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 6, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 6, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 6, 0, "ready"),
|
||||
}
|
||||
|
||||
def retrieve_data3():
|
||||
return set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
|
||||
actual_data = mg_sleep_and_assert(expected_data, retrieve_data3)
|
||||
assert actual_data == expected_data
|
||||
|
||||
# 12/
|
||||
execute_and_fetch_all(cursor, "CREATE (p1:Number {name:'Magic_again_again', value:44})")
|
||||
res_from_main = execute_and_fetch_all(cursor, QUERY_TO_CHECK)
|
||||
assert len(res_from_main) == 3
|
||||
for index in (2, 3, 4):
|
||||
assert interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query(QUERY_TO_CHECK) == res_from_main
|
||||
|
||||
# 13/
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "invalid"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 9, 0, "ready"),
|
||||
("replica_3", "127.0.0.1:10003", "async", 9, 0, "ready"),
|
||||
("replica_4", "127.0.0.1:10004", "async", 9, 0, "ready"),
|
||||
}
|
||||
actual_data = set(execute_and_fetch_all(cursor, "SHOW REPLICAS;"))
|
||||
assert actual_data == expected_data
|
||||
|
||||
|
||||
def test_conflict_at_startup(connection):
|
||||
# Goal of this test is to check starting up several instance with different replicas' configuration directory works as expected.
|
||||
# main_1 and main_2 have different directory.
|
||||
|
||||
data_directory1 = tempfile.TemporaryDirectory()
|
||||
data_directory2 = tempfile.TemporaryDirectory()
|
||||
CONFIGURATION = {
|
||||
"main_1": {
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE"],
|
||||
"log_file": "main1.log",
|
||||
"setup_queries": [],
|
||||
"data_directory": f"{data_directory1.name}",
|
||||
},
|
||||
"main_2": {
|
||||
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
|
||||
"log_file": "main2.log",
|
||||
"setup_queries": [],
|
||||
"data_directory": f"{data_directory2.name}",
|
||||
},
|
||||
}
|
||||
|
||||
interactive_mg_runner.start_all(CONFIGURATION)
|
||||
cursor_1 = connection(7687, "main_1").cursor()
|
||||
cursor_2 = connection(7688, "main_2").cursor()
|
||||
|
||||
assert execute_and_fetch_all(cursor_1, "SHOW REPLICATION ROLE;")[0][0] == "main"
|
||||
assert execute_and_fetch_all(cursor_2, "SHOW REPLICATION ROLE;")[0][0] == "main"
|
||||
|
||||
|
||||
def test_basic_recovery_when_replica_is_kill_when_main_is_down(connection):
|
||||
# Goal of this test is to check the recovery of main.
|
||||
# 0/ We start all replicas manually: we want to be able to kill them ourselves without relying on external tooling to kill processes.
|
||||
# 1/ We check that all replicas have the correct state: they should all be ready.
|
||||
# 2/ We kill main then kill a replica.
|
||||
# 3/ We re-start main: it should be able to restart.
|
||||
# 4/ Check status of replica: replica_2 is invalid.
|
||||
|
||||
data_directory = tempfile.TemporaryDirectory()
|
||||
CONFIGURATION = {
|
||||
"replica_1": {
|
||||
"args": ["--bolt-port", "7688", "--log-level=TRACE"],
|
||||
"log_file": "replica1.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10001;"],
|
||||
},
|
||||
"replica_2": {
|
||||
"args": ["--bolt-port", "7689", "--log-level=TRACE"],
|
||||
"log_file": "replica2.log",
|
||||
"setup_queries": ["SET REPLICATION ROLE TO REPLICA WITH PORT 10002;"],
|
||||
},
|
||||
"main": {
|
||||
"args": ["--bolt-port", "7687", "--log-level=TRACE", "--storage-recover-on-startup=true"],
|
||||
"log_file": "main.log",
|
||||
"setup_queries": [],
|
||||
"data_directory": f"{data_directory.name}",
|
||||
},
|
||||
}
|
||||
|
||||
interactive_mg_runner.start_all(CONFIGURATION)
|
||||
|
||||
cursor = connection(7687, "main").cursor()
|
||||
# We want to execute manually and not via the configuration, otherwise re-starting main would also execute these registration.
|
||||
interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001';")
|
||||
interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002';")
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 0 TO '127.0.0.1:10001';",
|
||||
)
|
||||
# 1/
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "ready"),
|
||||
}
|
||||
actual_data = set(interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICAS;"))
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT -5 TO '127.0.0.1:10001';",
|
||||
)
|
||||
assert actual_data == expected_data
|
||||
|
||||
actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;")
|
||||
assert 0 == len(actual_data)
|
||||
def check_roles():
|
||||
assert "main" == interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICATION ROLE;")[0][0]
|
||||
for index in range(1, 2):
|
||||
assert (
|
||||
"replica"
|
||||
== interactive_mg_runner.MEMGRAPH_INSTANCES[f"replica_{index}"].query("SHOW REPLICATION ROLE;")[0][0]
|
||||
)
|
||||
|
||||
execute_and_fetch_all(cursor, "REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10001';")
|
||||
actual_data = execute_and_fetch_all(cursor, "SHOW REPLICAS;")
|
||||
assert 1 == len(actual_data)
|
||||
check_roles()
|
||||
|
||||
# 2/
|
||||
interactive_mg_runner.kill(CONFIGURATION, "main")
|
||||
interactive_mg_runner.kill(CONFIGURATION, "replica_2")
|
||||
|
||||
# 3/
|
||||
interactive_mg_runner.start(CONFIGURATION, "main")
|
||||
|
||||
# 4/
|
||||
expected_data = {
|
||||
("replica_1", "127.0.0.1:10001", "sync", 0, 0, "ready"),
|
||||
("replica_2", "127.0.0.1:10002", "sync", 0, 0, "invalid"),
|
||||
}
|
||||
actual_data = set(interactive_mg_runner.MEMGRAPH_INSTANCES["main"].query("SHOW REPLICAS;"))
|
||||
assert actual_data == expected_data
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -29,8 +29,8 @@ template_cluster: &template_cluster
|
||||
args: ["--bolt-port", "7687", "--log-level=TRACE"]
|
||||
log_file: "replication-e2e-main.log"
|
||||
setup_queries: [
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
|
||||
"REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002'",
|
||||
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003'"
|
||||
]
|
||||
<<: *template_validation_queries
|
||||
@@ -69,8 +69,8 @@ workloads:
|
||||
args: ["--bolt-port", "7687", "--log-level=TRACE"]
|
||||
log_file: "replication-e2e-main.log"
|
||||
setup_queries: [
|
||||
"REGISTER REPLICA replica_1 SYNC WITH TIMEOUT 2 TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_2 SYNC WITH TIMEOUT 1 TO '127.0.0.1:10002'",
|
||||
"REGISTER REPLICA replica_1 SYNC TO '127.0.0.1:10001'",
|
||||
"REGISTER REPLICA replica_2 SYNC TO '127.0.0.1:10002'",
|
||||
"REGISTER REPLICA replica_3 ASYNC TO '127.0.0.1:10003'"
|
||||
]
|
||||
validation_queries: []
|
||||
|
||||
@@ -4,7 +4,7 @@ bolt_port: &bolt_port "7687"
|
||||
template_cluster: &template_cluster
|
||||
cluster:
|
||||
server:
|
||||
args: ["--bolt-port=7687", "--log-level=TRACE", "--"]
|
||||
args: ["--bolt-port=7687", "--log-level=TRACE"]
|
||||
log_file: "server-connection-e2e.log"
|
||||
template_cluster_ssl: &template_cluster_ssl
|
||||
cluster:
|
||||
@@ -18,7 +18,6 @@ template_cluster_ssl: &template_cluster_ssl
|
||||
*cert_file,
|
||||
"--bolt-key-file",
|
||||
*key_file,
|
||||
"--",
|
||||
]
|
||||
log_file: "server-connection-ssl-e2e.log"
|
||||
ssl: true
|
||||
|
||||
@@ -9,3 +9,5 @@ copy_streams_e2e_python_files(streams_owner_tests.py)
|
||||
copy_streams_e2e_python_files(pulsar_streams_tests.py)
|
||||
|
||||
add_subdirectory(transformations)
|
||||
|
||||
copy_e2e_python_files_from_parent_folder(streams ".." mg_utils.py)
|
||||
|
||||
@@ -13,6 +13,7 @@ import mgclient
|
||||
import pytest
|
||||
import time
|
||||
|
||||
from mg_utils import mg_sleep_and_assert
|
||||
from multiprocessing import Manager, Process, Value
|
||||
|
||||
# These are the indices of the different values in the result of SHOW STREAM
|
||||
@@ -115,10 +116,7 @@ def start_stream(cursor, stream_name):
|
||||
|
||||
def start_stream_with_limit(cursor, stream_name, batch_limit, timeout=None):
|
||||
if timeout is not None:
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"START STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout} ",
|
||||
)
|
||||
execute_and_fetch_all(cursor, f"START STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout} ")
|
||||
else:
|
||||
execute_and_fetch_all(cursor, f"START STREAM {stream_name} BATCH_LIMIT {batch_limit}")
|
||||
|
||||
@@ -159,12 +157,7 @@ def pulsar_default_namespace_topic(topic):
|
||||
|
||||
|
||||
def test_start_and_stop_during_check(
|
||||
operation,
|
||||
connection,
|
||||
stream_creator,
|
||||
message_sender,
|
||||
already_stopped_error,
|
||||
batchSize,
|
||||
operation, connection, stream_creator, message_sender, already_stopped_error, batchSize
|
||||
):
|
||||
# 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
|
||||
@@ -325,42 +318,24 @@ def test_check_stream_same_number_of_queries_than_messages(connection, stream_cr
|
||||
|
||||
expected_queries_and_raw_messages_1 = (
|
||||
[ # queries
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 01"},
|
||||
QUERY_LITERAL: "Message: 01",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 02"},
|
||||
QUERY_LITERAL: "Message: 02",
|
||||
},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 01"}, QUERY_LITERAL: "Message: 01"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 02"}, QUERY_LITERAL: "Message: 02"},
|
||||
],
|
||||
["01", "02"], # raw message
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_2 = (
|
||||
[ # queries
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 03"},
|
||||
QUERY_LITERAL: "Message: 03",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 04"},
|
||||
QUERY_LITERAL: "Message: 04",
|
||||
},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 03"}, QUERY_LITERAL: "Message: 03"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 04"}, QUERY_LITERAL: "Message: 04"},
|
||||
],
|
||||
["03", "04"], # raw message
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_3 = (
|
||||
[ # queries
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 05"},
|
||||
QUERY_LITERAL: "Message: 05",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 06"},
|
||||
QUERY_LITERAL: "Message: 06",
|
||||
},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 05"}, QUERY_LITERAL: "Message: 05"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 06"}, QUERY_LITERAL: "Message: 06"},
|
||||
],
|
||||
["05", "06"], # raw message
|
||||
)
|
||||
@@ -415,32 +390,20 @@ def test_check_stream_different_number_of_queries_than_messages(connection, stre
|
||||
|
||||
expected_queries_and_raw_messages_2 = (
|
||||
[ # queries
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 03"},
|
||||
QUERY_LITERAL: "Message: 03",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 04"},
|
||||
QUERY_LITERAL: "Message: 04",
|
||||
},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 03"}, QUERY_LITERAL: "Message: 03"},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 04"}, QUERY_LITERAL: "Message: 04"},
|
||||
],
|
||||
["03", "04"], # raw message
|
||||
)
|
||||
|
||||
expected_queries_and_raw_messages_3 = (
|
||||
[ # queries
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: b_05"},
|
||||
QUERY_LITERAL: "Message: b_05",
|
||||
},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: b_05"}, QUERY_LITERAL: "Message: b_05"},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: extra_b_05"},
|
||||
QUERY_LITERAL: "Message: extra_b_05",
|
||||
},
|
||||
{
|
||||
PARAMETERS_LITERAL: {"value": "Parameter: 06"},
|
||||
QUERY_LITERAL: "Message: 06",
|
||||
},
|
||||
{PARAMETERS_LITERAL: {"value": "Parameter: 06"}, QUERY_LITERAL: "Message: 06"},
|
||||
],
|
||||
["b_05", "06"], # raw message
|
||||
)
|
||||
@@ -465,8 +428,10 @@ def test_start_stream_with_batch_limit(connection, stream_creator, messages_send
|
||||
thread_stream_running = Process(target=start_new_stream_with_limit, daemon=True, args=(STREAM_NAME, BATCH_LIMIT))
|
||||
thread_stream_running.start()
|
||||
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
def is_running():
|
||||
return get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
assert mg_sleep_and_assert(True, is_running)
|
||||
|
||||
messages_sender(BATCH_LIMIT - 1)
|
||||
|
||||
@@ -476,10 +441,8 @@ def test_start_stream_with_batch_limit(connection, stream_creator, messages_send
|
||||
# We send a last message to reach the batch_limit
|
||||
messages_sender(1)
|
||||
|
||||
time.sleep(2)
|
||||
|
||||
# We check that the stream has correctly stoped.
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
assert not mg_sleep_and_assert(False, is_running)
|
||||
|
||||
|
||||
def test_start_stream_with_batch_limit_timeout(connection, stream_creator):
|
||||
@@ -505,10 +468,7 @@ def test_start_stream_with_batch_limit_reaching_timeout(connection, stream_creat
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"START STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
execute_and_fetch_all(cursor, f"START STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}")
|
||||
|
||||
end_time = time.time()
|
||||
assert (
|
||||
@@ -524,10 +484,7 @@ def test_start_stream_with_batch_limit_while_check_running(
|
||||
def start_check_stream(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}",
|
||||
)
|
||||
execute_and_fetch_all(cursor, f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}")
|
||||
|
||||
def start_new_stream_with_limit(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
@@ -548,8 +505,11 @@ def test_start_stream_with_batch_limit_while_check_running(
|
||||
# 1/
|
||||
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT))
|
||||
thread_stream_check.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
def is_running():
|
||||
return get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
assert mg_sleep_and_assert(True, is_running)
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
start_stream_with_limit(cursor, STREAM_NAME, BATCH_LIMIT, timeout=TIMEOUT)
|
||||
@@ -562,18 +522,15 @@ def test_start_stream_with_batch_limit_while_check_running(
|
||||
|
||||
# 2/
|
||||
thread_stream_running = Process(
|
||||
target=start_new_stream_with_limit,
|
||||
daemon=True,
|
||||
args=(STREAM_NAME, BATCH_LIMIT + 1, TIMEOUT),
|
||||
target=start_new_stream_with_limit, daemon=True, args=(STREAM_NAME, BATCH_LIMIT + 1, TIMEOUT)
|
||||
) # Sending BATCH_LIMIT + 1 messages as BATCH_LIMIT messages have already been sent during the CHECK STREAM (and not consumed)
|
||||
thread_stream_running.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
assert mg_sleep_and_assert(True, is_running)
|
||||
|
||||
message_sender(SIMPLE_MSG)
|
||||
time.sleep(2)
|
||||
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
assert not mg_sleep_and_assert(False, is_running)
|
||||
|
||||
|
||||
def test_check_while_stream_with_batch_limit_running(connection, stream_creator, message_sender):
|
||||
@@ -587,10 +544,7 @@ def test_check_while_stream_with_batch_limit_running(connection, stream_creator,
|
||||
def start_check_stream(stream_name, batch_limit, timeout):
|
||||
connection = connect()
|
||||
cursor = connection.cursor()
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}",
|
||||
)
|
||||
execute_and_fetch_all(cursor, f"CHECK STREAM {stream_name} BATCH_LIMIT {batch_limit} TIMEOUT {timeout}")
|
||||
|
||||
STREAM_NAME = "test_batch_limit_and_check"
|
||||
BATCH_LIMIT = 1
|
||||
@@ -602,42 +556,34 @@ def test_check_while_stream_with_batch_limit_running(connection, stream_creator,
|
||||
|
||||
# 1/
|
||||
thread_stream_running = Process(
|
||||
target=start_new_stream_with_limit,
|
||||
daemon=True,
|
||||
args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT),
|
||||
target=start_new_stream_with_limit, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT)
|
||||
)
|
||||
start_time = time.time()
|
||||
thread_stream_running.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
def is_running():
|
||||
return get_is_running(cursor, STREAM_NAME)
|
||||
|
||||
assert mg_sleep_and_assert(True, is_running)
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
execute_and_fetch_all(cursor, f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {BATCH_LIMIT} TIMEOUT {TIMEOUT}")
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
message_sender(SIMPLE_MSG)
|
||||
time.sleep(2)
|
||||
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
assert not mg_sleep_and_assert(False, is_running)
|
||||
|
||||
# 2/
|
||||
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(STREAM_NAME, BATCH_LIMIT, TIMEOUT))
|
||||
start_time = time.time()
|
||||
thread_stream_check.start()
|
||||
time.sleep(2)
|
||||
assert get_is_running(cursor, STREAM_NAME)
|
||||
assert mg_sleep_and_assert(True, is_running)
|
||||
|
||||
message_sender(SIMPLE_MSG)
|
||||
time.sleep(2)
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
assert not get_is_running(cursor, STREAM_NAME)
|
||||
assert not mg_sleep_and_assert(False, is_running)
|
||||
|
||||
|
||||
def test_start_stream_with_batch_limit_with_invalid_batch_limit(connection, stream_creator):
|
||||
@@ -686,10 +632,7 @@ def test_check_stream_with_batch_limit_with_invalid_batch_limit(connection, stre
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
execute_and_fetch_all(cursor, f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}")
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
@@ -699,10 +642,7 @@ def test_check_stream_with_batch_limit_with_invalid_batch_limit(connection, stre
|
||||
start_time = time.time()
|
||||
|
||||
with pytest.raises(mgclient.DatabaseError):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}",
|
||||
)
|
||||
execute_and_fetch_all(cursor, f"CHECK STREAM {STREAM_NAME} BATCH_LIMIT {batch_limit} TIMEOUT {TIMEOUT}")
|
||||
|
||||
end_time = time.time()
|
||||
assert (end_time - start_time) < 0.8 * TIMEOUT_IN_SECONDS, "The CHECK STREAM has probably thrown due to timeout!"
|
||||
|
||||
@@ -37,22 +37,29 @@ def connection():
|
||||
|
||||
|
||||
def get_topics(num):
|
||||
return [f"topic_{i}" for i in range(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")
|
||||
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"]
|
||||
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_to_create = []
|
||||
for topic in topics:
|
||||
topics_to_create.append(NewTopic(name=topic, num_partitions=1, replication_factor=1))
|
||||
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
|
||||
@@ -73,5 +80,6 @@ def pulsar_client():
|
||||
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")
|
||||
requests.delete(
|
||||
f'http://127.0.0.1:6652/admin/v2/persistent/public/default/{topic}?force=true')
|
||||
yield topics
|
||||
|
||||
@@ -15,15 +15,13 @@ import sys
|
||||
import pytest
|
||||
import mgclient
|
||||
import time
|
||||
from mg_utils import mg_sleep_and_assert
|
||||
from multiprocessing import Process, Value
|
||||
import common
|
||||
|
||||
TRANSFORMATIONS_TO_CHECK_C = ["c_transformations.empty_transformation"]
|
||||
|
||||
TRANSFORMATIONS_TO_CHECK_PY = [
|
||||
"kafka_transform.simple",
|
||||
"kafka_transform.with_parameters",
|
||||
]
|
||||
TRANSFORMATIONS_TO_CHECK_PY = ["kafka_transform.simple", "kafka_transform.with_parameters"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("transformation", TRANSFORMATIONS_TO_CHECK_PY)
|
||||
@@ -466,14 +464,13 @@ def test_start_stream_with_batch_limit_while_check_running(kafka_producer, kafka
|
||||
kafka_producer.send(kafka_topics[0], message).get(timeout=6000)
|
||||
|
||||
def setup_function(start_check_stream, cursor, stream_name, batch_limit, timeout):
|
||||
thread_stream_check = Process(
|
||||
target=start_check_stream,
|
||||
daemon=True,
|
||||
args=(stream_name, batch_limit, timeout),
|
||||
)
|
||||
thread_stream_check = Process(target=start_check_stream, daemon=True, args=(stream_name, batch_limit, timeout))
|
||||
thread_stream_check.start()
|
||||
time.sleep(2)
|
||||
assert common.get_is_running(cursor, stream_name)
|
||||
|
||||
def is_running():
|
||||
return common.get_is_running(cursor, stream_name)
|
||||
|
||||
assert mg_sleep_and_assert(True, is_running)
|
||||
message_sender(common.SIMPLE_MSG)
|
||||
thread_stream_check.join()
|
||||
|
||||
|
||||
@@ -18,20 +18,13 @@ import time
|
||||
from multiprocessing import Process, Value
|
||||
import common
|
||||
|
||||
TRANSFORMATIONS_TO_CHECK = [
|
||||
"pulsar_transform.simple",
|
||||
"pulsar_transform.with_parameters",
|
||||
]
|
||||
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}"',
|
||||
},
|
||||
cursor, {"topic": f'"{common.pulsar_default_namespace_topic(topic)}"', "payload": f'"{decoded_payload}"'}
|
||||
)
|
||||
|
||||
|
||||
@@ -44,7 +37,6 @@ def test_simple(pulsar_client, pulsar_topics, connection, transformation):
|
||||
f"CREATE PULSAR STREAM test TOPICS '{','.join(pulsar_topics)}' TRANSFORM {transformation}",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
producer = pulsar_client.create_producer(
|
||||
@@ -73,8 +65,6 @@ def test_separate_consumers(pulsar_client, pulsar_topics, connection, transforma
|
||||
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)
|
||||
@@ -96,7 +86,6 @@ def test_start_from_latest_messages(pulsar_client, pulsar_topics, connection):
|
||||
f"CREATE PULSAR STREAM test 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(
|
||||
@@ -107,8 +96,7 @@ def test_start_from_latest_messages(pulsar_client, pulsar_topics, connection):
|
||||
assert len(vertices_with_msg) == 0
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]),
|
||||
send_timeout_millis=60000,
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000
|
||||
)
|
||||
producer.send(common.SIMPLE_MSG)
|
||||
|
||||
@@ -162,11 +150,9 @@ def test_check_stream(pulsar_client, pulsar_topics, connection, transformation):
|
||||
f"CREATE PULSAR STREAM test TOPICS {pulsar_topics[0]} TRANSFORM {transformation} BATCH_SIZE {BATCH_SIZE}",
|
||||
)
|
||||
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,
|
||||
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)
|
||||
@@ -272,8 +258,7 @@ def test_start_and_stop_during_check(pulsar_client, pulsar_topics, connection, o
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE {BATCH_SIZE}"
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]),
|
||||
send_timeout_millis=60000,
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000
|
||||
)
|
||||
|
||||
def message_sender(msg):
|
||||
@@ -318,17 +303,14 @@ def test_restart_after_error(pulsar_client, pulsar_topics, connection):
|
||||
)
|
||||
|
||||
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,
|
||||
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")
|
||||
|
||||
@@ -343,7 +325,6 @@ def test_service_url(pulsar_client, pulsar_topics, connection, transformation):
|
||||
f"CREATE PULSAR STREAM test TOPICS {','.join(pulsar_topics)} TRANSFORM {transformation} SERVICE_URL '{LOCAL}'",
|
||||
)
|
||||
common.start_stream(cursor, "test")
|
||||
time.sleep(5)
|
||||
|
||||
for topic in pulsar_topics:
|
||||
producer = pulsar_client.create_producer(
|
||||
@@ -362,8 +343,7 @@ def test_start_stream_with_batch_limit(pulsar_client, pulsar_topics, connection)
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1"
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]),
|
||||
send_timeout_millis=60000,
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000
|
||||
)
|
||||
|
||||
def messages_sender(nof_messages):
|
||||
@@ -398,8 +378,7 @@ def test_start_stream_with_batch_limit_while_check_running(pulsar_client, pulsar
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1"
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]),
|
||||
send_timeout_millis=60000,
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000
|
||||
)
|
||||
|
||||
def message_sender(message):
|
||||
@@ -415,8 +394,7 @@ def test_check_while_stream_with_batch_limit_running(pulsar_client, pulsar_topic
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM pulsar_transform.simple BATCH_SIZE 1"
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]),
|
||||
send_timeout_millis=60000,
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000
|
||||
)
|
||||
|
||||
def message_sender(message):
|
||||
@@ -434,8 +412,7 @@ def test_check_stream_same_number_of_queries_than_messages(pulsar_client, pulsar
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM {TRANSFORMATION} BATCH_INTERVAL 3000 BATCH_SIZE {batch_size} "
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]),
|
||||
send_timeout_millis=60000,
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000
|
||||
)
|
||||
|
||||
def message_sender(msg):
|
||||
@@ -453,8 +430,7 @@ def test_check_stream_different_number_of_queries_than_messages(pulsar_client, p
|
||||
return f"CREATE PULSAR STREAM {stream_name} TOPICS {pulsar_topics[0]} TRANSFORM {TRANSFORMATION} BATCH_INTERVAL 3000 BATCH_SIZE {batch_size} "
|
||||
|
||||
producer = pulsar_client.create_producer(
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]),
|
||||
send_timeout_millis=60000,
|
||||
common.pulsar_default_namespace_topic(pulsar_topics[0]), send_timeout_millis=60000
|
||||
)
|
||||
|
||||
def message_sender(msg):
|
||||
|
||||
@@ -15,7 +15,6 @@ import time
|
||||
import mgclient
|
||||
import common
|
||||
|
||||
|
||||
def get_cursor_with_user(username):
|
||||
connection = common.connect(username=username, password="")
|
||||
return connection.cursor()
|
||||
@@ -23,21 +22,23 @@ def get_cursor_with_user(username):
|
||||
|
||||
def create_admin_user(cursor, admin_user):
|
||||
common.execute_and_fetch_all(cursor, f"CREATE USER {admin_user}")
|
||||
common.execute_and_fetch_all(cursor, f"GRANT ALL PRIVILEGES TO {admin_user}")
|
||||
common.execute_and_fetch_all(
|
||||
cursor, f"GRANT ALL PRIVILEGES TO {admin_user}")
|
||||
|
||||
|
||||
def create_stream_user(cursor, stream_user):
|
||||
common.execute_and_fetch_all(cursor, f"CREATE USER {stream_user}")
|
||||
common.execute_and_fetch_all(cursor, f"GRANT STREAM TO {stream_user}")
|
||||
common.execute_and_fetch_all(
|
||||
cursor, f"GRANT STREAM TO {stream_user}")
|
||||
|
||||
|
||||
def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
assert len(kafka_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",
|
||||
)
|
||||
common.execute_and_fetch_all(userless_cursor,
|
||||
"CREATE KAFKA STREAM ownerless "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
common.start_stream(userless_cursor, "ownerless")
|
||||
time.sleep(1)
|
||||
|
||||
@@ -45,9 +46,11 @@ def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
create_admin_user(userless_cursor, admin_user)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
|
||||
assert common.timed_wait(lambda: not common.get_is_running(userless_cursor, "ownerless"))
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(userless_cursor, "ownerless"))
|
||||
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
|
||||
common.execute_and_fetch_all(userless_cursor, f"DROP USER {admin_user}")
|
||||
common.start_stream(userless_cursor, "ownerless")
|
||||
@@ -55,9 +58,11 @@ def test_ownerless_stream(kafka_producer, kafka_topics, connection):
|
||||
|
||||
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)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], second_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
def test_owner_is_shown(kafka_topics, connection):
|
||||
@@ -68,16 +73,12 @@ def test_owner_is_shown(kafka_topics, connection):
|
||||
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 KAFKA STREAM test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_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", "kafka", 100, 1000,
|
||||
"kafka_transform.simple", stream_user, False))
|
||||
|
||||
|
||||
def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
|
||||
@@ -92,10 +93,10 @@ def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
|
||||
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 insufficient_test " f"TOPICS {kafka_topics[0]} " f"TRANSFORM kafka_transform.simple",
|
||||
)
|
||||
common.execute_and_fetch_all(stream_cursor,
|
||||
"CREATE KAFKA STREAM insufficient_test "
|
||||
f"TOPICS {kafka_topics[0]} "
|
||||
f"TRANSFORM kafka_transform.simple")
|
||||
|
||||
# the stream is started by admin, but should check against the owner
|
||||
# privileges
|
||||
@@ -103,19 +104,24 @@ def test_insufficient_privileges(kafka_producer, kafka_topics, connection):
|
||||
time.sleep(1)
|
||||
|
||||
kafka_producer.send(kafka_topics[0], b"first message").get(timeout=60)
|
||||
assert common.timed_wait(lambda: not common.get_is_running(userless_cursor, "insufficient_test"))
|
||||
assert common.timed_wait(
|
||||
lambda: not common.get_is_running(userless_cursor, "insufficient_test"))
|
||||
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 0
|
||||
|
||||
common.execute_and_fetch_all(admin_cursor, f"GRANT CREATE TO {stream_user}")
|
||||
common.execute_and_fetch_all(
|
||||
admin_cursor, f"GRANT CREATE TO {stream_user}")
|
||||
common.start_stream(userless_cursor, "insufficient_test")
|
||||
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)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], second_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
def test_happy_case(kafka_producer, kafka_topics, connection):
|
||||
@@ -129,12 +135,13 @@ def test_happy_case(kafka_producer, kafka_topics, connection):
|
||||
stream_user = "stream_user"
|
||||
create_stream_user(userless_cursor, stream_user)
|
||||
stream_cursor = get_cursor_with_user(stream_user)
|
||||
common.execute_and_fetch_all(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",
|
||||
)
|
||||
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")
|
||||
|
||||
common.start_stream(stream_cursor, "insufficient_test")
|
||||
time.sleep(1)
|
||||
@@ -142,9 +149,11 @@ def test_happy_case(kafka_producer, kafka_topics, connection):
|
||||
first_message = b"first message"
|
||||
kafka_producer.send(kafka_topics[0], first_message).get(timeout=60)
|
||||
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(userless_cursor, kafka_topics[0], first_message)
|
||||
common.kafka_check_vertex_exists_with_topic_and_payload(
|
||||
userless_cursor, kafka_topics[0], first_message)
|
||||
|
||||
assert len(common.execute_and_fetch_all(userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
assert len(common.execute_and_fetch_all(
|
||||
userless_cursor, "MATCH (n) RETURN n")) == 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -23,10 +23,7 @@ def check_stream_no_filtering(
|
||||
message = messages.message_at(i)
|
||||
payload_as_str = message.payload().decode("utf-8")
|
||||
result_queries.append(
|
||||
mgp.Record(
|
||||
query=f"Message: {payload_as_str}",
|
||||
parameters={"value": f"Parameter: {payload_as_str}"},
|
||||
)
|
||||
mgp.Record(query=f"Message: {payload_as_str}", parameters={"value": f"Parameter: {payload_as_str}"})
|
||||
)
|
||||
|
||||
return result_queries
|
||||
@@ -47,17 +44,13 @@ def check_stream_with_filtering(
|
||||
continue
|
||||
|
||||
result_queries.append(
|
||||
mgp.Record(
|
||||
query=f"Message: {payload_as_str}",
|
||||
parameters={"value": f"Parameter: {payload_as_str}"},
|
||||
)
|
||||
mgp.Record(query=f"Message: {payload_as_str}", parameters={"value": f"Parameter: {payload_as_str}"})
|
||||
)
|
||||
|
||||
if "b" in payload_as_str:
|
||||
result_queries.append(
|
||||
mgp.Record(
|
||||
query=f"Message: extra_{payload_as_str}",
|
||||
parameters={"value": f"Parameter: extra_{payload_as_str}"},
|
||||
query=f"Message: extra_{payload_as_str}", parameters={"value": f"Parameter: extra_{payload_as_str}"}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -59,9 +59,7 @@ def with_parameters(context: mgp.TransCtx, messages: mgp.Messages) -> mgp.Record
|
||||
|
||||
|
||||
@mgp.transformation
|
||||
def query(
|
||||
messages: mgp.Messages,
|
||||
) -> mgp.Record(query=str, parameters=mgp.Nullable[mgp.Map]):
|
||||
def query(messages: mgp.Messages) -> mgp.Record(query=str, parameters=mgp.Nullable[mgp.Map]):
|
||||
result_queries = []
|
||||
|
||||
for i in range(0, messages.total_messages()):
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
import mgp
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def create_vertex(ctx: mgp.ProcCtx, id: mgp.Any) -> mgp.Record(v=mgp.Any):
|
||||
v = None
|
||||
@@ -37,14 +36,15 @@ def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record():
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def create_edge(
|
||||
ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, to_vertex: mgp.Vertex, edge_type: str
|
||||
) -> mgp.Record(e=mgp.Any):
|
||||
def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex,
|
||||
to_vertex: mgp.Vertex,
|
||||
edge_type: str) -> mgp.Record(e=mgp.Any):
|
||||
e = None
|
||||
try:
|
||||
e = ctx.graph.create_edge(from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
e.properties.set("id", 1)
|
||||
e.properties.set("tbd", 0)
|
||||
e = ctx.graph.create_edge(
|
||||
from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
e.properties.set("id", 1);
|
||||
e.properties.set("tbd", 0);
|
||||
except RuntimeError as ex:
|
||||
return mgp.Record(e=str(ex))
|
||||
return mgp.Record(e=e)
|
||||
@@ -61,20 +61,19 @@ def set_property(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record():
|
||||
object.properties.set("id", 2)
|
||||
return mgp.Record()
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def remove_property(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record():
|
||||
object.properties.set("tbd", None)
|
||||
return mgp.Record()
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.add_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.remove_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
@@ -13,7 +13,8 @@ import mgclient
|
||||
import typing
|
||||
|
||||
|
||||
def execute_and_fetch_all(cursor: mgclient.Cursor, query: str, params: dict = {}) -> typing.List[tuple]:
|
||||
def execute_and_fetch_all(cursor: mgclient.Cursor, query: str,
|
||||
params: dict = {}) -> typing.List[tuple]:
|
||||
cursor.execute(query, params)
|
||||
return cursor.fetchall()
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ import mgp
|
||||
|
||||
|
||||
@mgp.read_proc
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx,
|
||||
object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
return mgp.Record(mutable=object.underlying_graph_is_mutable())
|
||||
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@ def detach_delete_vertex(ctx: mgp.ProcCtx, v: mgp.Any) -> mgp.Record():
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def create_edge(
|
||||
ctx: mgp.ProcCtx, from_vertex: mgp.Vertex, to_vertex: mgp.Vertex, edge_type: str
|
||||
) -> mgp.Record(e=mgp.Any):
|
||||
def create_edge(ctx: mgp.ProcCtx, from_vertex: mgp.Vertex,
|
||||
to_vertex: mgp.Vertex,
|
||||
edge_type: str) -> mgp.Record(e=mgp.Any):
|
||||
e = None
|
||||
try:
|
||||
e = ctx.graph.create_edge(from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
e = ctx.graph.create_edge(
|
||||
from_vertex, to_vertex, mgp.EdgeType(edge_type))
|
||||
except RuntimeError as ex:
|
||||
return mgp.Record(e=str(ex))
|
||||
return mgp.Record(e=e)
|
||||
@@ -53,25 +54,29 @@ def delete_edge(ctx: mgp.ProcCtx, edge: mgp.Edge) -> mgp.Record():
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def set_property(ctx: mgp.ProcCtx, object: mgp.Any, name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
|
||||
def set_property(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str, value: mgp.Nullable[mgp.Any]) -> mgp.Record():
|
||||
object.properties.set(name, value)
|
||||
return mgp.Record()
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
def add_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.add_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any, name: str) -> mgp.Record(o=mgp.Any):
|
||||
def remove_label(ctx: mgp.ProcCtx, object: mgp.Any,
|
||||
name: str) -> mgp.Record(o=mgp.Any):
|
||||
object.remove_label(name)
|
||||
return mgp.Record(o=object)
|
||||
|
||||
|
||||
@mgp.write_proc
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx, object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
def underlying_graph_is_mutable(ctx: mgp.ProcCtx,
|
||||
object: mgp.Any) -> mgp.Record(mutable=bool):
|
||||
return mgp.Record(mutable=object.underlying_graph_is_mutable())
|
||||
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@ import typing
|
||||
import mgclient
|
||||
import sys
|
||||
import pytest
|
||||
from common import execute_and_fetch_all, has_one_result_row, has_n_result_row
|
||||
from common import (execute_and_fetch_all,
|
||||
has_one_result_row, has_n_result_row)
|
||||
|
||||
|
||||
def test_is_write(connection):
|
||||
@@ -21,19 +22,15 @@ def test_is_write(connection):
|
||||
result_order = "name, signature, is_write"
|
||||
cursor = connection.cursor()
|
||||
for proc in execute_and_fetch_all(
|
||||
cursor,
|
||||
"CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}",
|
||||
):
|
||||
cursor, "CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}"):
|
||||
assert proc[is_write] is True
|
||||
|
||||
for proc in execute_and_fetch_all(
|
||||
cursor,
|
||||
"CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE NOT name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}",
|
||||
):
|
||||
cursor, "CALL mg.procedures() YIELD * WITH name, signature, "
|
||||
"is_write WHERE NOT name STARTS WITH 'write' "
|
||||
f"RETURN {result_order}"):
|
||||
assert proc[is_write] is False
|
||||
|
||||
assert cursor.description[0].name == "name"
|
||||
@@ -44,7 +41,8 @@ def test_is_write(connection):
|
||||
def test_single_vertex(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
result = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")
|
||||
result = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")
|
||||
vertex = result[0][0]
|
||||
assert isinstance(vertex, mgclient.Node)
|
||||
assert has_one_result_row(cursor, "MATCH (n) RETURN n")
|
||||
@@ -52,13 +50,14 @@ def test_single_vertex(connection):
|
||||
assert vertex.properties == {}
|
||||
|
||||
def add_label(label: str):
|
||||
execute_and_fetch_all(cursor, f"MATCH (n) CALL write.add_label(n, '{label}') " "YIELD * RETURN *")
|
||||
execute_and_fetch_all(
|
||||
cursor, f"MATCH (n) CALL write.add_label(n, '{label}') "
|
||||
"YIELD * RETURN *")
|
||||
|
||||
def remove_label(label: str):
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"MATCH (n) CALL write.remove_label(n, '{label}') " "YIELD * RETURN *",
|
||||
)
|
||||
cursor, f"MATCH (n) CALL write.remove_label(n, '{label}') "
|
||||
"YIELD * RETURN *")
|
||||
|
||||
def get_vertex() -> mgclient.Node:
|
||||
return execute_and_fetch_all(cursor, "MATCH (n) RETURN n")[0][0]
|
||||
@@ -66,10 +65,8 @@ def test_single_vertex(connection):
|
||||
def set_property(property_name: str, property: typing.Any):
|
||||
nonlocal cursor
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"MATCH (n) CALL write.set_property(n, '{property_name}', " "$property) YIELD * RETURN *",
|
||||
{"property": property},
|
||||
)
|
||||
cursor, f"MATCH (n) CALL write.set_property(n, '{property_name}', "
|
||||
"$property) YIELD * RETURN *", {"property": property})
|
||||
|
||||
label_1 = "LABEL1"
|
||||
label_2 = "LABEL2"
|
||||
@@ -92,23 +89,24 @@ def test_single_vertex(connection):
|
||||
set_property(property_name, None)
|
||||
assert get_vertex().properties == {}
|
||||
|
||||
execute_and_fetch_all(cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
|
||||
execute_and_fetch_all(
|
||||
cursor, "MATCH (n) CALL write.delete_vertex(n) YIELD * RETURN 1")
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
|
||||
|
||||
def test_single_edge(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v1_id = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v2_id = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
edge_type = "EDGE"
|
||||
edge = execute_and_fetch_all(
|
||||
cursor,
|
||||
f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
f"MATCH (m) WHERE id(m) = {v2_id} "
|
||||
f"CALL write.create_edge(n, m, '{edge_type}') "
|
||||
"YIELD e RETURN e",
|
||||
)[0][0]
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
f"MATCH (m) WHERE id(m) = {v2_id} "
|
||||
f"CALL write.create_edge(n, m, '{edge_type}') "
|
||||
"YIELD e RETURN e")[0][0]
|
||||
|
||||
assert edge.type == edge_type
|
||||
assert edge.properties == {}
|
||||
@@ -122,10 +120,9 @@ def test_single_edge(connection):
|
||||
def set_property(property_name: str, property: typing.Any):
|
||||
nonlocal cursor
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"MATCH ()-[e]->() " f"CALL write.set_property(e, '{property_name}', " "$property) YIELD * RETURN *",
|
||||
{"property": property},
|
||||
)
|
||||
cursor, "MATCH ()-[e]->() "
|
||||
f"CALL write.set_property(e, '{property_name}', "
|
||||
"$property) YIELD * RETURN *", {"property": property})
|
||||
|
||||
set_property(property_name, property_value_1)
|
||||
assert get_edge().properties == {property_name: property_value_1}
|
||||
@@ -133,68 +130,60 @@ def test_single_edge(connection):
|
||||
assert get_edge().properties == {property_name: property_value_2}
|
||||
set_property(property_name, None)
|
||||
assert get_edge().properties == {}
|
||||
execute_and_fetch_all(cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
|
||||
execute_and_fetch_all(
|
||||
cursor, "MATCH ()-[e]->() CALL write.delete_edge(e) YIELD * RETURN 1")
|
||||
assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0)
|
||||
|
||||
|
||||
def test_detach_delete_vertex(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v1_id = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v2_id = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
f"MATCH (m) WHERE id(m) = {v2_id} "
|
||||
f"CALL write.create_edge(n, m, 'EDGE') "
|
||||
"YIELD e RETURN e",
|
||||
)
|
||||
"YIELD e RETURN e")
|
||||
|
||||
assert has_one_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m")
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"MATCH (n) WHERE id(n) = {v1_id} " "CALL write.detach_delete_vertex(n) YIELD * RETURN 1",
|
||||
)
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
"CALL write.detach_delete_vertex(n) YIELD * RETURN 1")
|
||||
assert has_n_result_row(cursor, "MATCH (n)-[e]->(m) RETURN n, e, m", 0)
|
||||
assert has_n_result_row(cursor, "MATCH ()-[e]->() RETURN e", 0)
|
||||
assert has_one_result_row(cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
|
||||
assert has_one_result_row(
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v2_id} RETURN n")
|
||||
|
||||
|
||||
def test_graph_mutability(connection):
|
||||
cursor = connection.cursor()
|
||||
assert has_n_result_row(cursor, "MATCH (n) RETURN n", 0)
|
||||
v1_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v2_id = execute_and_fetch_all(cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v1_id = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
v2_id = execute_and_fetch_all(
|
||||
cursor, "CALL write.create_vertex() YIELD v RETURN v")[0][0].id
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
cursor, f"MATCH (n) WHERE id(n) = {v1_id} "
|
||||
f"MATCH (m) WHERE id(m) = {v2_id} "
|
||||
f"CALL write.create_edge(n, m, 'EDGE') "
|
||||
"YIELD e RETURN e",
|
||||
)
|
||||
"YIELD e RETURN e")
|
||||
|
||||
def test_mutability(is_write: bool):
|
||||
module = "write" if is_write else "read"
|
||||
assert (
|
||||
execute_and_fetch_all(cursor, f"CALL {module}.graph_is_mutable() " "YIELD mutable RETURN mutable",)[
|
||||
0
|
||||
][0]
|
||||
is is_write
|
||||
)
|
||||
assert (
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"MATCH (n) " f"CALL {module}.underlying_graph_is_mutable(n) " "YIELD mutable RETURN mutable",
|
||||
)[0][0]
|
||||
is is_write
|
||||
)
|
||||
assert (
|
||||
execute_and_fetch_all(
|
||||
cursor,
|
||||
"MATCH (n)-[e]->(m) " f"CALL {module}.underlying_graph_is_mutable(e) " "YIELD mutable RETURN mutable",
|
||||
)[0][0]
|
||||
is is_write
|
||||
)
|
||||
assert execute_and_fetch_all(
|
||||
cursor, f"CALL {module}.graph_is_mutable() "
|
||||
"YIELD mutable RETURN mutable")[0][0] is is_write
|
||||
assert execute_and_fetch_all(
|
||||
cursor, "MATCH (n) "
|
||||
f"CALL {module}.underlying_graph_is_mutable(n) "
|
||||
"YIELD mutable RETURN mutable")[0][0] is is_write
|
||||
assert execute_and_fetch_all(
|
||||
cursor, "MATCH (n)-[e]->(m) "
|
||||
f"CALL {module}.underlying_graph_is_mutable(e) "
|
||||
"YIELD mutable RETURN mutable")[0][0] is is_write
|
||||
|
||||
test_mutability(True)
|
||||
test_mutability(False)
|
||||
|
||||
@@ -20,7 +20,6 @@ from neo4j import GraphDatabase, basic_auth
|
||||
|
||||
# Helper class and functions
|
||||
|
||||
|
||||
class TestResults:
|
||||
def __init__(self):
|
||||
self.total = 0
|
||||
@@ -40,16 +39,18 @@ class TestResults:
|
||||
|
||||
# Behave specific functions
|
||||
|
||||
|
||||
def before_all(context):
|
||||
# logging
|
||||
logging.basicConfig(level="DEBUG")
|
||||
context.log = logging.getLogger(__name__)
|
||||
|
||||
# driver
|
||||
uri = "bolt://{}:{}".format(context.config.db_host, context.config.db_port)
|
||||
auth_token = basic_auth(context.config.db_user, context.config.db_pass)
|
||||
context.driver = GraphDatabase.driver(uri, auth=auth_token, encrypted=False)
|
||||
uri = "bolt://{}:{}".format(context.config.db_host,
|
||||
context.config.db_port)
|
||||
auth_token = basic_auth(
|
||||
context.config.db_user, context.config.db_pass)
|
||||
context.driver = GraphDatabase.driver(uri, auth=auth_token,
|
||||
encrypted=False)
|
||||
|
||||
# test results
|
||||
context.test_results = TestResults()
|
||||
@@ -62,7 +63,8 @@ def before_scenario(context, scenario):
|
||||
|
||||
def after_scenario(context, scenario):
|
||||
context.test_results.add_test(scenario.status)
|
||||
if context.config.single_scenario or (context.config.single_fail and scenario.status == "failed"):
|
||||
if context.config.single_scenario or \
|
||||
(context.config.single_fail and scenario.status == "failed"):
|
||||
print("Press enter to continue")
|
||||
sys.stdin.readline()
|
||||
|
||||
@@ -85,5 +87,5 @@ def after_all(context):
|
||||
"test_suite": context.config.test_suite,
|
||||
}
|
||||
|
||||
with open(context.config.stats_file, "w") as f:
|
||||
with open(context.config.stats_file, 'w') as f:
|
||||
json.dump(js, f)
|
||||
|
||||
@@ -55,14 +55,22 @@ def main():
|
||||
add_config("--test-directory")
|
||||
|
||||
# Arguments that should be passed on to Behave
|
||||
add_argument("--db-host", default="127.0.0.1", help="server host (default is 127.0.0.1)")
|
||||
add_argument("--db-port", default="7687", help="server port (default is 7687)")
|
||||
add_argument("--db-user", default="memgraph", help="server user (default is memgraph)")
|
||||
add_argument("--db-pass", default="memgraph", help="server pass (default is memgraph)")
|
||||
add_argument("--stop", action="store_true", help="stop testing after first fail")
|
||||
add_argument("--single-fail", action="store_true", help="pause after failed scenario")
|
||||
add_argument("--single-scenario", action="store_true", help="pause after every scenario")
|
||||
add_argument("--single-feature", action="store_true", help="pause after every feature")
|
||||
add_argument("--db-host", default="127.0.0.1",
|
||||
help="server host (default is 127.0.0.1)")
|
||||
add_argument("--db-port", default="7687",
|
||||
help="server port (default is 7687)")
|
||||
add_argument("--db-user", default="memgraph",
|
||||
help="server user (default is memgraph)")
|
||||
add_argument("--db-pass", default="memgraph",
|
||||
help="server pass (default is memgraph)")
|
||||
add_argument("--stop", action="store_true",
|
||||
help="stop testing after first fail")
|
||||
add_argument("--single-fail", action="store_true",
|
||||
help="pause after failed scenario")
|
||||
add_argument("--single-scenario", action="store_true",
|
||||
help="pause after every scenario")
|
||||
add_argument("--single-feature", action="store_true",
|
||||
help="pause after every feature")
|
||||
add_argument("--stats-file", default="", help="statistics output file")
|
||||
|
||||
# Parse arguments
|
||||
@@ -88,5 +96,5 @@ def main():
|
||||
return behave_main(behave_args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
|
||||
@@ -15,11 +15,11 @@ from behave import given
|
||||
import graph
|
||||
|
||||
|
||||
@given("the binary-tree-1 graph")
|
||||
@given(u'the binary-tree-1 graph')
|
||||
def step_impl(context):
|
||||
graph.create_graph("binary-tree-1", context)
|
||||
graph.create_graph('binary-tree-1', context)
|
||||
|
||||
|
||||
@given("the binary-tree-2 graph")
|
||||
@given(u'the binary-tree-2 graph')
|
||||
def step_impl(context):
|
||||
graph.create_graph("binary-tree-2", context)
|
||||
graph.create_graph('binary-tree-2', context)
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
def query(q, context, params={}):
|
||||
"""
|
||||
Function used to execute query on database. Query results are
|
||||
@@ -45,7 +44,7 @@ def query(q, context, params={}):
|
||||
except Exception as e:
|
||||
# exception
|
||||
context.exception = e
|
||||
context.log.info("%s", str(e))
|
||||
context.log.info('%s', str(e))
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
|
||||
@@ -24,234 +24,234 @@ def handle_error(context):
|
||||
@param context:
|
||||
behave.runner.Context, context of behave.
|
||||
"""
|
||||
assert context.exception is not None
|
||||
assert(context.exception is not None)
|
||||
|
||||
|
||||
@then("an error should be raised")
|
||||
@then('an error should be raised')
|
||||
def error(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: NestedAggregation")
|
||||
@then('a SyntaxError should be raised at compile time: NestedAggregation')
|
||||
def syntax_error(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("TypeError should be raised at compile time: IncomparableValues")
|
||||
@then('TypeError should be raised at compile time: IncomparableValues')
|
||||
def type_error(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at compile time: IncomparableValues")
|
||||
@then(u'a TypeError should be raised at compile time: IncomparableValues')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: RequiresDirectedRelationship")
|
||||
@then(u'a SyntaxError should be raised at compile time: RequiresDirectedRelationship')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidRelationshipPattern")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidRelationshipPattern')
|
||||
def syntax_error(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at runtime: MapElementAccessByNonString")
|
||||
@then(u'a TypeError should be raised at runtime: MapElementAccessByNonString')
|
||||
def type_error(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a ConstraintVerificationFailed should be raised at runtime: DeleteConnectedNode")
|
||||
@then(u'a ConstraintVerificationFailed should be raised at runtime: DeleteConnectedNode')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at runtime: ListElementAccessByNonInteger")
|
||||
@then(u'a TypeError should be raised at runtime: ListElementAccessByNonInteger')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidArgumentType")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentType')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at runtime: InvalidElementAccess")
|
||||
@then(u'a TypeError should be raised at runtime: InvalidElementAccess')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a ArgumentError should be raised at runtime: NumberOutOfRange")
|
||||
@then(u'a ArgumentError should be raised at runtime: NumberOutOfRange')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at runtime: InvalidArgumentValue")
|
||||
@then(u'a TypeError should be raised at runtime: InvalidArgumentValue')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: VariableAlreadyBound")
|
||||
@then(u'a SyntaxError should be raised at compile time: VariableAlreadyBound')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at runtime: IncomparableValues")
|
||||
@then(u'a TypeError should be raised at runtime: IncomparableValues')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at runtime: PropertyAccessOnNonMap")
|
||||
@then(u'a TypeError should be raised at runtime: PropertyAccessOnNonMap')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidUnicodeLiteral")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidUnicodeLiteral')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SemanticError should be raised at compile time: MergeReadOwnWrites")
|
||||
@then(u'a SemanticError should be raised at compile time: MergeReadOwnWrites')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidAggregation")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidAggregation')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: NoExpressionAlias")
|
||||
@then(u'a SyntaxError should be raised at compile time: NoExpressionAlias')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: UndefinedVariable")
|
||||
@then(u'a SyntaxError should be raised at compile time: UndefinedVariable')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: VariableTypeConflict")
|
||||
@then(u'a SyntaxError should be raised at compile time: VariableTypeConflict')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: DifferentColumnsInUnion")
|
||||
@then(u'a SyntaxError should be raised at compile time: DifferentColumnsInUnion')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidClauseComposition")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidClauseComposition')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a TypeError should be raised at compile time: InvalidPropertyType")
|
||||
@then(u'a TypeError should be raised at compile time: InvalidPropertyType')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: ColumnNameConflict")
|
||||
@then(u'a SyntaxError should be raised at compile time: ColumnNameConflict')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: NoVariablesInScope")
|
||||
@then(u'a SyntaxError should be raised at compile time: NoVariablesInScope')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidDelete")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidDelete')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: NegativeIntegerArgument")
|
||||
@then(u'a SyntaxError should be raised at compile time: NegativeIntegerArgument')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a EntityNotFound should be raised at runtime: DeletedEntityAccess")
|
||||
@then(u'a EntityNotFound should be raised at runtime: DeletedEntityAccess')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: RelationshipUniquenessViolation")
|
||||
@then(u'a SyntaxError should be raised at compile time: RelationshipUniquenessViolation')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: CreatingVarLength")
|
||||
@then(u'a SyntaxError should be raised at compile time: CreatingVarLength')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidParameterUse")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidParameterUse')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: FloatingPointOverflow")
|
||||
@then(u'a SyntaxError should be raised at compile time: FloatingPointOverflow')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time InvalidArgumentExpression")
|
||||
@then(u'a SyntaxError should be raised at compile time InvalidArgumentExpression')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time InvalidUnicodeCharacter")
|
||||
@then(u'a SyntaxError should be raised at compile time InvalidUnicodeCharacter')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: NonConstantExpression")
|
||||
@then(u'a SyntaxError should be raised at compile time: NonConstantExpression')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: NoSingleRelationshipType")
|
||||
@then(u'a SyntaxError should be raised at compile time: NoSingleRelationshipType')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: UnknownFunction")
|
||||
@then(u'a SyntaxError should be raised at compile time: UnknownFunction')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidNumberLiteral")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidNumberLiteral')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidArgumentExpression")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentExpression')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidUnicodeCharacter")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidUnicodeCharacter')
|
||||
def step(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidArgumentPassingMode")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidArgumentPassingMode')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a SyntaxError should be raised at compile time: InvalidNumberOfArguments")
|
||||
@then(u'a SyntaxError should be raised at compile time: InvalidNumberOfArguments')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a ParameterMissing should be raised at compile time: MissingParameter")
|
||||
@then(u'a ParameterMissing should be raised at compile time: MissingParameter')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
|
||||
@then("a ProcedureError should be raised at compile time: ProcedureNotFound")
|
||||
@then(u'a ProcedureError should be raised at compile time: ProcedureNotFound')
|
||||
def step_impl(context):
|
||||
handle_error(context)
|
||||
|
||||
@@ -23,12 +23,12 @@ def clear_graph(context):
|
||||
database.query("MATCH (n) DETACH DELETE n", context)
|
||||
|
||||
|
||||
@given("an empty graph")
|
||||
@given('an empty graph')
|
||||
def empty_graph_step(context):
|
||||
clear_graph(context)
|
||||
|
||||
|
||||
@given("any graph")
|
||||
@given('any graph')
|
||||
def any_graph_step(context):
|
||||
clear_graph(context)
|
||||
|
||||
@@ -46,18 +46,20 @@ def create_graph(name, context):
|
||||
and sets graph properties to beginning values.
|
||||
"""
|
||||
clear_graph(context)
|
||||
path = os.path.join(context.config.test_directory, "graphs", name + ".cypher")
|
||||
path = os.path.join(context.config.test_directory, "graphs",
|
||||
name + ".cypher")
|
||||
|
||||
q_marks = ["'", '"', "`"]
|
||||
q_marks = ["'", '"', '`']
|
||||
|
||||
with open(path, "r") as f:
|
||||
content = f.read().replace("\n", " ")
|
||||
single_query = ""
|
||||
with open(path, 'r') as f:
|
||||
content = f.read().replace('\n', ' ')
|
||||
single_query = ''
|
||||
quote = None
|
||||
i = 0
|
||||
while i < len(content):
|
||||
ch = content[i]
|
||||
if ch == "\\" and i != len(content) - 1 and content[i + 1] in q_marks:
|
||||
if ch == '\\' and i != len(content) - 1 and \
|
||||
content[i + 1] in q_marks:
|
||||
single_query += ch + content[i + 1]
|
||||
i += 2
|
||||
else:
|
||||
@@ -66,9 +68,9 @@ def create_graph(name, context):
|
||||
quote = None
|
||||
elif ch in q_marks and quote is None:
|
||||
quote = ch
|
||||
if ch == ";" and quote is None:
|
||||
if ch == ';' and quote is None:
|
||||
database.query(single_query, context)
|
||||
single_query = ""
|
||||
single_query = ''
|
||||
i += 1
|
||||
if single_query.strip() != "":
|
||||
if single_query.strip() != '':
|
||||
database.query(single_query, context)
|
||||
|
||||
@@ -29,13 +29,13 @@ def parse(el, ignore_order):
|
||||
@return:
|
||||
Parsed string of element.
|
||||
"""
|
||||
if el.startswith("(") and el.endswith(")"):
|
||||
if el.startswith('(') and el.endswith(')'):
|
||||
return parse_node(el, ignore_order)
|
||||
if el.startswith("<") and el.endswith(">"):
|
||||
if el.startswith('<') and el.endswith('>'):
|
||||
return parse_path(el, ignore_order)
|
||||
if el.startswith("{") and el.endswith("}"):
|
||||
if el.startswith('{') and el.endswith('}'):
|
||||
return parse_map(el, ignore_order)
|
||||
if el.startswith("[") and el.endswith("]"):
|
||||
if el.startswith('[') and el.endswith(']'):
|
||||
if is_list(el):
|
||||
return parse_list(el, ignore_order)
|
||||
else:
|
||||
@@ -51,7 +51,7 @@ def is_list(el):
|
||||
@return:
|
||||
true if el is list.
|
||||
"""
|
||||
if el[1] == ":":
|
||||
if el[1] == ':':
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -64,20 +64,20 @@ def parse_path(path, ignore_order):
|
||||
@return:
|
||||
parsed path
|
||||
"""
|
||||
parsed_path = "<"
|
||||
parsed_path = '<'
|
||||
dif_open_closed_brackets = 0
|
||||
for i in range(1, len(path) - 1):
|
||||
if path[i] == "(" or path[i] == "{" or path[i] == "[":
|
||||
if path[i] == '(' or path[i] == '{' or path[i] == '[':
|
||||
dif_open_closed_brackets += 1
|
||||
if dif_open_closed_brackets == 1:
|
||||
start = i
|
||||
if path[i] == ")" or path[i] == "}" or path[i] == "]":
|
||||
if path[i] == ')' or path[i] == '}' or path[i] == ']':
|
||||
dif_open_closed_brackets -= 1
|
||||
if dif_open_closed_brackets == 0:
|
||||
parsed_path += parse(path[start : (i + 1)], ignore_order)
|
||||
parsed_path += parse(path[start:(i + 1)], ignore_order)
|
||||
elif dif_open_closed_brackets == 0:
|
||||
parsed_path += path[i]
|
||||
parsed_path += ">"
|
||||
parsed_path += '>'
|
||||
return parsed_path
|
||||
|
||||
|
||||
@@ -89,27 +89,28 @@ def parse_node(node_str, ignore_order):
|
||||
@return:
|
||||
parsed node
|
||||
"""
|
||||
label = ""
|
||||
label = ''
|
||||
labels = []
|
||||
props_start = None
|
||||
for i in range(1, len(node_str)):
|
||||
if node_str[i] == ":" or node_str[i] == ")" or node_str[i] == "{":
|
||||
if label.startswith(":"):
|
||||
if node_str[i] == ':' or node_str[i] == ')' or node_str[i] == '{':
|
||||
if label.startswith(':'):
|
||||
labels.append(label)
|
||||
label = ""
|
||||
label = ''
|
||||
label += node_str[i]
|
||||
|
||||
if node_str[i] == "{":
|
||||
if node_str[i] == '{':
|
||||
props_start = i
|
||||
break
|
||||
|
||||
labels.sort()
|
||||
parsed_node = "("
|
||||
parsed_node = '('
|
||||
for label in labels:
|
||||
parsed_node += label
|
||||
if props_start is not None:
|
||||
parsed_node += parse_map(node_str[props_start : len(node_str) - 1], ignore_order)
|
||||
parsed_node += ")"
|
||||
parsed_node += parse_map(
|
||||
node_str[props_start:len(node_str) - 1], ignore_order)
|
||||
parsed_node += ')'
|
||||
return parsed_node
|
||||
|
||||
|
||||
@@ -122,23 +123,23 @@ def parse_map(props, ignore_order):
|
||||
parsed map
|
||||
"""
|
||||
dif_open_closed_brackets = 0
|
||||
prop = ""
|
||||
prop = ''
|
||||
list_props = []
|
||||
for i in range(1, len(props) - 1):
|
||||
if props[i] == "," and dif_open_closed_brackets == 0:
|
||||
if props[i] == ',' and dif_open_closed_brackets == 0:
|
||||
list_props.append(prop_to_str(prop, ignore_order))
|
||||
prop = ""
|
||||
prop = ''
|
||||
else:
|
||||
prop += props[i]
|
||||
if props[i] == "(" or props[i] == "{" or props[i] == "[":
|
||||
if props[i] == '(' or props[i] == '{' or props[i] == '[':
|
||||
dif_open_closed_brackets += 1
|
||||
elif props[i] == ")" or props[i] == "}" or props[i] == "]":
|
||||
elif props[i] == ')' or props[i] == '}' or props[i] == ']':
|
||||
dif_open_closed_brackets -= 1
|
||||
if prop != "":
|
||||
if prop != '':
|
||||
list_props.append(prop_to_str(prop, ignore_order))
|
||||
|
||||
list_props.sort()
|
||||
return "{" + ",".join(list_props) + "}"
|
||||
return '{' + ','.join(list_props) + '}'
|
||||
|
||||
|
||||
def prop_to_str(prop, ignore_order):
|
||||
@@ -151,8 +152,8 @@ def prop_to_str(prop, ignore_order):
|
||||
@return:
|
||||
parsed prop
|
||||
"""
|
||||
key = prop.split(":", 1)[0]
|
||||
val = prop.split(":", 1)[1]
|
||||
key = prop.split(':', 1)[0]
|
||||
val = prop.split(':', 1)[1]
|
||||
return key + ":" + parse(val, ignore_order)
|
||||
|
||||
|
||||
@@ -165,25 +166,25 @@ def parse_list(l, ignore_order):
|
||||
parsed list
|
||||
"""
|
||||
dif_open_closed_brackets = 0
|
||||
el = ""
|
||||
el = ''
|
||||
list_el = []
|
||||
for i in range(1, len(l) - 1):
|
||||
if l[i] == "," and dif_open_closed_brackets == 0:
|
||||
if l[i] == ',' and dif_open_closed_brackets == 0:
|
||||
list_el.append(parse(el, ignore_order))
|
||||
el = ""
|
||||
el = ''
|
||||
else:
|
||||
el += l[i]
|
||||
if l[i] == "(" or l[i] == "{" or l[i] == "[":
|
||||
if l[i] == '(' or l[i] == '{' or l[i] == '[':
|
||||
dif_open_closed_brackets += 1
|
||||
elif l[i] == ")" or l[i] == "}" or l[i] == "]":
|
||||
elif l[i] == ')' or l[i] == '}' or l[i] == ']':
|
||||
dif_open_closed_brackets -= 1
|
||||
if el != "":
|
||||
if el != '':
|
||||
list_el.append(parse(el, ignore_order))
|
||||
|
||||
if ignore_order:
|
||||
list_el.sort()
|
||||
|
||||
return "[" + ",".join(list_el) + "]"
|
||||
return '[' + ','.join(list_el) + ']'
|
||||
|
||||
|
||||
def parse_rel(rel, ignore_order):
|
||||
@@ -194,25 +195,25 @@ def parse_rel(rel, ignore_order):
|
||||
@return:
|
||||
parsed relationship
|
||||
"""
|
||||
label = ""
|
||||
label = ''
|
||||
labels = []
|
||||
props_start = None
|
||||
for i in range(1, len(rel)):
|
||||
if rel[i] == ":" or rel[i] == "]" or rel[i] == "{":
|
||||
if label.startswith(":"):
|
||||
if rel[i] == ':' or rel[i] == ']' or rel[i] == '{':
|
||||
if label.startswith(':'):
|
||||
labels.append(label)
|
||||
label = ""
|
||||
label = ''
|
||||
label += rel[i]
|
||||
|
||||
if rel[i] == "{":
|
||||
if rel[i] == '{':
|
||||
props_start = i
|
||||
break
|
||||
|
||||
labels.sort()
|
||||
parsed_rel = "["
|
||||
parsed_rel = '['
|
||||
for label in labels:
|
||||
parsed_rel += label
|
||||
if props_start is not None:
|
||||
parsed_rel += parse_map(rel[props_start : len(rel) - 1], ignore_order)
|
||||
parsed_rel += "]"
|
||||
parsed_rel += parse_map(rel[props_start:len(rel) - 1], ignore_order)
|
||||
parsed_rel += ']'
|
||||
return parsed_rel
|
||||
|
||||
@@ -17,29 +17,32 @@ from behave import given, then, step, when
|
||||
from neo4j.graph import Node, Path, Relationship
|
||||
|
||||
|
||||
@given("parameters are")
|
||||
@given('parameters are')
|
||||
def parameters_step(context):
|
||||
context.test_parameters.set_parameters_from_table(context.table)
|
||||
|
||||
|
||||
@then("parameters are")
|
||||
@then('parameters are')
|
||||
def parameters_step(context):
|
||||
context.test_parameters.set_parameters_from_table(context.table)
|
||||
|
||||
|
||||
@step("having executed")
|
||||
@step('having executed')
|
||||
def having_executed_step(context):
|
||||
context.results = database.query(context.text, context, context.test_parameters.get_parameters())
|
||||
context.results = database.query(
|
||||
context.text, context, context.test_parameters.get_parameters())
|
||||
|
||||
|
||||
@when("executing query")
|
||||
@when('executing query')
|
||||
def executing_query_step(context):
|
||||
context.results = database.query(context.text, context, context.test_parameters.get_parameters())
|
||||
context.results = database.query(
|
||||
context.text, context, context.test_parameters.get_parameters())
|
||||
|
||||
|
||||
@when("executing control query")
|
||||
@when('executing control query')
|
||||
def executing_query_step(context):
|
||||
context.results = database.query(context.text, context, context.test_parameters.get_parameters())
|
||||
context.results = database.query(
|
||||
context.text, context, context.test_parameters.get_parameters())
|
||||
|
||||
|
||||
def parse_props(props_key_value):
|
||||
@@ -90,11 +93,11 @@ def to_string(element):
|
||||
# parsing Node
|
||||
sol = "("
|
||||
if element.labels:
|
||||
sol += ":" + ": ".join(element.labels)
|
||||
sol += ':' + ': '.join(element.labels)
|
||||
|
||||
if element.keys():
|
||||
if element.labels:
|
||||
sol += " "
|
||||
sol += ' '
|
||||
sol += parse_props(element.items())
|
||||
|
||||
sol += ")"
|
||||
@@ -106,7 +109,7 @@ def to_string(element):
|
||||
if element.type:
|
||||
sol += element.type
|
||||
if element.keys():
|
||||
sol += " "
|
||||
sol += ' '
|
||||
sol += parse_props(element.items())
|
||||
sol += "]"
|
||||
return sol
|
||||
@@ -141,12 +144,12 @@ def to_string(element):
|
||||
|
||||
elif isinstance(element, list):
|
||||
# parsing list
|
||||
sol = "["
|
||||
sol = '['
|
||||
el_str = []
|
||||
for el in element:
|
||||
el_str.append(to_string(el))
|
||||
sol += ", ".join(el_str)
|
||||
sol += "]"
|
||||
sol += ', '.join(el_str)
|
||||
sol += ']'
|
||||
|
||||
return sol
|
||||
|
||||
@@ -159,22 +162,23 @@ def to_string(element):
|
||||
elif isinstance(element, dict):
|
||||
# parsing map
|
||||
if len(element) == 0:
|
||||
return "{}"
|
||||
sol = "{"
|
||||
return '{}'
|
||||
sol = '{'
|
||||
for key, val in element.items():
|
||||
sol += key + ":" + to_string(val) + ","
|
||||
sol = sol[:-1] + "}"
|
||||
sol += key + ':' + to_string(val) + ','
|
||||
sol = sol[:-1] + '}'
|
||||
return sol
|
||||
|
||||
elif isinstance(element, float):
|
||||
# parsing float, scientific
|
||||
if "e" in str(element):
|
||||
if str(element)[-3] == "-":
|
||||
if 'e' in str(element):
|
||||
if str(element)[-3] == '-':
|
||||
zeroes = int(str(element)[-2:]) - 1
|
||||
num_str = ""
|
||||
if str(element)[0] == "-":
|
||||
num_str += "-"
|
||||
num_str += "." + zeroes * "0" + str(element)[:-4].replace("-", "").replace(".", "")
|
||||
num_str = ''
|
||||
if str(element)[0] == '-':
|
||||
num_str += '-'
|
||||
num_str += '.' + zeroes * '0' + \
|
||||
str(element)[:-4].replace("-", "").replace(".", "")
|
||||
return num_str
|
||||
|
||||
return str(element)
|
||||
@@ -197,14 +201,9 @@ def get_result_rows(context, ignore_order):
|
||||
keys = result.keys()
|
||||
values = result.values()
|
||||
for i in range(0, len(keys)):
|
||||
result_rows.append(
|
||||
keys[i]
|
||||
+ ":"
|
||||
+ parser.parse(
|
||||
to_string(values[i]).replace("\n", "\\n").replace(" ", ""),
|
||||
ignore_order,
|
||||
)
|
||||
)
|
||||
result_rows.append(keys[i] + ":" + parser.parse(
|
||||
to_string(values[i]).replace("\n", "\\n").replace(" ", ""),
|
||||
ignore_order))
|
||||
return result_rows
|
||||
|
||||
|
||||
@@ -222,7 +221,9 @@ def get_expected_rows(context, ignore_order):
|
||||
expected_rows = []
|
||||
for row in context.table:
|
||||
for col in context.table.headings:
|
||||
expected_rows.append(col + ":" + parser.parse(row[col].replace(" ", ""), ignore_order))
|
||||
expected_rows.append(
|
||||
col + ":" + parser.parse(row[col].replace(" ", ""),
|
||||
ignore_order))
|
||||
return expected_rows
|
||||
|
||||
|
||||
@@ -241,13 +242,13 @@ def validate(context, ignore_order):
|
||||
|
||||
context.log.info("Expected: %s", str(expected_rows))
|
||||
context.log.info("Results: %s", str(result_rows))
|
||||
assert len(expected_rows) == len(result_rows)
|
||||
assert(len(expected_rows) == len(result_rows))
|
||||
|
||||
for i in range(0, len(expected_rows)):
|
||||
if expected_rows[i] in result_rows:
|
||||
result_rows.remove(expected_rows[i])
|
||||
else:
|
||||
assert False
|
||||
assert(False)
|
||||
|
||||
|
||||
def validate_in_order(context, ignore_order):
|
||||
@@ -266,26 +267,26 @@ def validate_in_order(context, ignore_order):
|
||||
|
||||
context.log.info("Expected: %s", str(expected_rows))
|
||||
context.log.info("Results: %s", str(result_rows))
|
||||
assert len(expected_rows) == len(result_rows)
|
||||
assert(len(expected_rows) == len(result_rows))
|
||||
|
||||
for i in range(0, len(expected_rows)):
|
||||
if expected_rows[i] != result_rows[i]:
|
||||
assert False
|
||||
assert(False)
|
||||
|
||||
|
||||
@then("the result should be")
|
||||
@then('the result should be')
|
||||
def expected_result_step(context):
|
||||
validate(context, False)
|
||||
check_exception(context)
|
||||
|
||||
|
||||
@then("the result should be, in order")
|
||||
@then('the result should be, in order')
|
||||
def expected_result_step(context):
|
||||
validate_in_order(context, False)
|
||||
check_exception(context)
|
||||
|
||||
|
||||
@then("the result should be (ignoring element order for lists)")
|
||||
@then('the result should be (ignoring element order for lists)')
|
||||
def expected_result_step(context):
|
||||
validate(context, True)
|
||||
check_exception(context)
|
||||
@@ -294,20 +295,20 @@ def expected_result_step(context):
|
||||
def check_exception(context):
|
||||
if context.exception is not None:
|
||||
context.log.info("Exception when executing query!")
|
||||
assert False
|
||||
assert(False)
|
||||
|
||||
|
||||
@then("the result should be empty")
|
||||
@then('the result should be empty')
|
||||
def empty_result_step(context):
|
||||
assert len(context.results) == 0
|
||||
assert(len(context.results) == 0)
|
||||
check_exception(context)
|
||||
|
||||
|
||||
@then("the side effects should be")
|
||||
@then('the side effects should be')
|
||||
def side_effects_step(context):
|
||||
return
|
||||
|
||||
|
||||
@then("no side effects")
|
||||
@then('no side effects')
|
||||
def side_effects_step(context):
|
||||
return
|
||||
|
||||
@@ -40,15 +40,15 @@ class TestParameters:
|
||||
par = dict()
|
||||
for row in table:
|
||||
par[row[0]] = self.parse_parameters(row[1])
|
||||
if isinstance(par[row[0]], str) and par[row[0]].startswith("'") and par[row[0]].endswith("'"):
|
||||
par[row[0]] = par[row[0]][1 : len(par[row[0]]) - 1]
|
||||
if isinstance(par[row[0]], str) and par[row[0]].startswith("'") \
|
||||
and par[row[0]].endswith("'"):
|
||||
par[row[0]] = par[row[0]][1:len(par[row[0]]) - 1]
|
||||
par[table.headings[0]] = self.parse_parameters(table.headings[1])
|
||||
if (
|
||||
isinstance(par[table.headings[0]], str)
|
||||
and par[table.headings[0]].startswith("'")
|
||||
and par[table.headings[0]].endswith("'")
|
||||
):
|
||||
par[table.headings[0]] = par[table.headings[0]][1 : len(par[table.headings[0]]) - 1]
|
||||
if isinstance(par[table.headings[0]], str) and \
|
||||
par[table.headings[0]].startswith("'") and \
|
||||
par[table.headings[0]].endswith("'"):
|
||||
par[table.headings[0]] = \
|
||||
par[table.headings[0]][1:len(par[table.headings[0]]) - 1]
|
||||
|
||||
self.parameters = par
|
||||
|
||||
|
||||
@@ -57,3 +57,43 @@ Feature: Case
|
||||
Then the result should be:
|
||||
| z |
|
||||
| ['nottwo', 'two', 'nottwo'] |
|
||||
|
||||
Scenario: Simple CASE nullcheck does not have match:
|
||||
Given an empty graph
|
||||
When executing query:
|
||||
"""
|
||||
WITH 2 AS name RETURN CASE name WHEN 3 THEN 'something went wrong' WHEN null THEN "doesn't work" ELSE 'works' END
|
||||
"""
|
||||
Then the result should be:
|
||||
| CASE name WHEN 3 THEN 'something went wrong' WHEN null THEN "doesn't work" ELSE 'works' END |
|
||||
| 'works' |
|
||||
|
||||
Scenario: Simple CASE nullcheck does have match:
|
||||
Given an empty graph
|
||||
When executing query:
|
||||
"""
|
||||
WITH 2 AS name RETURN CASE name WHEN 2 THEN 'works' WHEN null THEN "doesn't work" ELSE 'something went wrong' END
|
||||
"""
|
||||
Then the result should be:
|
||||
| CASE name WHEN 2 THEN 'works' WHEN null THEN "doesn't work" ELSE 'something went wrong' END |
|
||||
| 'works' |
|
||||
|
||||
Scenario: Generic CASE nullcheck does have match:
|
||||
Given an empty graph
|
||||
When executing query:
|
||||
"""
|
||||
WITH 2 AS name RETURN CASE WHEN name is NULL THEN "doesn't work" WHEN name = 2 THEN "works" ELSE "something went wrong" END
|
||||
"""
|
||||
Then the result should be:
|
||||
| CASE WHEN name is NULL THEN "doesn't work" WHEN name = 2 THEN "works" ELSE "something went wrong" END |
|
||||
| 'works' |
|
||||
|
||||
Scenario: Generic CASE expression is null:
|
||||
Given an empty graph
|
||||
When executing query:
|
||||
"""
|
||||
WITH null AS name RETURN CASE name WHEN null THEN "doesn't work" WHEN 2 THEN "doesn't work" ELSE 'works' END
|
||||
"""
|
||||
Then the result should be:
|
||||
| CASE name WHEN null THEN "doesn't work" WHEN 2 THEN "doesn't work" ELSE 'works' END |
|
||||
| 'works' |
|
||||
|
||||
@@ -37,14 +37,20 @@ QUERIES = [
|
||||
("CREATE (n {name: $name})", {"name": 5, "leftover": 42}),
|
||||
("MATCH (n), (m) CREATE (n)-[:e {when: $when}]->(m)", {"when": 42}),
|
||||
("MATCH (n) RETURN n", {}),
|
||||
("MATCH (n), (m {type: $type}) RETURN count(n), count(m)", {"type": "dadada"}),
|
||||
(
|
||||
"MATCH (n), (m {type: $type}) RETURN count(n), count(m)",
|
||||
{"type": "dadada"}
|
||||
),
|
||||
(
|
||||
"MERGE (n) ON CREATE SET n.created = timestamp() "
|
||||
"ON MATCH SET n.lastSeen = timestamp() "
|
||||
"RETURN n.name, n.created, n.lastSeen",
|
||||
{},
|
||||
{}
|
||||
),
|
||||
(
|
||||
"MATCH (n {value: $value}) SET n.value = 0 RETURN n",
|
||||
{"value": "nandare!"}
|
||||
),
|
||||
("MATCH (n {value: $value}) SET n.value = 0 RETURN n", {"value": "nandare!"}),
|
||||
("MATCH (n), (m) SET n.value = m.value", {}),
|
||||
("MATCH (n {test: $test}) REMOVE n.value", {"test": 48}),
|
||||
("MATCH (n), (m) REMOVE n.value, m.value", {}),
|
||||
@@ -68,8 +74,7 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
storage_directory.name,
|
||||
"--audit-enabled",
|
||||
"--log-file=memgraph.log",
|
||||
"--log-level=TRACE",
|
||||
]
|
||||
"--log-level=TRACE"]
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph = subprocess.Popen(list(map(str, memgraph_args)))
|
||||
@@ -87,13 +92,8 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
def execute_queries(queries):
|
||||
for query, params in queries:
|
||||
print(query, params)
|
||||
args = [
|
||||
tester_binary,
|
||||
"--query",
|
||||
query,
|
||||
"--params-json",
|
||||
json.dumps(params),
|
||||
]
|
||||
args = [tester_binary, "--query", query,
|
||||
"--params-json", json.dumps(params)]
|
||||
subprocess.run(args).check_returncode()
|
||||
|
||||
# Execute all queries
|
||||
@@ -109,17 +109,10 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
# Verify the written log
|
||||
print("\033[1;36m~~ Starting log verification ~~\033[0m")
|
||||
with open(os.path.join(storage_directory.name, "audit", "audit.log")) as f:
|
||||
reader = csv.reader(
|
||||
f,
|
||||
delimiter=",",
|
||||
doublequote=False,
|
||||
escapechar="\\",
|
||||
lineterminator="\n",
|
||||
quotechar='"',
|
||||
quoting=csv.QUOTE_MINIMAL,
|
||||
skipinitialspace=False,
|
||||
strict=True,
|
||||
)
|
||||
reader = csv.reader(f, delimiter=',', doublequote=False,
|
||||
escapechar='\\', lineterminator='\n',
|
||||
quotechar='"', quoting=csv.QUOTE_MINIMAL,
|
||||
skipinitialspace=False, strict=True)
|
||||
queries = []
|
||||
for line in reader:
|
||||
timestamp, address, username, query, params = line
|
||||
@@ -127,13 +120,15 @@ def execute_test(memgraph_binary, tester_binary):
|
||||
queries.append((query, params))
|
||||
print(query, params)
|
||||
|
||||
assert queries == QUERIES, "Logged queries don't match " "executed queries!"
|
||||
assert queries == QUERIES, "Logged queries don't match " \
|
||||
"executed queries!"
|
||||
print("\033[1;36m~~ Finished log verification ~~\033[0m\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "audit", "tester")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "audit", "tester")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
|
||||
@@ -29,8 +29,15 @@ PROJECT_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, "..", "..", ".."))
|
||||
|
||||
QUERIES = [
|
||||
# CREATE
|
||||
("CREATE (n)", ("CREATE",)),
|
||||
("MATCH (n), (m) CREATE (n)-[:e]->(m)", ("CREATE", "MATCH")),
|
||||
(
|
||||
"CREATE (n)",
|
||||
("CREATE",)
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) CREATE (n)-[:e]->(m)",
|
||||
("CREATE", "MATCH")
|
||||
),
|
||||
|
||||
# DELETE
|
||||
(
|
||||
"MATCH (n) DELETE n",
|
||||
@@ -40,43 +47,116 @@ QUERIES = [
|
||||
"MATCH (n) DETACH DELETE n",
|
||||
("DELETE", "MATCH"),
|
||||
),
|
||||
|
||||
# MATCH
|
||||
("MATCH (n) RETURN n", ("MATCH",)),
|
||||
("MATCH (n), (m) RETURN count(n), count(m)", ("MATCH",)),
|
||||
(
|
||||
"MATCH (n) RETURN n",
|
||||
("MATCH",)
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) RETURN count(n), count(m)",
|
||||
("MATCH",)
|
||||
),
|
||||
|
||||
# MERGE
|
||||
(
|
||||
"MERGE (n) ON CREATE SET n.created = timestamp() "
|
||||
"ON MATCH SET n.lastSeen = timestamp() "
|
||||
"RETURN n.name, n.created, n.lastSeen",
|
||||
("MERGE",),
|
||||
("MERGE",)
|
||||
),
|
||||
|
||||
# SET
|
||||
("MATCH (n) SET n.value = 0 RETURN n", ("SET", "MATCH")),
|
||||
("MATCH (n), (m) SET n.value = m.value", ("SET", "MATCH")),
|
||||
(
|
||||
"MATCH (n) SET n.value = 0 RETURN n",
|
||||
("SET", "MATCH")
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) SET n.value = m.value",
|
||||
("SET", "MATCH")
|
||||
),
|
||||
|
||||
# REMOVE
|
||||
("MATCH (n) REMOVE n.value", ("REMOVE", "MATCH")),
|
||||
("MATCH (n), (m) REMOVE n.value, m.value", ("REMOVE", "MATCH")),
|
||||
(
|
||||
"MATCH (n) REMOVE n.value",
|
||||
("REMOVE", "MATCH")
|
||||
),
|
||||
(
|
||||
"MATCH (n), (m) REMOVE n.value, m.value",
|
||||
("REMOVE", "MATCH")
|
||||
),
|
||||
|
||||
# INDEX
|
||||
("CREATE INDEX ON :User (id)", ("INDEX",)),
|
||||
(
|
||||
"CREATE INDEX ON :User (id)",
|
||||
("INDEX",)
|
||||
),
|
||||
|
||||
# AUTH
|
||||
("CREATE ROLE test_role", ("AUTH",)),
|
||||
("DROP ROLE test_role", ("AUTH",)),
|
||||
("SHOW ROLES", ("AUTH",)),
|
||||
("CREATE USER test_user", ("AUTH",)),
|
||||
("SET PASSWORD FOR test_user TO '1234'", ("AUTH",)),
|
||||
("DROP USER test_user", ("AUTH",)),
|
||||
("SHOW USERS", ("AUTH",)),
|
||||
("SET ROLE FOR test_user TO test_role", ("AUTH",)),
|
||||
("CLEAR ROLE FOR test_user", ("AUTH",)),
|
||||
("GRANT ALL PRIVILEGES TO test_user", ("AUTH",)),
|
||||
("DENY ALL PRIVILEGES TO test_user", ("AUTH",)),
|
||||
("REVOKE ALL PRIVILEGES FROM test_user", ("AUTH",)),
|
||||
("SHOW PRIVILEGES FOR test_user", ("AUTH",)),
|
||||
("SHOW ROLE FOR test_user", ("AUTH",)),
|
||||
("SHOW USERS FOR test_role", ("AUTH",)),
|
||||
(
|
||||
"CREATE ROLE test_role",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"DROP ROLE test_role",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SHOW ROLES",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"CREATE USER test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SET PASSWORD FOR test_user TO '1234'",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"DROP USER test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SHOW USERS",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SET ROLE FOR test_user TO test_role",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"CLEAR ROLE FOR test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"GRANT ALL PRIVILEGES TO test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"DENY ALL PRIVILEGES TO test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"REVOKE ALL PRIVILEGES FROM test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SHOW PRIVILEGES FOR test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SHOW ROLE FOR test_user",
|
||||
("AUTH",)
|
||||
),
|
||||
(
|
||||
"SHOW USERS FOR test_role",
|
||||
("AUTH",)
|
||||
),
|
||||
]
|
||||
|
||||
UNAUTHORIZED_ERROR = "You are not authorized to execute this query! Please " "contact your database administrator."
|
||||
UNAUTHORIZED_ERROR = "You are not authorized to execute this query! Please " \
|
||||
"contact your database administrator."
|
||||
|
||||
|
||||
def wait_for_server(port, delay=0.1):
|
||||
@@ -86,15 +166,8 @@ def wait_for_server(port, delay=0.1):
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def execute_tester(
|
||||
binary,
|
||||
queries,
|
||||
should_fail=False,
|
||||
failure_message="",
|
||||
username="",
|
||||
password="",
|
||||
check_failure=True,
|
||||
):
|
||||
def execute_tester(binary, queries, should_fail=False, failure_message="",
|
||||
username="", password="", check_failure=True):
|
||||
args = [binary, "--username", username, "--password", password]
|
||||
if should_fail:
|
||||
args.append("--should-fail")
|
||||
@@ -127,28 +200,18 @@ def check_permissions(query_perms, user_perms):
|
||||
|
||||
def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
storage_directory = tempfile.TemporaryDirectory()
|
||||
memgraph_args = [memgraph_binary, "--data-directory", storage_directory.name]
|
||||
memgraph_args = [memgraph_binary,
|
||||
"--data-directory", storage_directory.name]
|
||||
|
||||
def execute_admin_queries(queries):
|
||||
return execute_tester(
|
||||
tester_binary,
|
||||
queries,
|
||||
should_fail=False,
|
||||
check_failure=True,
|
||||
username="admin",
|
||||
password="admin",
|
||||
)
|
||||
return execute_tester(tester_binary, queries, should_fail=False,
|
||||
check_failure=True, username="admin",
|
||||
password="admin")
|
||||
|
||||
def execute_user_queries(queries, should_fail=False, failure_message="", check_failure=True):
|
||||
return execute_tester(
|
||||
tester_binary,
|
||||
queries,
|
||||
should_fail,
|
||||
failure_message,
|
||||
"user",
|
||||
"user",
|
||||
check_failure,
|
||||
)
|
||||
def execute_user_queries(queries, should_fail=False, failure_message="",
|
||||
check_failure=True):
|
||||
return execute_tester(tester_binary, queries, should_fail,
|
||||
failure_message, "user", "user", check_failure)
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph = subprocess.Popen(list(map(str, memgraph_args)))
|
||||
@@ -164,13 +227,11 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
|
||||
|
||||
# Prepare all users
|
||||
execute_admin_queries(
|
||||
[
|
||||
"CREATE USER ADmin IDENTIFIED BY 'admin'",
|
||||
"GRANT ALL PRIVILEGES TO admIN",
|
||||
"CREATE USER usEr IDENTIFIED BY 'user'",
|
||||
]
|
||||
)
|
||||
execute_admin_queries([
|
||||
"CREATE USER ADmin IDENTIFIED BY 'admin'",
|
||||
"GRANT ALL PRIVILEGES TO admIN",
|
||||
"CREATE USER usEr IDENTIFIED BY 'user'",
|
||||
])
|
||||
|
||||
# Find all existing permissions
|
||||
permissions = set()
|
||||
@@ -182,14 +243,12 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
print("\033[1;36m~~ Starting query test ~~\033[0m")
|
||||
for mask in range(0, 2 ** len(permissions)):
|
||||
user_perms = get_permissions(permissions, mask)
|
||||
print(
|
||||
"\033[1;34m~~ Checking queries with privileges: ",
|
||||
", ".join(user_perms),
|
||||
" ~~\033[0m",
|
||||
)
|
||||
print("\033[1;34m~~ Checking queries with privileges: ",
|
||||
", ".join(user_perms), " ~~\033[0m")
|
||||
admin_queries = ["REVOKE ALL PRIVILEGES FROM uSer"]
|
||||
if len(user_perms) > 0:
|
||||
admin_queries.append("GRANT {} TO User".format(", ".join(user_perms)))
|
||||
admin_queries.append(
|
||||
"GRANT {} TO User".format(", ".join(user_perms)))
|
||||
execute_admin_queries(admin_queries)
|
||||
authorized, unauthorized = [], []
|
||||
for query, query_perms in QUERIES:
|
||||
@@ -197,43 +256,35 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
authorized.append(query)
|
||||
else:
|
||||
unauthorized.append(query)
|
||||
execute_user_queries(authorized, check_failure=False, failure_message=UNAUTHORIZED_ERROR)
|
||||
execute_user_queries(unauthorized, should_fail=True, failure_message=UNAUTHORIZED_ERROR)
|
||||
execute_user_queries(authorized, check_failure=False,
|
||||
failure_message=UNAUTHORIZED_ERROR)
|
||||
execute_user_queries(unauthorized, should_fail=True,
|
||||
failure_message=UNAUTHORIZED_ERROR)
|
||||
print("\033[1;36m~~ Finished query test ~~\033[0m\n")
|
||||
|
||||
# Run the user/role permissions test
|
||||
print("\033[1;36m~~ Starting permissions test ~~\033[0m")
|
||||
execute_admin_queries(
|
||||
[
|
||||
"CREATE ROLE roLe",
|
||||
"REVOKE ALL PRIVILEGES FROM uSeR",
|
||||
]
|
||||
)
|
||||
execute_admin_queries([
|
||||
"CREATE ROLE roLe",
|
||||
"REVOKE ALL PRIVILEGES FROM uSeR",
|
||||
])
|
||||
execute_checker(checker_binary, [])
|
||||
for user_perm in ["GRANT", "DENY", "REVOKE"]:
|
||||
for role_perm in ["GRANT", "DENY", "REVOKE"]:
|
||||
for mapped in [True, False]:
|
||||
print(
|
||||
"\033[1;34m~~ Checking permissions with user ",
|
||||
user_perm,
|
||||
", role ",
|
||||
role_perm,
|
||||
"user mapped to role:",
|
||||
mapped,
|
||||
" ~~\033[0m",
|
||||
)
|
||||
print("\033[1;34m~~ Checking permissions with user ",
|
||||
user_perm, ", role ", role_perm,
|
||||
"user mapped to role:", mapped, " ~~\033[0m")
|
||||
if mapped:
|
||||
execute_admin_queries(["SET ROLE FOR USER TO roLE"])
|
||||
else:
|
||||
execute_admin_queries(["CLEAR ROLE FOR user"])
|
||||
user_prep = "FROM" if user_perm == "REVOKE" else "TO"
|
||||
role_prep = "FROM" if role_perm == "REVOKE" else "TO"
|
||||
execute_admin_queries(
|
||||
[
|
||||
"{} MATCH {} user".format(user_perm, user_prep),
|
||||
"{} MATCH {} rOLe".format(role_perm, role_prep),
|
||||
]
|
||||
)
|
||||
execute_admin_queries([
|
||||
"{} MATCH {} user".format(user_perm, user_prep),
|
||||
"{} MATCH {} rOLe".format(role_perm, role_prep)
|
||||
])
|
||||
expected = []
|
||||
perms = [user_perm, role_perm] if mapped else [user_perm]
|
||||
if "DENY" in perms:
|
||||
@@ -262,8 +313,10 @@ def execute_test(memgraph_binary, tester_binary, checker_binary):
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "auth", "tester")
|
||||
checker_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "auth", "checker")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "auth", "tester")
|
||||
checker_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "auth", "checker")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
|
||||
@@ -40,7 +40,7 @@ def wait_for_server(port, delay=0.1):
|
||||
|
||||
|
||||
def sorted_content(file_path):
|
||||
with open(file_path, "r") as fin:
|
||||
with open(file_path, 'r') as fin:
|
||||
return sorted(list(map(lambda x: x.strip(), fin.readlines())))
|
||||
|
||||
|
||||
@@ -52,30 +52,32 @@ def list_to_string(data):
|
||||
return ret
|
||||
|
||||
|
||||
def execute_test(memgraph_binary, dump_binary, test_directory, test_type, write_expected):
|
||||
assert test_type in [
|
||||
"SNAPSHOT",
|
||||
"WAL",
|
||||
], "Test type should be either 'SNAPSHOT' or 'WAL'."
|
||||
print("\033[1;36m~~ Executing test {} ({}) ~~\033[0m".format(os.path.relpath(test_directory, TESTS_DIR), test_type))
|
||||
def execute_test(
|
||||
memgraph_binary,
|
||||
dump_binary,
|
||||
test_directory,
|
||||
test_type,
|
||||
write_expected):
|
||||
assert test_type in ["SNAPSHOT", "WAL"], \
|
||||
"Test type should be either 'SNAPSHOT' or 'WAL'."
|
||||
print("\033[1;36m~~ Executing test {} ({}) ~~\033[0m"
|
||||
.format(os.path.relpath(test_directory, TESTS_DIR), test_type))
|
||||
|
||||
working_data_directory = tempfile.TemporaryDirectory()
|
||||
if test_type == "SNAPSHOT":
|
||||
snapshots_dir = os.path.join(working_data_directory.name, "snapshots")
|
||||
os.makedirs(snapshots_dir)
|
||||
shutil.copy(os.path.join(test_directory, SNAPSHOT_FILE_NAME), snapshots_dir)
|
||||
shutil.copy(os.path.join(test_directory, SNAPSHOT_FILE_NAME),
|
||||
snapshots_dir)
|
||||
else:
|
||||
wal_dir = os.path.join(working_data_directory.name, "wal")
|
||||
os.makedirs(wal_dir)
|
||||
shutil.copy(os.path.join(test_directory, WAL_FILE_NAME), wal_dir)
|
||||
|
||||
memgraph_args = [
|
||||
memgraph_binary,
|
||||
"--storage-recover-on-startup",
|
||||
"--storage-properties-on-edges",
|
||||
"--data-directory",
|
||||
working_data_directory.name,
|
||||
]
|
||||
memgraph_args = [memgraph_binary,
|
||||
"--storage-recover-on-startup",
|
||||
"--storage-properties-on-edges",
|
||||
"--data-directory", working_data_directory.name]
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph = subprocess.Popen(memgraph_args)
|
||||
@@ -102,21 +104,22 @@ def execute_test(memgraph_binary, dump_binary, test_directory, test_type, write_
|
||||
dump_file_name = DUMP_SNAPSHOT_FILE_NAME if test_type == "SNAPSHOT" else DUMP_WAL_FILE_NAME
|
||||
|
||||
if write_expected:
|
||||
with open(dump_output_file.name, "r") as dump:
|
||||
with open(dump_output_file.name, 'r') as dump:
|
||||
queries_got = dump.readlines()
|
||||
# Write dump files
|
||||
expected_dump_file = os.path.join(test_directory, dump_file_name)
|
||||
with open(expected_dump_file, "w") as expected:
|
||||
with open(expected_dump_file, 'w') as expected:
|
||||
expected.writelines(queries_got)
|
||||
else:
|
||||
# Compare dump files
|
||||
expected_dump_file = os.path.join(test_directory, dump_file_name)
|
||||
assert os.path.exists(expected_dump_file), "Could not find expected dump path {}".format(expected_dump_file)
|
||||
assert os.path.exists(expected_dump_file), \
|
||||
"Could not find expected dump path {}".format(expected_dump_file)
|
||||
queries_got = sorted_content(dump_output_file.name)
|
||||
queries_expected = sorted_content(expected_dump_file)
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" "{}".format(
|
||||
list_to_string(queries_got), list_to_string(queries_expected)
|
||||
)
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" \
|
||||
"{}".format(list_to_string(queries_got),
|
||||
list_to_string(queries_expected))
|
||||
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
|
||||
@@ -138,17 +141,15 @@ def find_test_directories(directory):
|
||||
continue
|
||||
snapshot_file = os.path.join(test_dir_path, SNAPSHOT_FILE_NAME)
|
||||
wal_file = os.path.join(test_dir_path, WAL_FILE_NAME)
|
||||
dump_snapshot_file = os.path.join(test_dir_path, DUMP_SNAPSHOT_FILE_NAME)
|
||||
dump_snapshot_file = os.path.join(
|
||||
test_dir_path, DUMP_SNAPSHOT_FILE_NAME)
|
||||
dump_wal_file = os.path.join(test_dir_path, DUMP_WAL_FILE_NAME)
|
||||
if (
|
||||
os.path.isfile(snapshot_file)
|
||||
and os.path.isfile(dump_snapshot_file)
|
||||
and os.path.isfile(wal_file)
|
||||
and os.path.isfile(dump_wal_file)
|
||||
):
|
||||
if (os.path.isfile(snapshot_file) and os.path.isfile(dump_snapshot_file)
|
||||
and os.path.isfile(wal_file) and os.path.isfile(dump_wal_file)):
|
||||
test_dirs.append(test_dir_path)
|
||||
else:
|
||||
raise Exception("Missing data in test directory '{}'".format(test_dir_path))
|
||||
raise Exception("Missing data in test directory '{}'"
|
||||
.format(test_dir_path))
|
||||
return test_dirs
|
||||
|
||||
|
||||
@@ -160,17 +161,26 @@ if __name__ == "__main__":
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
parser.add_argument("--dump", default=dump_binary)
|
||||
parser.add_argument(
|
||||
"--write-expected",
|
||||
action="store_true",
|
||||
help="Overwrite the expected cypher with results from current run",
|
||||
)
|
||||
'--write-expected',
|
||||
action='store_true',
|
||||
help='Overwrite the expected cypher with results from current run')
|
||||
args = parser.parse_args()
|
||||
|
||||
test_directories = find_test_directories(TESTS_DIR)
|
||||
assert len(test_directories) > 0, "No tests have been found!"
|
||||
|
||||
for test_directory in test_directories:
|
||||
execute_test(args.memgraph, args.dump, test_directory, "SNAPSHOT", args.write_expected)
|
||||
execute_test(args.memgraph, args.dump, test_directory, "WAL", args.write_expected)
|
||||
execute_test(
|
||||
args.memgraph,
|
||||
args.dump,
|
||||
test_directory,
|
||||
"SNAPSHOT",
|
||||
args.write_expected)
|
||||
execute_test(
|
||||
args.memgraph,
|
||||
args.dump,
|
||||
test_directory,
|
||||
"WAL",
|
||||
args.write_expected)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
@@ -52,14 +52,8 @@ def wait_for_server(port, delay=0.1):
|
||||
time.sleep(delay)
|
||||
|
||||
|
||||
def execute_tester(
|
||||
binary,
|
||||
queries,
|
||||
username="",
|
||||
password="",
|
||||
auth_should_fail=False,
|
||||
query_should_fail=False,
|
||||
):
|
||||
def execute_tester(binary, queries, username="", password="",
|
||||
auth_should_fail=False, query_should_fail=False):
|
||||
if password == "":
|
||||
password = username
|
||||
args = [binary, "--username", username, "--password", password]
|
||||
@@ -82,14 +76,18 @@ class Memgraph:
|
||||
def start(self, **kwargs):
|
||||
self.stop()
|
||||
self._storage_directory = tempfile.TemporaryDirectory()
|
||||
self._auth_module = os.path.join(self._storage_directory.name, "ldap.py")
|
||||
self._auth_config = os.path.join(self._storage_directory.name, "ldap.yaml")
|
||||
script_file = os.path.join(PROJECT_DIR, "src", "auth", "reference_modules", "ldap.py")
|
||||
self._auth_module = os.path.join(self._storage_directory.name,
|
||||
"ldap.py")
|
||||
self._auth_config = os.path.join(self._storage_directory.name,
|
||||
"ldap.yaml")
|
||||
script_file = os.path.join(PROJECT_DIR, "src", "auth",
|
||||
"reference_modules", "ldap.py")
|
||||
virtualenv_bin = os.path.join(SCRIPT_DIR, "ve3", "bin", "python3")
|
||||
with open(script_file) as fin:
|
||||
data = fin.read()
|
||||
data = data.replace("/usr/bin/python3", virtualenv_bin)
|
||||
data = data.replace("/etc/memgraph/auth/ldap.yaml", self._auth_config)
|
||||
data = data.replace("/etc/memgraph/auth/ldap.yaml",
|
||||
self._auth_config)
|
||||
with open(self._auth_module, "w") as fout:
|
||||
fout.write(data)
|
||||
os.chmod(self._auth_module, stat.S_IRWXU | stat.S_IRWXG)
|
||||
@@ -108,13 +106,10 @@ class Memgraph:
|
||||
}
|
||||
with open(self._auth_config, "w") as f:
|
||||
f.write(CONFIG_TEMPLATE.format(**config))
|
||||
args = [
|
||||
self._binary,
|
||||
"--data-directory",
|
||||
self._storage_directory.name,
|
||||
"--auth-module-executable",
|
||||
kwargs.pop("module_executable", self._auth_module),
|
||||
]
|
||||
args = [self._binary,
|
||||
"--data-directory", self._storage_directory.name,
|
||||
"--auth-module-executable",
|
||||
kwargs.pop("module_executable", self._auth_module)]
|
||||
for key, value in kwargs.items():
|
||||
ldap_key = "--auth-module-" + key.replace("_", "-")
|
||||
if isinstance(value, bool):
|
||||
@@ -124,7 +119,8 @@ class Memgraph:
|
||||
args.append(value)
|
||||
self._process = subprocess.Popen(args)
|
||||
time.sleep(0.1)
|
||||
assert self._process.poll() is None, "Memgraph process died " "prematurely!"
|
||||
assert self._process.poll() is None, "Memgraph process died " \
|
||||
"prematurely!"
|
||||
wait_for_server(7687)
|
||||
|
||||
def stop(self, check=True):
|
||||
@@ -141,7 +137,8 @@ class Memgraph:
|
||||
def initialize_test(memgraph, tester_binary, **kwargs):
|
||||
memgraph.start(module_executable="")
|
||||
|
||||
execute_tester(tester_binary, ["CREATE USER root", "GRANT ALL PRIVILEGES TO root"])
|
||||
execute_tester(tester_binary,
|
||||
["CREATE USER root", "GRANT ALL PRIVILEGES TO root"])
|
||||
check_login = kwargs.pop("check_login", True)
|
||||
memgraph.restart(**kwargs)
|
||||
if check_login:
|
||||
@@ -173,15 +170,18 @@ def test_role_mapping(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
|
||||
execute_tester(tester_binary, [], "alice")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
|
||||
execute_tester(tester_binary, [], "bob")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "bob", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "bob",
|
||||
query_should_fail=True)
|
||||
|
||||
execute_tester(tester_binary, [], "carol")
|
||||
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT CREATE TO admin"], "root")
|
||||
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "carol")
|
||||
execute_tester(tester_binary, ["CREATE (n) RETURN n"], "dave")
|
||||
@@ -192,13 +192,15 @@ def test_role_mapping(memgraph, tester_binary):
|
||||
def test_role_removal(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, [], "alice")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.restart(manage_roles=False)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
execute_tester(tester_binary, ["CLEAR ROLE FOR alice"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
@@ -227,22 +229,28 @@ def test_user_is_role(memgraph, tester_binary):
|
||||
|
||||
def test_user_permissions_persistancy(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, ["CREATE USER alice", "GRANT MATCH TO alice"], "root")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE USER alice", "GRANT MATCH TO alice"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_role_permissions_persistancy(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_only_authentication(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, manage_roles=False)
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
@@ -259,16 +267,22 @@ def test_wrong_suffix(memgraph, tester_binary):
|
||||
|
||||
|
||||
def test_suffix_with_spaces(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, suffix=", ou= people, dc = memgraph, dc = com")
|
||||
execute_tester(tester_binary, ["CREATE USER alice", "GRANT MATCH TO alice"], "root")
|
||||
initialize_test(memgraph, tester_binary,
|
||||
suffix=", ou= people, dc = memgraph, dc = com")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE USER alice", "GRANT MATCH TO alice"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_role_mapping_wrong_root_dn(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, root_dn="ou=invalid,dc=memgraph,dc=com")
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
initialize_test(memgraph, tester_binary,
|
||||
root_dn="ou=invalid,dc=memgraph,dc=com")
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
@@ -276,8 +290,11 @@ def test_role_mapping_wrong_root_dn(memgraph, tester_binary):
|
||||
|
||||
def test_role_mapping_wrong_root_objectclass(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, root_objectclass="person")
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
@@ -285,8 +302,11 @@ def test_role_mapping_wrong_root_objectclass(memgraph, tester_binary):
|
||||
|
||||
def test_role_mapping_wrong_user_attribute(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, user_attribute="cn")
|
||||
execute_tester(tester_binary, ["CREATE ROLE moderator", "GRANT MATCH TO moderator"], "root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice", query_should_fail=True)
|
||||
execute_tester(tester_binary,
|
||||
["CREATE ROLE moderator", "GRANT MATCH TO moderator"],
|
||||
"root")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice",
|
||||
query_should_fail=True)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "alice")
|
||||
memgraph.stop()
|
||||
@@ -294,7 +314,8 @@ def test_role_mapping_wrong_user_attribute(memgraph, tester_binary):
|
||||
|
||||
def test_wrong_password(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary)
|
||||
execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.stop()
|
||||
|
||||
@@ -305,10 +326,12 @@ def test_password_persistancy(memgraph, tester_binary):
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="sudo")
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.restart(module_executable="")
|
||||
execute_tester(tester_binary, [], "root", password="sudo", auth_should_fail=True)
|
||||
execute_tester(tester_binary, [], "root", password="sudo",
|
||||
auth_should_fail=True)
|
||||
execute_tester(tester_binary, ["SHOW USERS"], "root", password="root")
|
||||
memgraph.stop()
|
||||
|
||||
@@ -316,25 +339,33 @@ def test_password_persistancy(memgraph, tester_binary):
|
||||
def test_user_multiple_roles(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, check_login=False)
|
||||
memgraph.restart()
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
memgraph.restart(manage_roles=False)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
memgraph.restart(manage_roles=False, root_dn="")
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root", query_should_fail=True)
|
||||
execute_tester(tester_binary, ["MATCH (n) RETURN n"], "eve",
|
||||
query_should_fail=True)
|
||||
execute_tester(tester_binary, ["GRANT MATCH TO moderator"], "root",
|
||||
query_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_starttls_failure(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, encryption="starttls", check_login=False)
|
||||
initialize_test(memgraph, tester_binary, encryption="starttls",
|
||||
check_login=False)
|
||||
execute_tester(tester_binary, [], "root", auth_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
|
||||
def test_ssl_failure(memgraph, tester_binary):
|
||||
initialize_test(memgraph, tester_binary, encryption="ssl", check_login=False)
|
||||
initialize_test(memgraph, tester_binary, encryption="ssl",
|
||||
check_login=False)
|
||||
execute_tester(tester_binary, [], "root", auth_should_fail=True)
|
||||
memgraph.stop()
|
||||
|
||||
@@ -344,25 +375,22 @@ def test_ssl_failure(memgraph, tester_binary):
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(PROJECT_DIR, "build", "memgraph")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "ldap", "tester")
|
||||
tester_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "ldap", "tester")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
parser.add_argument("--tester", default=tester_binary)
|
||||
parser.add_argument("--openldap-dir", default=os.path.join(SCRIPT_DIR, "openldap-2.4.47"))
|
||||
parser.add_argument("--openldap-dir",
|
||||
default=os.path.join(SCRIPT_DIR, "openldap-2.4.47"))
|
||||
args = parser.parse_args()
|
||||
|
||||
# Setup Memgraph handler
|
||||
memgraph = Memgraph(args.memgraph)
|
||||
|
||||
# Start the slapd binary
|
||||
slapd_args = [
|
||||
os.path.join(args.openldap_dir, "exe", "libexec", "slapd"),
|
||||
"-h",
|
||||
"ldap://127.0.0.1:1389/",
|
||||
"-d",
|
||||
"0",
|
||||
]
|
||||
slapd_args = [os.path.join(args.openldap_dir, "exe", "libexec", "slapd"),
|
||||
"-h", "ldap://127.0.0.1:1389/", "-d", "0"]
|
||||
slapd = subprocess.Popen(slapd_args)
|
||||
time.sleep(0.1)
|
||||
assert slapd.poll() is None, "slapd process died prematurely!"
|
||||
@@ -381,7 +409,8 @@ if __name__ == "__main__":
|
||||
if slapd_stat != 0:
|
||||
print("slapd process didn't exit cleanly!")
|
||||
|
||||
assert mg_stat == 0 and slapd_stat == 0, "Some of the processes " "(memgraph, slapd) crashed!"
|
||||
assert mg_stat == 0 and slapd_stat == 0, "Some of the processes " \
|
||||
"(memgraph, slapd) crashed!"
|
||||
|
||||
# Execute tests
|
||||
names = sorted(globals().keys())
|
||||
|
||||
@@ -46,18 +46,17 @@ def list_to_string(data):
|
||||
|
||||
|
||||
def verify_lifetime(memgraph_binary, mg_import_csv_binary):
|
||||
print("\033[1;36m~~ Verifying that mg_import_csv can't be started while " "memgraph is running ~~\033[0m")
|
||||
print("\033[1;36m~~ Verifying that mg_import_csv can't be started while "
|
||||
"memgraph is running ~~\033[0m")
|
||||
storage_directory = tempfile.TemporaryDirectory()
|
||||
|
||||
# Generate common args
|
||||
common_args = [
|
||||
"--data-directory",
|
||||
storage_directory.name,
|
||||
"--storage-properties-on-edges=false",
|
||||
]
|
||||
common_args = ["--data-directory", storage_directory.name,
|
||||
"--storage-properties-on-edges=false"]
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + common_args
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + \
|
||||
common_args
|
||||
memgraph = subprocess.Popen(list(map(str, memgraph_args)))
|
||||
time.sleep(0.1)
|
||||
assert memgraph.poll() is None, "Memgraph process died prematurely!"
|
||||
@@ -71,12 +70,14 @@ def verify_lifetime(memgraph_binary, mg_import_csv_binary):
|
||||
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
|
||||
|
||||
# Execute mg_import_csv.
|
||||
mg_import_csv_args = [mg_import_csv_binary, "--nodes", "/dev/null"] + common_args
|
||||
mg_import_csv_args = [mg_import_csv_binary, "--nodes", "/dev/null"] + \
|
||||
common_args
|
||||
ret = subprocess.run(mg_import_csv_args)
|
||||
|
||||
# Check the return code
|
||||
if ret.returncode == 0:
|
||||
raise Exception("The importer was able to run while memgraph was running!")
|
||||
raise Exception(
|
||||
"The importer was able to run while memgraph was running!")
|
||||
|
||||
# Shutdown the memgraph binary
|
||||
memgraph.terminate()
|
||||
@@ -85,34 +86,27 @@ def verify_lifetime(memgraph_binary, mg_import_csv_binary):
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
|
||||
|
||||
def execute_test(
|
||||
name,
|
||||
test_path,
|
||||
test_config,
|
||||
memgraph_binary,
|
||||
mg_import_csv_binary,
|
||||
tester_binary,
|
||||
write_expected,
|
||||
):
|
||||
def execute_test(name, test_path, test_config, memgraph_binary,
|
||||
mg_import_csv_binary, tester_binary, write_expected):
|
||||
print("\033[1;36m~~ Executing test", name, "~~\033[0m")
|
||||
storage_directory = tempfile.TemporaryDirectory()
|
||||
|
||||
# Verify test configuration
|
||||
if ("import_should_fail" not in test_config and "expected" not in test_config) or (
|
||||
"import_should_fail" in test_config and "expected" in test_config
|
||||
):
|
||||
raise Exception("The test should specify either 'import_should_fail' " "or 'expected'!")
|
||||
if ("import_should_fail" not in test_config and
|
||||
"expected" not in test_config) or \
|
||||
("import_should_fail" in test_config and
|
||||
"expected" in test_config):
|
||||
raise Exception("The test should specify either 'import_should_fail' "
|
||||
"or 'expected'!")
|
||||
|
||||
expected_path = test_config.pop("expected", "")
|
||||
import_should_fail = test_config.pop("import_should_fail", False)
|
||||
|
||||
# Generate common args
|
||||
properties_on_edges = bool(test_config.pop("properties_on_edges", False))
|
||||
common_args = [
|
||||
"--data-directory",
|
||||
storage_directory.name,
|
||||
"--storage-properties-on-edges=" + str(properties_on_edges).lower(),
|
||||
]
|
||||
common_args = ["--data-directory", storage_directory.name,
|
||||
"--storage-properties-on-edges=" +
|
||||
str(properties_on_edges).lower()]
|
||||
|
||||
# Generate mg_import_csv args using flags specified in the test
|
||||
mg_import_csv_args = [mg_import_csv_binary] + common_args
|
||||
@@ -131,16 +125,19 @@ def execute_test(
|
||||
|
||||
if import_should_fail:
|
||||
if ret.returncode == 0:
|
||||
raise Exception("The import should have failed, but it " "succeeded instead!")
|
||||
raise Exception("The import should have failed, but it "
|
||||
"succeeded instead!")
|
||||
else:
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
return
|
||||
else:
|
||||
if ret.returncode != 0:
|
||||
raise Exception("The import should have succeeded, but it " "failed instead!")
|
||||
raise Exception("The import should have succeeded, but it "
|
||||
"failed instead!")
|
||||
|
||||
# Start the memgraph binary
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + common_args
|
||||
memgraph_args = [memgraph_binary, "--storage-recover-on-startup"] + \
|
||||
common_args
|
||||
memgraph = subprocess.Popen(list(map(str, memgraph_args)))
|
||||
time.sleep(0.1)
|
||||
assert memgraph.poll() is None, "Memgraph process died prematurely!"
|
||||
@@ -154,17 +151,17 @@ def execute_test(
|
||||
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
|
||||
|
||||
# Get the contents of the database
|
||||
queries_got = extract_rows(
|
||||
subprocess.run([tester_binary], stdout=subprocess.PIPE, check=True).stdout.decode("utf-8")
|
||||
)
|
||||
queries_got = extract_rows(subprocess.run(
|
||||
[tester_binary], stdout=subprocess.PIPE,
|
||||
check=True).stdout.decode("utf-8"))
|
||||
|
||||
# Shutdown the memgraph binary
|
||||
memgraph.terminate()
|
||||
assert memgraph.wait() == 0, "Memgraph process didn't exit cleanly!"
|
||||
|
||||
if write_expected:
|
||||
with open(os.path.join(test_path, expected_path), "w") as expected:
|
||||
expected.write("\n".join(queries_got))
|
||||
with open(os.path.join(test_path, expected_path), 'w') as expected:
|
||||
expected.write('\n'.join(queries_got))
|
||||
|
||||
else:
|
||||
if expected_path:
|
||||
@@ -176,16 +173,18 @@ def execute_test(
|
||||
# Verify the queries
|
||||
queries_expected.sort()
|
||||
queries_got.sort()
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" "{}".format(
|
||||
list_to_string(queries_got), list_to_string(queries_expected)
|
||||
)
|
||||
assert queries_got == queries_expected, "Expected\n{}\nto be equal to\n" \
|
||||
"{}".format(list_to_string(queries_got),
|
||||
list_to_string(queries_expected))
|
||||
print("\033[1;32m~~ Test successful ~~\033[0m\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
memgraph_binary = os.path.join(BUILD_DIR, "memgraph")
|
||||
mg_import_csv_binary = os.path.join(BUILD_DIR, "src", "mg_import_csv")
|
||||
tester_binary = os.path.join(BUILD_DIR, "tests", "integration", "mg_import_csv", "tester")
|
||||
mg_import_csv_binary = os.path.join(
|
||||
BUILD_DIR, "src", "mg_import_csv")
|
||||
tester_binary = os.path.join(
|
||||
BUILD_DIR, "tests", "integration", "mg_import_csv", "tester")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--memgraph", default=memgraph_binary)
|
||||
@@ -194,8 +193,7 @@ if __name__ == "__main__":
|
||||
parser.add_argument(
|
||||
"--write-expected",
|
||||
action="store_true",
|
||||
help="Overwrite the expected values with the results of the current run",
|
||||
)
|
||||
help="Overwrite the expected values with the results of the current run")
|
||||
args = parser.parse_args()
|
||||
|
||||
# First test whether the CSV importer can be started while the main
|
||||
@@ -213,14 +211,7 @@ if __name__ == "__main__":
|
||||
testcases = yaml.safe_load(f)
|
||||
for test_config in testcases:
|
||||
test_name = name + "/" + test_config.pop("name")
|
||||
execute_test(
|
||||
test_name,
|
||||
test_path,
|
||||
test_config,
|
||||
args.memgraph,
|
||||
args.mg_import_csv,
|
||||
args.tester,
|
||||
args.write_expected,
|
||||
)
|
||||
execute_test(test_name, test_path, test_config, args.memgraph,
|
||||
args.mg_import_csv, args.tester, args.write_expected)
|
||||
|
||||
sys.exit(0)
|
||||
|
||||
@@ -36,7 +36,8 @@ def execute_test(**kwargs):
|
||||
timeout = duration * 2 if "hang" not in kwargs else duration * 2 + 60
|
||||
success = False
|
||||
|
||||
server_args = [server_binary, "--interval", interval, "--duration", duration]
|
||||
server_args = [server_binary, "--interval", interval,
|
||||
"--duration", duration]
|
||||
for flag, value in kwargs.items():
|
||||
flag = "--" + flag.replace("_", "-")
|
||||
# We handle boolean flags here. The type of value must be `bool`, and
|
||||
@@ -47,15 +48,9 @@ def execute_test(**kwargs):
|
||||
else:
|
||||
server_args.extend([flag, value])
|
||||
|
||||
client_args = [
|
||||
client_binary,
|
||||
"--interval",
|
||||
interval,
|
||||
"--duration",
|
||||
duration,
|
||||
"--storage-directory",
|
||||
storage_directory,
|
||||
]
|
||||
client_args = [client_binary, "--interval", interval,
|
||||
"--duration", duration,
|
||||
"--storage-directory", storage_directory]
|
||||
if endpoint:
|
||||
client_args.extend(["--endpoint", endpoint])
|
||||
|
||||
@@ -66,7 +61,8 @@ def execute_test(**kwargs):
|
||||
assert server.poll() is None, "Server process died prematurely!"
|
||||
|
||||
try:
|
||||
subprocess.run(list(map(str, client_args)), timeout=timeout, check=True)
|
||||
subprocess.run(list(map(str, client_args)), timeout=timeout,
|
||||
check=True)
|
||||
finally:
|
||||
if server is None:
|
||||
success = True
|
||||
@@ -92,14 +88,16 @@ TESTS = [
|
||||
{"endpoint": "http://127.0.0.1:9000/nonexistant/", "no_check": True},
|
||||
{"start_server": False},
|
||||
{"startups": 4, "no_check_duration": True}, # the last 3 tests failed
|
||||
# to send any data + this test
|
||||
{"add_garbage": True},
|
||||
# to send any data + this test
|
||||
{"add_garbage": True}
|
||||
]
|
||||
|
||||
if __name__ == "__main__":
|
||||
server_binary = os.path.join(SCRIPT_DIR, "server.py")
|
||||
client_binary = os.path.join(PROJECT_DIR, "build", "tests", "integration", "telemetry", "client")
|
||||
kvstore_console_binary = os.path.join(PROJECT_DIR, "build", "tests", "manual", "kvstore_console")
|
||||
client_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"integration", "telemetry", "client")
|
||||
kvstore_console_binary = os.path.join(PROJECT_DIR, "build", "tests",
|
||||
"manual", "kvstore_console")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--client", default=client_binary)
|
||||
@@ -110,23 +108,19 @@ if __name__ == "__main__":
|
||||
storage = tempfile.TemporaryDirectory()
|
||||
|
||||
for test in TESTS:
|
||||
print(
|
||||
"\033[1;36m~~ Executing test with arguments:",
|
||||
json.dumps(test, sort_keys=True),
|
||||
"~~\033[0m",
|
||||
)
|
||||
print("\033[1;36m~~ Executing test with arguments:",
|
||||
json.dumps(test, sort_keys=True), "~~\033[0m")
|
||||
|
||||
if test.pop("add_garbage", False):
|
||||
proc = subprocess.Popen(
|
||||
[args.kvstore_console, "--path", storage.name],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
)
|
||||
proc = subprocess.Popen([args.kvstore_console, "--path",
|
||||
storage.name], stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL)
|
||||
proc.communicate("put garbage garbage".encode("utf-8"))
|
||||
assert proc.wait() == 0
|
||||
|
||||
try:
|
||||
success = execute_test(client=args.client, server=args.server, storage=storage.name, **test)
|
||||
success = execute_test(client=args.client, server=args.server,
|
||||
storage=storage.name, **test)
|
||||
except Exception as e:
|
||||
print("\033[1;33m", e, "\033[0m", sep="")
|
||||
success = False
|
||||
|
||||
@@ -46,7 +46,7 @@ def build_handler(storage, args):
|
||||
assert self.headers["accept"] == "application/json"
|
||||
assert self.headers["content-type"] == "application/json"
|
||||
|
||||
content_len = int(self.headers.get("content-length", 0))
|
||||
content_len = int(self.headers.get('content-length', 0))
|
||||
data = json.loads(self.rfile.read(content_len).decode("utf-8"))
|
||||
|
||||
if self.path not in [args.path, args.redirect_path]:
|
||||
@@ -195,4 +195,4 @@ if __name__ == "__main__":
|
||||
verify_storage(startup, args)
|
||||
|
||||
# machine id has to be same for every run on the same machine
|
||||
assert len(set(map(lambda x: x["machine_id"], itertools.chain(*startups)))) == 1
|
||||
assert len(set(map(lambda x: x['machine_id'], itertools.chain(*startups)))) == 1
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
"n3" {:replication-role :replica :replication-mode :async :port 10000}}
|
||||
{"n1" {:replication-role :main}
|
||||
"n2" {:replication-role :replica :replication-mode :async :port 10000}
|
||||
"n3" {:replication-role :replica :replication-mode :sync :port 10000 :timeout 3}}]
|
||||
"n3" {:replication-role :replica :replication-mode :sync :port 10000}}]
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
[node-config]
|
||||
(case (:replication-mode node-config)
|
||||
:async "ASYNC"
|
||||
:sync (str "SYNC" (when-let [timeout (:timeout node-config)] (str " WITH TIMEOUT " timeout)))))
|
||||
:sync "SYNC" ))
|
||||
|
||||
(defn create-register-replica-query
|
||||
[name node-config]
|
||||
|
||||
@@ -129,13 +129,7 @@
|
||||
:replication-mode
|
||||
(str "Invalid node configuration. "
|
||||
"Every replication node requires "
|
||||
":replication-mode to be defined."))
|
||||
(throw-if-key-missing-in-any
|
||||
(filter #(= (:replication-mode %) :sync) replica-nodes-configs)
|
||||
:timeout
|
||||
(str "Invalid node confiruation. "
|
||||
"Every SYNC replication node requires "
|
||||
":timeout to be defined."))))
|
||||
":replication-mode to be defined."))))
|
||||
|
||||
(map (fn [node-config] (resolve-all-node-hostnames
|
||||
(merge
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
:--storage-recover-on-startup
|
||||
:--storage-wal-enabled
|
||||
:--storage-snapshot-interval-sec 300
|
||||
:--storage-properties-on-edges))
|
||||
:--storage-properties-on-edges
|
||||
:--storage-restore-replicas-on-startup false))
|
||||
|
||||
(defn stop-node!
|
||||
[test node]
|
||||
|
||||
@@ -34,8 +34,7 @@ class QueryClient:
|
||||
self.default_num_workers = default_num_workers
|
||||
|
||||
def __call__(self, queries, database, num_workers=None):
|
||||
if num_workers is None:
|
||||
num_workers = self.default_num_workers
|
||||
if num_workers is None: num_workers = self.default_num_workers
|
||||
self.log.debug("execute('%s')", str(queries))
|
||||
|
||||
client_path = "tests/macro_benchmark/query_client"
|
||||
@@ -54,36 +53,29 @@ class QueryClient:
|
||||
output_fd, output = tempfile.mkstemp()
|
||||
os.close(output_fd)
|
||||
|
||||
client_args = [
|
||||
"--port",
|
||||
database.args.port,
|
||||
"--num-workers",
|
||||
str(num_workers),
|
||||
"--output",
|
||||
output,
|
||||
]
|
||||
client_args = ["--port", database.args.port,
|
||||
"--num-workers", str(num_workers),
|
||||
"--output", output]
|
||||
|
||||
cpu_time_start = database.database_bin.get_usage()["cpu"]
|
||||
# TODO make the timeout configurable per query or something
|
||||
return_code = self.client.run_and_wait(client, client_args, timeout=600, stdin=queries_path)
|
||||
return_code = self.client.run_and_wait(
|
||||
client, client_args, timeout=600, stdin=queries_path)
|
||||
usage = database.database_bin.get_usage()
|
||||
cpu_time_end = usage["cpu"]
|
||||
os.remove(queries_path)
|
||||
if return_code != 0:
|
||||
with open(self.client.get_stderr()) as f:
|
||||
stderr = f.read()
|
||||
self.log.error(
|
||||
"Error while executing queries '%s'. " "Failed with return_code %d and stderr:\n%s",
|
||||
str(queries),
|
||||
return_code,
|
||||
stderr,
|
||||
)
|
||||
self.log.error("Error while executing queries '%s'. "
|
||||
"Failed with return_code %d and stderr:\n%s",
|
||||
str(queries), return_code, stderr)
|
||||
raise Exception("BoltClient execution failed")
|
||||
|
||||
data = {"groups": []}
|
||||
data = {"groups" : []}
|
||||
with open(output) as f:
|
||||
for line in f:
|
||||
data["groups"].append(json.loads(line))
|
||||
data["groups"].append(json.loads(line))
|
||||
data[CPU_TIME] = cpu_time_end - cpu_time_start
|
||||
data[MAX_MEMORY] = usage["max_memory"]
|
||||
|
||||
@@ -102,8 +94,7 @@ class LongRunningClient:
|
||||
# TODO: This is quite similar to __call__ method of QueryClient. Remove
|
||||
# duplication.
|
||||
def __call__(self, config, database, duration, client, num_workers=None):
|
||||
if num_workers is None:
|
||||
num_workers = self.default_num_workers
|
||||
if num_workers is None: num_workers = self.default_num_workers
|
||||
self.log.debug("execute('%s')", config)
|
||||
|
||||
client_path = "tests/macro_benchmark/{}".format(client)
|
||||
@@ -122,41 +113,32 @@ class LongRunningClient:
|
||||
output_fd, output = tempfile.mkstemp()
|
||||
os.close(output_fd)
|
||||
|
||||
client_args = [
|
||||
"--port",
|
||||
database.args.port,
|
||||
"--num-workers",
|
||||
str(num_workers),
|
||||
"--output",
|
||||
output,
|
||||
"--duration",
|
||||
str(duration),
|
||||
"--db",
|
||||
database.name,
|
||||
"--scenario",
|
||||
self.workload,
|
||||
]
|
||||
client_args = ["--port", database.args.port,
|
||||
"--num-workers", str(num_workers),
|
||||
"--output", output,
|
||||
"--duration", str(duration),
|
||||
"--db", database.name,
|
||||
"--scenario", self.workload]
|
||||
|
||||
return_code = self.client.run_and_wait(client, client_args, timeout=600, stdin=config_path)
|
||||
return_code = self.client.run_and_wait(
|
||||
client, client_args, timeout=600, stdin=config_path)
|
||||
os.remove(config_path)
|
||||
if return_code != 0:
|
||||
with open(self.client.get_stderr()) as f:
|
||||
stderr = f.read()
|
||||
self.log.error(
|
||||
"Error while executing config '%s'. " "Failed with return_code %d and stderr:\n%s",
|
||||
str(config),
|
||||
return_code,
|
||||
stderr,
|
||||
)
|
||||
self.log.error("Error while executing config '%s'. "
|
||||
"Failed with return_code %d and stderr:\n%s",
|
||||
str(config), return_code, stderr)
|
||||
raise Exception("BoltClient execution failed")
|
||||
|
||||
|
||||
# TODO: We shouldn't wait for process to finish to start reading output.
|
||||
# We should implement periodic reading of data and stream data when it
|
||||
# becomes available.
|
||||
data = []
|
||||
with open(output) as f:
|
||||
for line in f:
|
||||
data.append(json.loads(line))
|
||||
data.append(json.loads(line))
|
||||
|
||||
os.remove(output)
|
||||
return data
|
||||
|
||||
@@ -14,11 +14,9 @@ from argparse import ArgumentParser
|
||||
|
||||
try:
|
||||
import jail
|
||||
|
||||
APOLLO = True
|
||||
except:
|
||||
import jail_faker as jail
|
||||
|
||||
APOLLO = False
|
||||
|
||||
|
||||
@@ -47,15 +45,13 @@ def get_absolute_path(path, base=""):
|
||||
def set_cpus(flag_name, process, args):
|
||||
argp = ArgumentParser()
|
||||
# named, optional arguments
|
||||
argp.add_argument(
|
||||
"--" + flag_name,
|
||||
nargs="+",
|
||||
type=int,
|
||||
help="cpus that " "will be used by process. Obligatory on Apollo, ignored " "otherwise.",
|
||||
)
|
||||
argp.add_argument("--" + flag_name, nargs="+", type=int, help="cpus that "
|
||||
"will be used by process. Obligatory on Apollo, ignored "
|
||||
"otherwise.")
|
||||
args, _ = argp.parse_known_args(args)
|
||||
attr_flag_name = flag_name.replace("-", "_")
|
||||
cpus = getattr(args, attr_flag_name)
|
||||
assert not APOLLO or cpus, "flag --{} is obligatory on Apollo".format(flag_name)
|
||||
assert not APOLLO or cpus, \
|
||||
"flag --{} is obligatory on Apollo".format(flag_name)
|
||||
if cpus:
|
||||
process.set_cpus(cpus, hyper=False)
|
||||
process.set_cpus(cpus, hyper = False)
|
||||
|
||||
@@ -36,12 +36,13 @@ class Memgraph:
|
||||
"""
|
||||
Knows how to start and stop memgraph.
|
||||
"""
|
||||
|
||||
def __init__(self, args, num_workers):
|
||||
self.log = logging.getLogger("MemgraphRunner")
|
||||
argp = ArgumentParser("MemgraphArgumentParser")
|
||||
argp.add_argument("--runner-bin", default=get_absolute_path("memgraph", "build"))
|
||||
argp.add_argument("--port", default="7687", help="Database and client port")
|
||||
argp.add_argument("--runner-bin",
|
||||
default=get_absolute_path("memgraph", "build"))
|
||||
argp.add_argument("--port", default="7687",
|
||||
help="Database and client port")
|
||||
argp.add_argument("--data-directory", default=None)
|
||||
argp.add_argument("--storage-snapshot-on-exit", action="store_true")
|
||||
argp.add_argument("--storage-recover-on-startup", action="store_true")
|
||||
@@ -54,12 +55,8 @@ class Memgraph:
|
||||
|
||||
def start(self):
|
||||
self.log.info("start")
|
||||
database_args = [
|
||||
"--bolt-port",
|
||||
self.args.port,
|
||||
"--query-execution-timeout-sec",
|
||||
"0",
|
||||
]
|
||||
database_args = ["--bolt-port", self.args.port,
|
||||
"--query-execution-timeout-sec", "0"]
|
||||
if self.num_workers:
|
||||
database_args += ["--bolt-num-workers", str(self.num_workers)]
|
||||
if self.args.data_directory:
|
||||
@@ -85,13 +82,15 @@ class Neo:
|
||||
"""
|
||||
Knows how to start and stop neo4j.
|
||||
"""
|
||||
|
||||
def __init__(self, args, config):
|
||||
self.log = logging.getLogger("NeoRunner")
|
||||
argp = ArgumentParser("NeoArgumentParser")
|
||||
argp.add_argument("--runner-bin", default=get_absolute_path("neo4j/bin/neo4j", "libs"))
|
||||
argp.add_argument("--port", default="7687", help="Database and client port")
|
||||
argp.add_argument("--http-port", default="7474", help="Database and client port")
|
||||
argp.add_argument("--runner-bin", default=get_absolute_path(
|
||||
"neo4j/bin/neo4j", "libs"))
|
||||
argp.add_argument("--port", default="7687",
|
||||
help="Database and client port")
|
||||
argp.add_argument("--http-port", default="7474",
|
||||
help="Database and client port")
|
||||
self.log.info("Initializing Runner with arguments %r", args)
|
||||
self.args, _ = argp.parse_known_args(args)
|
||||
self.config = config
|
||||
@@ -106,23 +105,24 @@ class Neo:
|
||||
self.neo4j_home_path = tempfile.mkdtemp(dir="/dev/shm")
|
||||
|
||||
try:
|
||||
os.symlink(
|
||||
os.path.join(get_absolute_path("neo4j", "libs"), "lib"),
|
||||
os.path.join(self.neo4j_home_path, "lib"),
|
||||
)
|
||||
os.symlink(os.path.join(get_absolute_path("neo4j", "libs"), "lib"),
|
||||
os.path.join(self.neo4j_home_path, "lib"))
|
||||
neo4j_conf_dir = os.path.join(self.neo4j_home_path, "conf")
|
||||
neo4j_conf_file = os.path.join(neo4j_conf_dir, "neo4j.conf")
|
||||
os.mkdir(neo4j_conf_dir)
|
||||
shutil.copyfile(self.config, neo4j_conf_file)
|
||||
with open(neo4j_conf_file, "a") as f:
|
||||
f.write("\ndbms.connector.bolt.listen_address=:" + self.args.port + "\n")
|
||||
f.write("\ndbms.connector.http.listen_address=:" + self.args.http_port + "\n")
|
||||
f.write("\ndbms.connector.bolt.listen_address=:" +
|
||||
self.args.port + "\n")
|
||||
f.write("\ndbms.connector.http.listen_address=:" +
|
||||
self.args.http_port + "\n")
|
||||
|
||||
# environment
|
||||
cwd = os.path.dirname(self.args.runner_bin)
|
||||
env = {"NEO4J_HOME": self.neo4j_home_path}
|
||||
|
||||
self.database_bin.run(self.args.runner_bin, args=["console"], env=env, timeout=600, cwd=cwd)
|
||||
self.database_bin.run(self.args.runner_bin, args=["console"],
|
||||
env=env, timeout=600, cwd=cwd)
|
||||
except:
|
||||
shutil.rmtree(self.neo4j_home_path)
|
||||
raise Exception("Couldn't run Neo4j!")
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user