diff --git a/Cargo.toml b/Cargo.toml index eb590fb89d..0138c07e9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,9 +42,9 @@ debug = 0 pometry-storage = { version = ">=0.8.1", path = "pometry-storage" } #[private-storage] # pometry-storage = { path = "pometry-storage-private", package = "pometry-storage-private" } -async-graphql = { version = "7.0.13", features = ["dynamic-schema"] } +async-graphql = { version = "7.0.16", features = ["dynamic-schema"] } bincode = "1.3.3" -async-graphql-poem = "7.0.13" +async-graphql-poem = "7.0.16" dynamic-graphql = "0.10.1" reqwest = { version = "0.12.8", default-features = false, features = [ "rustls-tls", diff --git a/python/python/raphtory/__init__.pyi b/python/python/raphtory/__init__.pyi index 2d2b6d1537..c5b45171ca 100644 --- a/python/python/raphtory/__init__.pyi +++ b/python/python/raphtory/__init__.pyi @@ -113,6 +113,14 @@ class GraphView(object): def create_index(self): """Create graph index""" + def create_index_in_ram(self): + """ + Creates a graph index in memory (RAM). + + This is primarily intended for use in tests and should not be used in production environments, + as the index will not be persisted to disk. + """ + def default_layer(self) -> GraphView: """ Return a view of GraphView containing only the default edge layer @@ -257,18 +265,6 @@ class GraphView(object): GraphView: The filtered view """ - def filter_exploded_edges(self, filter: PropertyFilter) -> GraphView: - """ - Return a filtered view that only includes exploded edges that satisfy the filter - - Arguments: - filter (PropertyFilter): The filter to apply to the exploded edge properties. Construct a - filter using `Prop`. - - Returns: - GraphView: The filtered view - """ - def filter_nodes(self, filter: PropertyFilter) -> GraphView: """ Return a filtered view that only includes nodes that satisfy the filter @@ -2323,18 +2319,6 @@ class Node(object): Node: The filtered view """ - def filter_exploded_edges(self, filter: PropertyFilter) -> Node: - """ - Return a filtered view that only includes exploded edges that satisfy the filter - - Arguments: - filter (PropertyFilter): The filter to apply to the exploded edge properties. Construct a - filter using `Prop`. - - Returns: - Node: The filtered view - """ - def filter_nodes(self, filter: PropertyFilter) -> Node: """ Return a filtered view that only includes nodes that satisfy the filter @@ -2865,18 +2849,6 @@ class Nodes(object): Nodes: The filtered view """ - def filter_exploded_edges(self, filter: PropertyFilter) -> Nodes: - """ - Return a filtered view that only includes exploded edges that satisfy the filter - - Arguments: - filter (PropertyFilter): The filter to apply to the exploded edge properties. Construct a - filter using `Prop`. - - Returns: - Nodes: The filtered view - """ - def filter_nodes(self, filter: PropertyFilter) -> Nodes: """ Return a filtered view that only includes nodes that satisfy the filter @@ -3387,18 +3359,6 @@ class PathFromNode(object): PathFromNode: The filtered view """ - def filter_exploded_edges(self, filter: PropertyFilter) -> PathFromNode: - """ - Return a filtered view that only includes exploded edges that satisfy the filter - - Arguments: - filter (PropertyFilter): The filter to apply to the exploded edge properties. Construct a - filter using `Prop`. - - Returns: - PathFromNode: The filtered view - """ - def filter_nodes(self, filter: PropertyFilter) -> PathFromNode: """ Return a filtered view that only includes nodes that satisfy the filter @@ -3832,18 +3792,6 @@ class PathFromGraph(object): PathFromGraph: The filtered view """ - def filter_exploded_edges(self, filter: PropertyFilter) -> PathFromGraph: - """ - Return a filtered view that only includes exploded edges that satisfy the filter - - Arguments: - filter (PropertyFilter): The filter to apply to the exploded edge properties. Construct a - filter using `Prop`. - - Returns: - PathFromGraph: The filtered view - """ - def filter_nodes(self, filter: PropertyFilter) -> PathFromGraph: """ Return a filtered view that only includes nodes that satisfy the filter @@ -6029,7 +5977,15 @@ class Prop(object): def __new__(cls, name: str) -> Prop: """Create and return a new object. See help(type) for accurate signature.""" - def any(self, values: set[PropValue]) -> PropertyFilter: + def contains(self, value) -> PropertyFilter: + """ + Create a filter that keeps entities that contains the property + + Returns: + PropertyFilter: the property filter + """ + + def is_in(self, values: set[PropValue]) -> PropertyFilter: """ Create a filter that keeps entities if their property value is in the set @@ -6048,6 +6004,18 @@ class Prop(object): PropertyFilter: the property filter """ + def is_not_in(self, values: set[PropValue]) -> PropertyFilter: + """ + Create a filter that keeps entities if their property value is not in the set or + if they don't have the property + + Arguments: + values (set[PropValue]): the set of values to exclude + + Returns: + PropertyFilter: the property filter + """ + def is_some(self) -> PropertyFilter: """ Create a filter that only keeps entities if they have the property @@ -6056,13 +6024,9 @@ class Prop(object): PropertyFilter: the property filter """ - def not_any(self, values: set[PropValue]) -> PropertyFilter: + def not_contains(self, value) -> PropertyFilter: """ - Create a filter that keeps entities if their property value is not in the set or - if they don't have the property - - Arguments: - values (set[PropValue]): the set of values to exclude + Create a filter that keeps entities that do not contain the property Returns: PropertyFilter: the property filter diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 7732edcbef..ae59c92842 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -19,3 +19,5 @@ from pandas import DataFrame from os import PathLike import networkx as nx # type: ignore import pyvis # type: ignore + +def Property(name): ... diff --git a/python/python/raphtory/node_state/__init__.pyi b/python/python/raphtory/node_state/__init__.pyi index f2e7589610..12d1d8743d 100644 --- a/python/python/raphtory/node_state/__init__.pyi +++ b/python/python/raphtory/node_state/__init__.pyi @@ -6847,3 +6847,91 @@ class NodeLayout(object): Returns: Iterator[list[float]]: Iterator over values """ + +class NodeStateF64String(object): + def __eq__(self, value): + """Return self==value.""" + + def __ge__(self, value): + """Return self>=value.""" + + def __getitem__(self, key): + """Return self[key].""" + + def __gt__(self, value): + """Return self>value.""" + + def __iter__(self): + """Implement iter(self).""" + + def __le__(self, value): + """Return self<=value.""" + + def __len__(self): + """Return len(self).""" + + def __lt__(self, value): + """Return self Optional[Tuple[float, str]]: + """ + Get value for node + + Arguments: + node (NodeInput): the node + default (Optional[Tuple[float, str]]): the default value. Defaults to None. + + Returns: + Optional[Tuple[float, str]]: the value for the node or the default value + """ + + def items(self) -> Iterator[Tuple[Node, Tuple[float, str]]]: + """ + Iterate over items + + Returns: + Iterator[Tuple[Node, Tuple[float, str]]]: Iterator over items + """ + + def nodes(self) -> Nodes: + """ + Iterate over nodes + + Returns: + Nodes: The nodes + """ + + def sorted_by_id(self) -> NodeStateF64String: + """ + Sort results by node id + + Returns: + NodeStateF64String: The sorted node state + """ + + def to_df(self) -> DataFrame: + """ + Convert results to pandas DataFrame + + The DataFrame has two columns, "node" with the node ids and "value" with + the corresponding values. + + Returns: + DataFrame: the pandas DataFrame + """ + + def values(self) -> Iterator[Tuple[float, str]]: + """ + Iterate over values + + Returns: + Iterator[Tuple[float, str]]: Iterator over values + """ diff --git a/python/test_utils/filters_setup.py b/python/test_utils/filters_setup.py index 34df62fe73..6784785aac 100644 --- a/python/test_utils/filters_setup.py +++ b/python/test_utils/filters_setup.py @@ -10,22 +10,28 @@ def func(graph): for initializer in initializers: graph = initializer(graph) return graph + return func def init_graph(graph): nodes = [ - (1, 1, {"p1": "shivam_kapoor", "p9": 5, "p10": "Paper_airplane"}, "fire_nation"), - (2, 2, {"p1": "prop12", "p2": 2, "p10": "Paper_ship"}, "air_nomads"), - (3, 1, {"p1": "shivam_kapoor", "p9": 5}, "fire_nation"), - (3, 3, {"p2": 6, "p3": 1, "p10": "Paper_airplane"}, "fire_nation"), - (4, 1, {"p1": "shivam_kapoor", "p9": 5}, "fire_nation"), - (3, 4, {"p4": "pometry"}, None), - (4, 4, {"p5": 12}, None), + ( + 1, + 1, + {"p1": "shivam_kapoor", "p9": 5, "p10": "Paper_airplane"}, + "fire_nation", + ), + (2, 2, {"p1": "prop12", "p2": 2, "p10": "Paper_ship"}, "air_nomads"), + (3, 1, {"p1": "shivam_kapoor", "p9": 5}, "fire_nation"), + (3, 3, {"p2": 6, "p3": 1, "p10": "Paper_airplane"}, "fire_nation"), + (4, 1, {"p1": "shivam_kapoor", "p9": 5}, "fire_nation"), + (3, 4, {"p4": "pometry"}, None), + (4, 4, {"p5": 12}, None), ] for time, id, props, node_type in nodes: - graph.add_node(time, str(id), props, node_type) + graph.add_node(time, str(id), props, node_type) edge_data = [ (1, "1", "2", {"p1": "shivam_kapoor", "p10": "Paper_airplane"}, "fire_nation"), @@ -42,6 +48,7 @@ def init_graph(graph): return graph + def init_nodes_graph(graph): nodes = [ (6, "N1", {"p1": 2}), @@ -137,6 +144,7 @@ def init_nodes_graph(graph): return graph + # For this graph there won't be any temporal property index for property name "p1". def init_nodes_graph1(graph): nodes = [ @@ -157,17 +165,15 @@ def init_nodes_graph1(graph): return graph + # For this graph there won't be any constant property index for property name "p1". def init_nodes_graph2(graph): nodes = [ (1, "N1", {"p1": 1}), - (2, "N2", {"p1": 1}), (3, "N2", {"p1": 2}), - (2, "N3", {"p1": 2}), (3, "N3", {"p1": 1}), - (2, "N4", {}), ] @@ -259,13 +265,13 @@ def init_edges_graph1(graph): # For this graph there won't be any constant property index for property name "p1". def init_edges_graph2(graph): edges = [ - (2, "N1", "N2", {"p1": 1}), - (2, "N2", "N3", {"p1": 1}), - (2, "N2", "N3", {"p1": 2}), - (2, "N3", "N4", {"p1": 2}), - (2, "N3", "N4", {"p1": 1}), - (2, "N4", "N5", {}), - ] + (2, "N1", "N2", {"p1": 1}), + (2, "N2", "N3", {"p1": 1}), + (2, "N2", "N3", {"p1": 2}), + (2, "N3", "N4", {"p1": 2}), + (2, "N3", "N4", {"p1": 1}), + (2, "N4", "N5", {}), + ] for time, src, dst, props in edges: graph.add_edge(time, src, dst, props) diff --git a/python/test_utils/utils.py b/python/test_utils/utils.py index b9e77dcd19..aa725d224b 100644 --- a/python/test_utils/utils.py +++ b/python/test_utils/utils.py @@ -15,6 +15,7 @@ if "DISK_TEST_MARK" in os.environ: + def with_disk_graph(func): def inner(graph): def inner2(graph, tmpdirname): @@ -28,19 +29,27 @@ def inner2(graph, tmpdirname): return inner else: + def with_disk_graph(func): return func def with_disk_variants(init_fn, variants=None): if variants is None: - variants = ["graph", "persistent_graph", "event_disk_graph", "persistent_disk_graph"] + variants = [ + "graph", + "persistent_graph", + "event_disk_graph", + "persistent_disk_graph", + ] def decorator(func): @wraps(func) def wrapper(): check = func() - assert callable(check), f"Expected test function to return a callable, got {type(check)}" + assert callable( + check + ), f"Expected test function to return a callable, got {type(check)}" if "graph" in variants: g = init_fn(Graph()) @@ -54,7 +63,10 @@ def wrapper(): from raphtory import DiskGraphStorage with tempfile.TemporaryDirectory() as tmpdir: - if "event_disk_graph" in variants or "persistent_disk_graph" in variants: + if ( + "event_disk_graph" in variants + or "persistent_disk_graph" in variants + ): g = init_fn(Graph()) g.to_disk_graph(tmpdir) disk = DiskGraphStorage.load_from_dir(tmpdir) @@ -67,6 +79,7 @@ def wrapper(): del disk return wrapper + return decorator @@ -120,3 +133,20 @@ def run_graphql_error_test(query, expected_error_message, graph): assert ( error_message == expected_error_message ), f"Expected '{expected_error_message}', but got '{error_message}'" + + +def run_group_graphql_error_test(queries_and_expected_error_messages, graph): + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir).start(PORT) as server: + client = server.get_client() + client.send_graph(path="g", graph=graph) + for query, expected_error_message in queries_and_expected_error_messages: + with pytest.raises(Exception) as excinfo: + client.query(query) + + full_error_message = str(excinfo.value) + match = re.search(r'"message":"(.*?)"', full_error_message) + error_message = match.group(1) if match else "" + assert ( + error_message == expected_error_message + ), f"Expected '{expected_error_message}', but got '{error_message}'" diff --git a/python/tests/test_auth.py b/python/tests/test_auth.py index 19eb6188fa..5231aae9d2 100644 --- a/python/tests/test_auth.py +++ b/python/tests/test_auth.py @@ -31,11 +31,10 @@ NEW_TEST_GRAPH = """mutation { newGraph(path:"test", graphType:EVENT) }""" -QUERY_GRAPHS = """query { graphs { name } }""" -QUERY_NAMEPSACES = """query { namespaces { path } }""" -QUERY_ROOT = """query { root { graphs { path } } }""" +QUERY_NAMESPACES = """query { namespaces { list{ path} } }""" +QUERY_ROOT = """query { root { graphs { list{ path }} } }""" QUERY_GRAPH = """query { graph(path: "test") { path } }""" -TEST_QUERIES = [QUERY_GRAPHS, QUERY_NAMEPSACES, QUERY_ROOT, QUERY_GRAPH] +TEST_QUERIES = [QUERY_NAMESPACES, QUERY_GRAPH, QUERY_ROOT] def assert_successful_response(response: requests.Response): @@ -60,7 +59,7 @@ def test_expired_token(): "Authorization": f"Bearer {token}", } response = requests.post( - RAPHTORY, headers=headers, data=json.dumps({"query": QUERY_GRAPHS}) + RAPHTORY, headers=headers, data=json.dumps({"query": QUERY_ROOT}) ) assert response.status_code == 401 @@ -69,7 +68,7 @@ def test_expired_token(): "Authorization": f"Bearer {token}", } response = requests.post( - RAPHTORY, headers=headers, data=json.dumps({"query": QUERY_GRAPHS}) + RAPHTORY, headers=headers, data=json.dumps({"query": QUERY_ROOT}) ) assert response.status_code == 401 diff --git a/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py b/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py index c26faa8880..e228f71714 100644 --- a/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py +++ b/python/tests/test_base_install/test_filters/semantics/test_edge_property_filter_semantics.py @@ -1,10 +1,16 @@ from raphtory import filter, Prop import pytest -from filters_setup import init_edges_graph, init_edges_graph1, init_edges_graph2, combined +from filters_setup import ( + init_edges_graph, + init_edges_graph1, + init_edges_graph2, + combined, +) from utils import with_disk_variants # TODO: PropertyFilteringNotImplemented for variants persistent_graph for filter_edges. + def init_graph_for_secondary_indexes(graph): edges = [ (1, "N16", "N15", {"p1": 2}), @@ -25,11 +31,18 @@ def test_constant_semantics(): def check(graph): filter_expr = filter.Property("p1").constant() == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("N1","N2"), ("N10","N11"), ("N11","N12"), - ("N12","N13"), ("N13","N14"), ("N14","N15"), - ("N15","N1"), ("N9","N10") - ]) + expected_ids = sorted( + [ + ("N1", "N2"), + ("N10", "N11"), + ("N11", "N12"), + ("N12", "N13"), + ("N13", "N14"), + ("N14", "N15"), + ("N15", "N1"), + ("N9", "N10"), + ] + ) assert result_ids == expected_ids return check @@ -40,11 +53,18 @@ def test_temporal_any_semantics(): def check(graph): filter_expr = filter.Property("p1").temporal().any() == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("N1","N2"), ("N2","N3"), ("N3","N4"), - ("N4","N5"), ("N5","N6"), ("N6","N7"), - ("N7","N8"),("N8","N9") - ]) + expected_ids = sorted( + [ + ("N1", "N2"), + ("N2", "N3"), + ("N3", "N4"), + ("N4", "N5"), + ("N5", "N6"), + ("N6", "N7"), + ("N7", "N8"), + ("N8", "N9"), + ] + ) assert result_ids == expected_ids return check @@ -52,17 +72,26 @@ def check(graph): @with_disk_variants( init_fn=combined([init_edges_graph, init_graph_for_secondary_indexes]), - variants=["graph", "event_disk_graph"] + variants=["graph", "event_disk_graph"], ) def test_temporal_any_semantics_for_secondary_indexes(): def check(graph): filter_expr = filter.Property("p1").temporal().any() == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("N1", "N2"), ("N16", "N15"), ("N17", "N16"), ("N2", "N3"), - ("N3", "N4"), ("N4", "N5"), ("N5", "N6"), ("N6", "N7"), - ("N7", "N8"), ("N8", "N9") - ]) + expected_ids = sorted( + [ + ("N1", "N2"), + ("N16", "N15"), + ("N17", "N16"), + ("N2", "N3"), + ("N3", "N4"), + ("N4", "N5"), + ("N5", "N6"), + ("N6", "N7"), + ("N7", "N8"), + ("N8", "N9"), + ] + ) assert result_ids == expected_ids return check @@ -73,7 +102,9 @@ def test_temporal_latest_semantics(): def check(graph): filter_expr = filter.Property("p1").temporal().latest() == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("N1","N2"), ("N3","N4"), ("N4","N5"), ("N6","N7"), ("N7","N8")]) + expected_ids = sorted( + [("N1", "N2"), ("N3", "N4"), ("N4", "N5"), ("N6", "N7"), ("N7", "N8")] + ) assert result_ids == expected_ids return check @@ -81,16 +112,22 @@ def check(graph): @with_disk_variants( init_fn=combined([init_edges_graph, init_graph_for_secondary_indexes]), - variants=["graph", "event_disk_graph"] + variants=["graph", "event_disk_graph"], ) def test_temporal_latest_semantics_for_secondary_indexes3(): def check(graph): filter_expr = filter.Property("p1").temporal().latest() == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("N1","N2"), ("N16","N15"), ("N3","N4"), - ("N4","N5"), ("N6","N7"), ("N7","N8") - ]) + expected_ids = sorted( + [ + ("N1", "N2"), + ("N16", "N15"), + ("N3", "N4"), + ("N4", "N5"), + ("N6", "N7"), + ("N7", "N8"), + ] + ) assert result_ids == expected_ids return check @@ -101,10 +138,17 @@ def test_property_semantics(): def check(graph): filter_expr = filter.Property("p1") == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("N1","N2"), ("N14","N15"), ("N15","N1"), - ("N3","N4"), ("N4","N5"), ("N6","N7"), ("N7","N8") - ]) + expected_ids = sorted( + [ + ("N1", "N2"), + ("N14", "N15"), + ("N15", "N1"), + ("N3", "N4"), + ("N4", "N5"), + ("N6", "N7"), + ("N7", "N8"), + ] + ) assert result_ids == expected_ids return check @@ -116,7 +160,9 @@ def test_property_semantics2(): def check(graph): filter_expr = filter.Property("p1") == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("N1","N2"), ("N3","N4"), ("N4","N5"), ("N6","N7"), ("N7","N8")]) + expected_ids = sorted( + [("N1", "N2"), ("N3", "N4"), ("N4", "N5"), ("N6", "N7"), ("N7", "N8")] + ) assert result_ids == expected_ids return check @@ -124,17 +170,24 @@ def check(graph): @with_disk_variants( init_fn=combined([init_edges_graph, init_graph_for_secondary_indexes]), - variants=["graph"] + variants=["graph"], ) def test_property_semantics_for_secondary_indexes(): def check(graph): filter_expr = filter.Property("p1") == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("N1","N2"), ("N14","N15"), ("N15","N1"), - ("N16","N15"), ("N3","N4"), ("N4","N5"), - ("N6","N7"), ("N7","N8") - ]) + expected_ids = sorted( + [ + ("N1", "N2"), + ("N14", "N15"), + ("N15", "N1"), + ("N16", "N15"), + ("N3", "N4"), + ("N4", "N5"), + ("N6", "N7"), + ("N7", "N8"), + ] + ) assert result_ids == expected_ids return check @@ -143,16 +196,22 @@ def check(graph): # TODO: Const properties not supported for disk_graph. @with_disk_variants( init_fn=combined([init_edges_graph, init_graph_for_secondary_indexes]), - variants=["event_disk_graph"] + variants=["event_disk_graph"], ) def test_property_semantics_for_secondary_indexes_dsg(): def check(graph): filter_expr = filter.Property("p1") == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("N1","N2"), ("N16","N15"), ("N3","N4"), ("N4","N5"), - ("N6","N7"), ("N7","N8") - ]) + expected_ids = sorted( + [ + ("N1", "N2"), + ("N16", "N15"), + ("N3", "N4"), + ("N4", "N5"), + ("N6", "N7"), + ("N7", "N8"), + ] + ) assert result_ids == expected_ids return check @@ -163,7 +222,7 @@ def test_property_semantics_only_constant(): def check(graph): filter_expr = filter.Property("p1") == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("N1","N2"), ("N2","N3")]) + expected_ids = sorted([("N1", "N2"), ("N2", "N3")]) assert result_ids == expected_ids return check @@ -186,8 +245,7 @@ def test_property_semantics_only_temporal(): def check(graph): filter_expr = filter.Property("p1") == 1 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("N1","N2"), ("N3","N4")]) + expected_ids = sorted([("N1", "N2"), ("N3", "N4")]) assert result_ids == expected_ids return check - diff --git a/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py b/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py index a34704ea26..187c442936 100644 --- a/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py +++ b/python/tests/test_base_install/test_filters/semantics/test_node_property_filter_semantics.py @@ -1,6 +1,11 @@ from raphtory import filter, Prop import pytest -from filters_setup import init_nodes_graph, init_nodes_graph1, init_nodes_graph2, combined +from filters_setup import ( + init_nodes_graph, + init_nodes_graph1, + init_nodes_graph2, + combined, +) from utils import with_disk_variants @@ -43,7 +48,9 @@ def test_temporal_any_semantics_for_secondary_indexes(): def check(graph): filter_expr = filter.Property("p1").temporal().any() == 1 result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = sorted(["N1", "N16", "N17", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]) + expected_ids = sorted( + ["N1", "N16", "N17", "N2", "N3", "N4", "N5", "N6", "N7", "N8"] + ) assert result_ids == expected_ids return check diff --git a/python/tests/test_base_install/test_filters/test_edge_composite_filter.py b/python/tests/test_base_install/test_filters/test_edge_composite_filter.py index 04a2b50b66..02658902c6 100644 --- a/python/tests/test_base_install/test_filters/test_edge_composite_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_composite_filter.py @@ -16,33 +16,46 @@ def check(graph): filter_expr1 = filter.Property("p2") > 2 filter_expr2 = filter.Property("p1") == "shivam_kapoor" result_ids = sorted(graph.filter_edges(filter_expr1 | filter_expr2).edges.id) - expected_ids = sorted([('1', '2'), ('2', '1'), ('3', '1'), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids - # TODO: Enable this test once string property is fixed for disk_storage_graph - # filter_expr1 = filter.Property("p2") < 9 - # filter_expr2 = filter.Property("p1") == "shivam_kapoor" - # result_ids = sorted(graph.filter_edges(filter_expr1 & filter_expr2).edges.id) - # expected_ids = [("1", "2")] - # assert result_ids == expected_ids + # TODO: Enable this test once string property is fixed for disk_storage_graph + # filter_expr1 = filter.Property("p2") < 9 + # filter_expr2 = filter.Property("p1") == "shivam_kapoor" + # result_ids = sorted(graph.filter_edges(filter_expr1 & filter_expr2).edges.id) + # expected_ids = [("1", "2")] + # assert result_ids == expected_ids filter_expr1 = filter.Property("p2") < 9 filter_expr2 = filter.Property("p3") < 9 result_ids = sorted(graph.filter_edges(filter_expr1 & filter_expr2).edges.id) - expected_ids = [("2", "1"), ("3", "1"), ("David Gilmour", "John Mayer"), ("John Mayer", "Jimmy Page")] + expected_ids = [ + ("2", "1"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] assert result_ids == expected_ids - # TODO: Enable this test once string property is fixed for disk_storage_graph - # filter_expr1 = filter.Edge.src().name() == "1" - # filter_expr2 = filter.Property("p1") == "shivam_kapoor" - # result_ids = sorted(graph.filter_edges(filter_expr1 & filter_expr2).edges.id) - # expected_ids = [("1", "2")] - # assert result_ids == expected_ids + # TODO: Enable this test once string property is fixed for disk_storage_graph + # filter_expr1 = filter.Edge.src().name() == "1" + # filter_expr2 = filter.Property("p1") == "shivam_kapoor" + # result_ids = sorted(graph.filter_edges(filter_expr1 & filter_expr2).edges.id) + # expected_ids = [("1", "2")] + # assert result_ids == expected_ids filter_expr1 = filter.Edge.dst().name() == "1" filter_expr2 = filter.Property("p2") <= 6 result_ids = sorted(graph.filter_edges(filter_expr1 & filter_expr2).edges.id) - expected_ids = sorted([('2', '1'), ('3', '1')]) + expected_ids = sorted([("2", "1"), ("3", "1")]) assert result_ids == expected_ids # TODO: Enable this test once string property is fixed for disk_storage_graph @@ -62,14 +75,20 @@ def check(graph): filter_expr1 = filter.Edge.dst().name() == "1" filter_expr2 = filter.Property("p2") <= 2 result_ids = sorted(graph.filter_edges(~filter_expr1 & filter_expr2).edges.id) - expected_ids = [('2', '3')] + expected_ids = [("2", "3")] assert result_ids == expected_ids filter_expr1 = filter.Edge.dst().name() == "1" filter_expr2 = filter.Property("p2") <= 6 result_ids = sorted(graph.filter_edges(~(filter_expr1 & filter_expr2)).edges.id) - expected_ids = sorted([('1', '2'), ('2', '3'), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "3"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check - diff --git a/python/tests/test_base_install/test_filters/test_edge_filter.py b/python/tests/test_base_install/test_filters/test_edge_filter.py index 2b5275a3d6..a298ff0b38 100644 --- a/python/tests/test_base_install/test_filters/test_edge_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_filter.py @@ -20,11 +20,15 @@ def test_filter_edges_for_src_ne(): def check(graph): filter_expr = filter.Edge.src().name() != "1" result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("2", "1"), ("2", "3"), ("3", "1"), - ("David Gilmour", "John Mayer"), - ("John Mayer", "Jimmy Page") - ]) + expected_ids = sorted( + [ + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -51,11 +55,15 @@ def test_filter_edges_for_src_not_in(): def check(graph): filter_expr = filter.Edge.src().name().is_not_in(["1"]) result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("2", "1"), ("2", "3"), ("3", "1"), - ("David Gilmour", "John Mayer"), - ("John Mayer", "Jimmy Page") - ]) + expected_ids = sorted( + [ + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -77,11 +85,15 @@ def test_filter_edges_for_dst_ne(): def check(graph): filter_expr = filter.Edge.dst().name() != "2" result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("2", "1"), ("2", "3"), ("3", "1"), - ("David Gilmour", "John Mayer"), - ("John Mayer", "Jimmy Page") - ]) + expected_ids = sorted( + [ + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -108,11 +120,14 @@ def test_filter_edges_for_dst_not_in(): def check(graph): filter_expr = filter.Edge.dst().name().is_not_in(["1"]) result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("1", "2"), ("2", "3"), - ("David Gilmour", "John Mayer"), - ("John Mayer", "Jimmy Page") - ]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "3"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -135,7 +150,7 @@ def test_filter_edges_for_src_contains(): def check(graph): filter_expr = filter.Edge.src().name().contains("Mayer") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([('John Mayer', 'Jimmy Page')]) + expected_ids = sorted([("John Mayer", "Jimmy Page")]) assert result_ids == expected_ids return check @@ -146,10 +161,15 @@ def test_filter_edges_for_src_not_contains(): def check(graph): filter_expr = filter.Edge.src().name().not_contains("Mayer") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([ - ("1", "2"), ("2", "1"), ("2", "3"), ("3", "1"), - ("David Gilmour", "John Mayer") - ]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ] + ) assert result_ids == expected_ids return check @@ -170,7 +190,7 @@ def check(graph): filter_expr = filter.Edge.dst().name().fuzzy_search("John May", 2, False) result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = [('David Gilmour', 'John Mayer')] + expected_ids = [("David Gilmour", "John Mayer")] assert result_ids == expected_ids return check @@ -181,8 +201,7 @@ def test_filter_edges_for_not_src(): def check(graph): filter_expr = filter.Edge.src().name().not_contains("Mayer") result_ids = sorted(graph.filter_edges(~filter_expr).edges.id) - expected_ids = [('John Mayer', 'Jimmy Page')] + expected_ids = [("John Mayer", "Jimmy Page")] assert result_ids == expected_ids return check - diff --git a/python/tests/test_base_install/test_filters/test_edge_property_filter.py b/python/tests/test_base_install/test_filters/test_edge_property_filter.py index 8b51072cf7..d210ea7b1f 100644 --- a/python/tests/test_base_install/test_filters/test_edge_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_property_filter.py @@ -20,7 +20,15 @@ def test_filter_edges_for_property_ne(): def check(graph): filter_expr = filter.Property("p2") != 2 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("1", "2"), ("2", "1"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -31,7 +39,16 @@ def test_filter_edges_for_property_lt(): def check(graph): filter_expr = filter.Property("p2") < 10 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("1", "2"), ("2", "1"), ("2", "3"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -42,7 +59,16 @@ def test_filter_edges_for_property_le(): def check(graph): filter_expr = filter.Property("p2") <= 6 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("1", "2"), ("2", "1"), ("2", "3"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -53,7 +79,15 @@ def test_filter_edges_for_property_gt(): def check(graph): filter_expr = filter.Property("p2") > 2 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("1", "2"), ("2", "1"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -64,7 +98,16 @@ def test_edges_for_property_ge(): def check(graph): filter_expr = filter.Property("p2") >= 2 result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("1", "2"), ("2", "1"), ("2", "3"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -85,12 +128,27 @@ def check(graph): filter_expr = filter.Property("p2").is_in([6]) result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("2", "1"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("2", "1"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids filter_expr = filter.Property("p2").is_in([2, 6]) result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("2", "1"), ("2", "3"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -112,7 +170,16 @@ def test_filter_edges_for_property_is_some(): def check(graph): filter_expr = filter.Property("p2").is_some() result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("1", "2"), ("2", "1"), ("2", "3"), ("3", "1"), ('David Gilmour', 'John Mayer'), ('John Mayer', 'Jimmy Page')]) + expected_ids = sorted( + [ + ("1", "2"), + ("2", "1"), + ("2", "3"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check @@ -123,7 +190,7 @@ def test_filter_edges_for_property_is_none(): def check(graph): filter_expr = filter.Property("p3").is_none() result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = sorted([("1","2"),("2","3")]) + expected_ids = sorted([("1", "2"), ("2", "3")]) assert result_ids == expected_ids return check @@ -134,17 +201,17 @@ def test_filter_edges_for_property_contains(): def check(graph): filter_expr = filter.Property("p10").contains("Paper") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = [('1','2'), ('2','1'), ('2','3')] + expected_ids = [("1", "2"), ("2", "1"), ("2", "3")] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().any().contains("Paper") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = [('1','2'), ('2','1'), ('2','3')] + expected_ids = [("1", "2"), ("2", "1"), ("2", "3")] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().latest().contains("Paper") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = [('1','2'), ('2','1'), ('2','3')] + expected_ids = [("1", "2"), ("2", "1"), ("2", "3")] assert result_ids == expected_ids filter_expr = filter.Property("p10").constant().contains("Paper") @@ -160,17 +227,17 @@ def test_filter_edges_for_property_not_contains(): def check(graph): filter_expr = filter.Property("p10").not_contains("ship") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = [('1','2'), ('2','1')] + expected_ids = [("1", "2"), ("2", "1")] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().any().not_contains("ship") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = [('1','2'), ('2','1')] + expected_ids = [("1", "2"), ("2", "1")] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().latest().not_contains("ship") result_ids = sorted(graph.filter_edges(filter_expr).edges.id) - expected_ids = [('1','2'), ('2','1')] + expected_ids = [("1", "2"), ("2", "1")] assert result_ids == expected_ids filter_expr = filter.Property("p10").constant().not_contains("ship") @@ -186,8 +253,14 @@ def test_filter_edges_for_not_property(): def check(graph): filter_expr = filter.Property("p3").is_none() result_ids = sorted(graph.filter_edges(~filter_expr).edges.id) - expected_ids = sorted([("2","1"),("3","1"), ("David Gilmour", "John Mayer"), ("John Mayer", "Jimmy Page")]) + expected_ids = sorted( + [ + ("2", "1"), + ("3", "1"), + ("David Gilmour", "John Mayer"), + ("John Mayer", "Jimmy Page"), + ] + ) assert result_ids == expected_ids return check - diff --git a/python/tests/test_base_install/test_filters/test_node_composite_filter.py b/python/tests/test_base_install/test_filters/test_node_composite_filter.py index 23ba11f518..6eb4bd4cf0 100644 --- a/python/tests/test_base_install/test_filters/test_node_composite_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_composite_filter.py @@ -40,7 +40,9 @@ def check(graph): filter_expr1 = filter.Node.name() == "2" filter_expr2 = filter.Property("p2") == 2 filter_expr3 = filter.Property("p9") <= 5 - result_ids = sorted(graph.filter_nodes((filter_expr1 & filter_expr2) | filter_expr3).nodes.id) + result_ids = sorted( + graph.filter_nodes((filter_expr1 & filter_expr2) | filter_expr3).nodes.id + ) expected_ids = ["1", "2"] assert result_ids == expected_ids @@ -57,8 +59,9 @@ def check(graph): assert result_ids == expected_ids result_ids = sorted(graph.filter_nodes(~(filter_expr1 & filter_expr2)).nodes.id) - expected_ids = sorted(["1", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"]) + expected_ids = sorted( + ["1", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + ) assert result_ids == expected_ids return check - diff --git a/python/tests/test_base_install/test_filters/test_node_filter.py b/python/tests/test_base_install/test_filters/test_node_filter.py index 1c0c6813c2..bac2f42019 100644 --- a/python/tests/test_base_install/test_filters/test_node_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_filter.py @@ -20,7 +20,7 @@ def test_filter_nodes_for_node_name_ne(): def check(graph): filter_expr = filter.Node.name() != "2" result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ["1", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + expected_ids = ["1", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids return check @@ -47,7 +47,7 @@ def test_filter_nodes_for_node_name_not_in(): def check(graph): filter_expr = filter.Node.name().is_not_in(["1"]) result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ["2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + expected_ids = ["2", "3", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids return check @@ -69,7 +69,7 @@ def test_filter_nodes_for_node_type_ne(): def check(graph): filter_expr = filter.Node.node_type() != "fire_nation" result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids return check @@ -96,7 +96,7 @@ def test_filter_nodes_for_node_type_not_in(): def check(graph): filter_expr = filter.Node.node_type().is_not_in(["fire_nation"]) result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids return check @@ -118,7 +118,7 @@ def test_filter_nodes_for_node_type_not_contains(): def check(graph): filter_expr = filter.Node.node_type().not_contains("fire") result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + expected_ids = ["2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids return check diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index c943e54ec7..e74d9a9ad4 100644 --- a/python/tests/test_base_install/test_filters/test_node_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_node_property_filter.py @@ -3,17 +3,18 @@ from filters_setup import init_graph from utils import with_disk_variants + @with_disk_variants(init_graph) def test_filter_nodes_for_property_eq(): def check(graph): filter_expr = filter.Property("p2") == 2 result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['2'] + expected_ids = ["2"] assert result_ids == expected_ids filter_expr = filter.Property("p1") == "shivam_kapoor" result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['1'] + expected_ids = ["1"] assert result_ids == expected_ids return check @@ -24,7 +25,7 @@ def test_filter_nodes_for_property_ne(): def check(graph): filter_expr = filter.Property("p2") != 2 result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['3'] + expected_ids = ["3"] assert result_ids == expected_ids return check @@ -35,7 +36,7 @@ def test_filter_nodes_for_property_lt(): def check(graph): filter_expr = filter.Property("p2") < 10 result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['2', '3'] + expected_ids = ["2", "3"] assert result_ids == expected_ids return check @@ -46,7 +47,7 @@ def test_filter_nodes_for_property_le(): def check(graph): filter_expr = filter.Property("p2") <= 6 result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['2', '3'] + expected_ids = ["2", "3"] assert result_ids == expected_ids return check @@ -57,7 +58,7 @@ def test_filter_nodes_for_property_gt(): def check(graph): filter_expr = filter.Property("p2") > 2 result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['3'] + expected_ids = ["3"] assert result_ids == expected_ids return check @@ -68,7 +69,7 @@ def test_nodes_for_property_ge(): def check(graph): filter_expr = filter.Property("p2") >= 2 result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['2', '3'] + expected_ids = ["2", "3"] assert result_ids == expected_ids return check @@ -79,12 +80,12 @@ def test_filter_nodes_for_property_in(): def check(graph): filter_expr = filter.Property("p2").is_in([6]) result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['3'] + expected_ids = ["3"] assert result_ids == expected_ids filter_expr = filter.Property("p2").is_in([2, 6]) result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['2', '3'] + expected_ids = ["2", "3"] assert result_ids == expected_ids return check @@ -95,7 +96,7 @@ def test_filter_nodes_for_property_not_in(): def check(graph): filter_expr = filter.Property("p2").is_not_in([6]) result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['2'] + expected_ids = ["2"] assert result_ids == expected_ids return check @@ -106,7 +107,7 @@ def test_filter_nodes_for_property_is_some(): def check(graph): filter_expr = filter.Property("p2").is_some() result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['2', '3'] + expected_ids = ["2", "3"] assert result_ids == expected_ids return check @@ -117,7 +118,7 @@ def test_filter_nodes_for_property_is_none(): def check(graph): filter_expr = filter.Property("p2").is_none() result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + expected_ids = ["1", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids return check @@ -129,7 +130,7 @@ def check(graph): filter_expr1 = filter.Property("p4") == "pometry" filter_expr2 = filter.Property("p5") == 12 result_ids = sorted(graph.filter_nodes(filter_expr1 & filter_expr2).nodes.id) - expected_ids = ['4'] + expected_ids = ["4"] assert result_ids == expected_ids return check @@ -140,17 +141,17 @@ def test_filter_nodes_for_property_contains(): def check(graph): filter_expr = filter.Property("p10").contains("Paper") result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['1', '2', '3'] + expected_ids = ["1", "2", "3"] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().any().contains("Paper") result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['1', '2', '3'] + expected_ids = ["1", "2", "3"] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().latest().contains("Paper") result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['1', '2', '3'] + expected_ids = ["1", "2", "3"] assert result_ids == expected_ids filter_expr = filter.Property("p10").constant().contains("Paper") @@ -166,17 +167,17 @@ def test_filter_nodes_for_property_not_contains(): def check(graph): filter_expr = filter.Property("p10").not_contains("ship") result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['1', '3'] + expected_ids = ["1", "3"] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().any().not_contains("ship") result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['1', '3'] + expected_ids = ["1", "3"] assert result_ids == expected_ids filter_expr = filter.Property("p10").temporal().latest().not_contains("ship") result_ids = sorted(graph.filter_nodes(filter_expr).nodes.id) - expected_ids = ['1', '3'] + expected_ids = ["1", "3"] assert result_ids == expected_ids filter_expr = filter.Property("p10").constant().not_contains("ship") @@ -192,7 +193,7 @@ def test_filter_nodes_for_not_property(): def check(graph): filter_expr = filter.Property("p2") > 2 result_ids = sorted(graph.filter_nodes(~filter_expr).nodes.id) - expected_ids = ["1", "2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] + expected_ids = ["1", "2", "4", "David Gilmour", "Jimmy Page", "John Mayer"] assert result_ids == expected_ids return check diff --git a/python/tests/test_base_install/test_graphdb/test_property_filters.py b/python/tests/test_base_install/test_graphdb/test_property_filters.py index 7e7a83a539..cf057e286d 100644 --- a/python/tests/test_base_install/test_graphdb/test_property_filters.py +++ b/python/tests/test_base_install/test_graphdb/test_property_filters.py @@ -2,6 +2,7 @@ from raphtory import filter import pytest + def build_graph(): graph = Graph() @@ -64,20 +65,21 @@ def test_property_filter_edges(): test_cases = [ (filter.Property("test_str") == "first", [(1, 2)]), - (filter.Property("test_str") != "first", [(2, 3)]), # currently excludes edges without the property + ( + filter.Property("test_str") != "first", + [(2, 3)], + ), # currently excludes edges without the property (filter.Property("test_str").is_some(), [(1, 2), (2, 3)]), (filter.Property("test_str").is_none(), [(3, 4)]), (filter.Property("test_str") == "second", []), (filter.Property("test_str").is_in(["first", "fourth"]), [(1, 2)]), (filter.Property("test_str").is_not_in(["first"]), [(2, 3)]), - (filter.Property("test_int") == 2, []), (filter.Property("test_int") != 1, [(1, 2), (3, 4)]), (filter.Property("test_int") > 2, [(3, 4)]), (filter.Property("test_int") >= 1, [(2, 3), (3, 4)]), (filter.Property("test_int") < 3, [(1, 2), (2, 3)]), (filter.Property("test_int") <= 1, [(1, 2), (2, 3)]), - (filter.Property("test_bool") == True, [(2, 3)]), ] @@ -87,7 +89,9 @@ def test_property_filter_edges(): # edge case: temporal filtering before time 5 filter_expr = filter.Property("test_str") == "second" expected_ids = [(2, 3)] - assert sorted(graph.before(5).filter_edges(filter_expr).edges.id) == sorted(expected_ids) + assert sorted(graph.before(5).filter_edges(filter_expr).edges.id) == sorted( + expected_ids + ) @pytest.mark.skip(reason="Ignoring this test temporarily") @@ -95,23 +99,26 @@ def test_filter_exploded_edges(): graph = build_graph() test_cases = [ - (Prop("test_str") == "first", [(1, 2)]), - (Prop("test_str") != "first", [(2, 3)]), # currently excludes edges without the property - (Prop("test_str").is_some(), [(1, 2), (2, 3)]), - (Prop("test_str").is_none(), [(2, 3), (3, 4)]), - (Prop("test_str") == "second", [(2, 3)]), - (Prop("test_str").is_in({"first", "fourth"}), [(1, 2)]), - (Prop("test_str").is_not_in({"first"}), [(2, 3)]), - - (Prop("test_int") == 2, [(3, 4)]), - (Prop("test_int") != 2, [(1, 2), (2, 3), (3, 4)]), - (Prop("test_int") > 2, [(3, 4)]), - (Prop("test_int") >= 2, [(3, 4)]), - (Prop("test_int") < 3, [(1, 2), (2, 3), (3, 4)]), - (Prop("test_int") <= 1, [(1, 2), (2, 3)]), - - (Prop("test_bool") == True, [(2, 3)]), # worth adding special support for this? + (Prop("test_str") == "first", [(1, 2)]), + ( + Prop("test_str") != "first", + [(2, 3)], + ), # currently excludes edges without the property + (Prop("test_str").is_some(), [(1, 2), (2, 3)]), + (Prop("test_str").is_none(), [(2, 3), (3, 4)]), + (Prop("test_str") == "second", [(2, 3)]), + (Prop("test_str").is_in({"first", "fourth"}), [(1, 2)]), + (Prop("test_str").is_not_in({"first"}), [(2, 3)]), + (Prop("test_int") == 2, [(3, 4)]), + (Prop("test_int") != 2, [(1, 2), (2, 3), (3, 4)]), + (Prop("test_int") > 2, [(3, 4)]), + (Prop("test_int") >= 2, [(3, 4)]), + (Prop("test_int") < 3, [(1, 2), (2, 3), (3, 4)]), + (Prop("test_int") <= 1, [(1, 2), (2, 3)]), + (Prop("test_bool") == True, [(2, 3)]), # worth adding special support for this? ] for filter_expr, expected_ids in test_cases: - assert sorted(graph.filter_exploded_edges(filter_expr).edges.id) == sorted(expected_ids) + assert sorted(graph.filter_exploded_edges(filter_expr).edges.id) == sorted( + expected_ids + ) diff --git a/python/tests/test_base_install/test_graphql/edit_graph/test_get_graph.py b/python/tests/test_base_install/test_graphql/edit_graph/test_get_graph.py index cca50f3749..740278d623 100644 --- a/python/tests/test_base_install/test_graphql/edit_graph/test_get_graph.py +++ b/python/tests/test_base_install/test_graphql/edit_graph/test_get_graph.py @@ -87,37 +87,31 @@ def test_get_graphs_returns_emtpy_list_if_no_graphs_found(): client = server.get_client() # Assert if no graphs are discoverable - query = """{ graphs { name, path } }""" - assert client.query(query) == {"graphs": {"name": [], "path": []}} - - -def test_get_graphs_returns_graph_list_if_graphs_found(): - work_dir = tempfile.mkdtemp() - with GraphServer(work_dir).start() as server: - client = server.get_client() - - g = Graph() - g.add_edge(1, "ben", "hamza") - g.add_edge(2, "haaroon", "hamza") - g.add_edge(3, "ben", "haaroon") - - os.makedirs(os.path.join(work_dir, "shivam"), exist_ok=True) - g.save_to_file(os.path.join(work_dir, "g1")) - g.save_to_file(os.path.join(work_dir, "shivam", "g2")) - g.save_to_file(os.path.join(work_dir, "shivam", "g3")) - - # Assert if all graphs present in the work_dir are discoverable - query = """{ graphs { name, path } }""" - response = client.query(query) - sorted_response = { - "graphs": { - "name": sorted(response["graphs"]["name"]), - "path": sorted(normalize_path(p) for p in response["graphs"]["path"]), - } + query = """{ + root { + children { + list { + path + } + } + graphs { + list { + name + } + } + } + namespaces { + list { + path + graphs { + list { + name } - assert sorted_response == { - "graphs": { - "name": ["g1", "g2", "g3"], - "path": ["g1", "shivam/g2", "shivam/g3"], - } + } + } + } +}""" + assert client.query(query) == { + "root": {"children": {"list": []}, "graphs": {"list": []}}, + "namespaces": {"list": [{"path": "", "graphs": {"list": []}}]}, } diff --git a/python/tests/test_base_install/test_graphql/misc/test_graphql_vectors.py b/python/tests/test_base_install/test_graphql/misc/test_graphql_vectors.py index 62f021e40d..f346d1d1bc 100644 --- a/python/tests/test_base_install/test_graphql/misc/test_graphql_vectors.py +++ b/python/tests/test_base_install/test_graphql/misc/test_graphql_vectors.py @@ -25,7 +25,7 @@ def assert_correct_documents(client): globalSearch(query: "aab", limit: 1) { entity { __typename - ... on Graph { + ... on DocumentGraph { name } } @@ -61,7 +61,7 @@ def assert_correct_documents(client): "plugins": { "globalSearch": [ { - "entity": {"__typename": "Graph", "name": "abb"}, + "entity": {"__typename": "DocumentGraph", "name": "abb"}, "content": "abb", "embedding": [1.0, 2.0], }, diff --git a/python/tests/test_base_install/test_graphql/test_namespace.py b/python/tests/test_base_install/test_graphql/test_namespace.py index 1c14c58468..0f54018521 100644 --- a/python/tests/test_base_install/test_graphql/test_namespace.py +++ b/python/tests/test_base_install/test_graphql/test_namespace.py @@ -2,7 +2,6 @@ import pytest from raphtory import Graph from raphtory.graphql import GraphServer, RaphtoryClient - import json @@ -28,96 +27,203 @@ def sort_dict(d): return d -def test_children(): +def test_namespaces_and_metagraph(): work_dir = tempfile.mkdtemp() with GraphServer(work_dir).start(): client = RaphtoryClient("http://localhost:1736") make_folder_structure(client) - + # tests list and page on namespaces and metagraphs query = """{ - root { + root { + path + graphs { + list { + path + } + } + children { + list { + path + graphs { + list { + path + } + } + children { + page(limit: 1, offset: 1) { + path + children { + list { + graphs { + page(limit: 1, offset: 1) { + path + } + } + } + } + } + list { + path + graphs { + list { + path + } + } + children { + list { + path + graphs { + list { + path + } + } + children { + list { + path + graphs { + list { + path + } + } + children { + list { path - graphs { - path - } - children { - path - graphs { - path - } - children { - path - graphs { - path - } - children { - path - graphs { - path - } - children { - path - graphs { - path - } - children { - path - } - } - } - } - } } } + } + } + } + } + } + } + } + } + } +} """ result = client.query(query) correct = { "root": { "path": "", - "graphs": [{"path": "graph"}], - "children": [ - { - "path": "test", - "graphs": [{"path": "test/graph"}], - "children": [ - { - "path": "test/first", - "graphs": [], - "children": [ + "graphs": {"list": [{"path": "graph"}]}, + "children": { + "list": [ + { + "path": "test", + "graphs": {"list": [{"path": "test/graph"}]}, + "children": { + "page": [ { - "path": "test/first/internal", - "graphs": [ - {"path": "test/first/internal/graph"} - ], - "children": [], + "path": "test/second", + "children": { + "list": [ + { + "graphs": { + "page": [ + { + "path": "test/second/internal/graph2" + } + ] + } + } + ] + }, } ], - }, - { - "path": "test/second", - "graphs": [], - "children": [ + "list": [ { - "path": "test/second/internal", - "graphs": [ - {"path": "test/second/internal/graph2"}, - {"path": "test/second/internal/graph1"}, - ], - "children": [], - } + "path": "test/first", + "graphs": {"list": []}, + "children": { + "list": [ + { + "path": "test/first/internal", + "graphs": { + "list": [ + { + "path": "test/first/internal/graph" + } + ] + }, + "children": {"list": []}, + } + ] + }, + }, + { + "path": "test/second", + "graphs": {"list": []}, + "children": { + "list": [ + { + "path": "test/second/internal", + "graphs": { + "list": [ + { + "path": "test/second/internal/graph1" + }, + { + "path": "test/second/internal/graph2" + }, + ] + }, + "children": {"list": []}, + } + ] + }, + }, ], }, - ], - } - ], + } + ] + }, } } assert sort_dict(result) == sort_dict(correct) +def test_counting(): + work_dir = tempfile.mkdtemp() + + with GraphServer(work_dir).start(): + client = RaphtoryClient("http://localhost:1736") + make_folder_structure(client) + + query = """ + { + root { + children{ + count + } + } + namespaces{ + page(limit:2 offset:2){ + path + graphs{ + count + } + } + } +} + """ + + result = client.query(query) + correct = { + "root": {"children": {"count": 1}}, + "namespaces": { + "page": [ + {"path": "test/second", "graphs": {"count": 0}}, + {"path": "test/second/internal", "graphs": {"count": 2}}, + ] + }, + } + + assert sort_dict(result) == sort_dict(correct) + + def test_escaping_parent(): work_dir = tempfile.mkdtemp() @@ -236,3 +342,87 @@ def test_wrong_paths(): "The path to the graph contains a subpath to an existing graph: test/second/internal/graph1" in str(excinfo.value) ) + + +def test_namespaces(): + work_dir = tempfile.mkdtemp() + + with GraphServer(work_dir).start(): + client = RaphtoryClient("http://localhost:1736") + make_folder_structure(client) + + query = """ + { + namespaces { + list { + path + children { + page(limit: 5, offset: 0) { + path + } + } + graphs { + list { + name + path + } + } + } + } +}""" + result = client.query(query) + correct = { + "namespaces": { + "list": [ + { + "path": "", + "children": {"page": [{"path": "test"}]}, + "graphs": {"list": [{"name": "graph", "path": "graph"}]}, + }, + { + "path": "test", + "children": { + "page": [{"path": "test/first"}, {"path": "test/second"}] + }, + "graphs": {"list": [{"name": "graph", "path": "test/graph"}]}, + }, + { + "path": "test/first", + "children": {"page": [{"path": "test/first/internal"}]}, + "graphs": {"list": []}, + }, + { + "path": "test/first/internal", + "children": {"page": []}, + "graphs": { + "list": [ + {"name": "graph", "path": "test/first/internal/graph"} + ] + }, + }, + { + "path": "test/second", + "children": {"page": [{"path": "test/second/internal"}]}, + "graphs": {"list": []}, + }, + { + "path": "test/second/internal", + "children": {"page": []}, + "graphs": { + "list": [ + { + "name": "graph1", + "path": "test/second/internal/graph1", + }, + { + "name": "graph2", + "path": "test/second/internal/graph2", + }, + ] + }, + }, + ] + } + } + + assert result == correct diff --git a/python/tests/test_base_install/test_graphql/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_nodes_property_filter.py index e76b302e92..3149b83087 100644 --- a/python/tests/test_base_install/test_graphql/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_nodes_property_filter.py @@ -740,7 +740,7 @@ def test_node_property_filter_is_in_no_value(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": []}} }} + expected_output = {"graph": {"nodes": {"nodeFilter": {"list": []}}}} run_graphql_test(query, expected_output, graph()) @@ -885,4 +885,4 @@ def test_node_property_filter_is_not_in_type_error(graph): } """ expected_error_message = "PropertyType Error: Wrong type for property prop1: expected I64 but actual type is Str" - run_graphql_error_test(query, expected_error_message, graph()) \ No newline at end of file + run_graphql_error_test(query, expected_error_message, graph()) diff --git a/python/tests/test_base_install/test_graphql/test_rolling_expanding.py b/python/tests/test_base_install/test_graphql/test_rolling_expanding.py new file mode 100644 index 0000000000..9cbfa09f14 --- /dev/null +++ b/python/tests/test_base_install/test_graphql/test_rolling_expanding.py @@ -0,0 +1,2041 @@ +from raphtory import Graph +from utils import run_graphql_test, run_graphql_error_test, run_group_graphql_error_test +from datetime import datetime + + +def create_graph_epoch(g): + g.add_edge(1, 1, 2) + g.add_edge(2, 1, 2) + g.add_edge(3, 1, 2) + g.add_edge(2, 1, 3) + g.add_edge(3, 1, 3) + g.add_edge(4, 1, 3) + g.add_edge(5, 6, 7) + + +def create_graph_date(g): + dates = [ + datetime(2025, 1, 1, 0, 0), + datetime(2025, 1, 2, 0, 0), + datetime(2025, 1, 3, 0, 0), + datetime(2025, 1, 4, 0, 0), + datetime(2025, 1, 5, 0, 0), + ] + + g.add_edge(dates[0], 1, 2) + g.add_edge(dates[1], 1, 2) + g.add_edge(dates[2], 1, 2) + g.add_edge(dates[1], 1, 3) + g.add_edge(dates[2], 1, 3) + g.add_edge(dates[3], 1, 3) + g.add_edge(dates[4], 6, 7) + + +def test_graph_date(): + graph = Graph() + create_graph_date(graph) + query = """ + { + graph(path: "g") { + rolling(window: {duration:"1 day"}, step: {duration: "12 hours"}) { + page(limit: 5, offset: 1) { + nodes{ + list{ + name + } + } + } + count + list{ + start + end + } + } + } +} + """ + correct = { + "graph": { + "rolling": { + "page": [ + {"nodes": {"list": [{"name": "1"}, {"name": "2"}, {"name": "3"}]}}, + {"nodes": {"list": [{"name": "1"}, {"name": "3"}]}}, + {"nodes": {"list": [{"name": "1"}, {"name": "3"}]}}, + {"nodes": {"list": [{"name": "6"}, {"name": "7"}]}}, + ], + "count": 9, + "list": [ + {"start": 1735646400000, "end": 1735732800000}, + {"start": 1735689600000, "end": 1735776000000}, + {"start": 1735732800000, "end": 1735819200000}, + {"start": 1735776000000, "end": 1735862400000}, + {"start": 1735819200000, "end": 1735905600000}, + {"start": 1735862400000, "end": 1735948800000}, + {"start": 1735905600000, "end": 1735992000000}, + {"start": 1735948800000, "end": 1736035200000}, + {"start": 1735992000000, "end": 1736078400000}, + ], + } + } + } + run_graphql_test(query, correct, graph) + + query = """ +{ + graph(path: "g") { + expanding(step: {duration: "3 days"}) { + list{ + start + end + } + } + } +} + """ + correct = { + "graph": { + "expanding": { + "list": [ + {"start": None, "end": 1735948800000}, + {"start": None, "end": 1736208000000}, + ] + } + } + } + run_graphql_test(query, correct, graph) + + +def test_graph_epoch(): + graph = Graph() + create_graph_epoch(graph) + query = """ + { + graph(path: "g") { + rolling(window: {epoch: 1}) { + list { + earliestTime + latestTime + } + } + } + } + """ + correct = { + "graph": { + "rolling": { + "list": [ + {"earliestTime": 1, "latestTime": 1}, + {"earliestTime": 2, "latestTime": 2}, + {"earliestTime": 3, "latestTime": 3}, + {"earliestTime": 4, "latestTime": 4}, + {"earliestTime": 5, "latestTime": 5}, + ] + } + } + } + run_graphql_test(query, correct, graph) + + query = """ + { + graph(path: "g") { + rolling(window: {epoch: 1},step:{epoch:2}) { + list { + earliestTime + latestTime + } + } + } + } + """ + correct = { + "graph": { + "rolling": { + "list": [ + {"earliestTime": 2, "latestTime": 2}, + {"earliestTime": 4, "latestTime": 4}, + ] + } + } + } + run_graphql_test(query, correct, graph) + + query = """ + { + graph(path: "g") { + window(start: 2, end: 5) { + rolling(window: {epoch: 2}, step: {epoch: 1}) { + list { + start + end + } + } + } + } +} + """ + correct = { + "graph": { + "window": { + "rolling": { + "list": [ + {"start": 2, "end": 3}, + {"start": 2, "end": 4}, + {"start": 3, "end": 5}, + ] + } + } + } + } + run_graphql_test(query, correct, graph) + + query = """ + { + graph(path: "g") { + window(start: 2, end: 7) { + expanding(step: {epoch: 3}) { + list { + end + nodes { + list { + name + } + } + } + } + } + } +} + """ + correct = { + "graph": { + "window": { + "expanding": { + "list": [ + { + "end": 5, + "nodes": { + "list": [{"name": "1"}, {"name": "2"}, {"name": "3"}] + }, + }, + { + "end": 7, + "nodes": { + "list": [ + {"name": "1"}, + {"name": "2"}, + {"name": "3"}, + {"name": "6"}, + {"name": "7"}, + ] + }, + }, + ] + } + } + } + } + run_graphql_test(query, correct, graph) + + query = """ + { + graph(path: "g") { + window(start: 2, end: 5) { + expanding(step: {epoch: 1}) { + list { + start + end + } + } + } + } + } + """ + correct = { + "graph": { + "window": { + "expanding": { + "list": [ + {"start": 2, "end": 3}, + {"start": 2, "end": 4}, + {"start": 2, "end": 5}, + ] + } + } + } + } + run_graphql_test(query, correct, graph) + + query = """ + { + graph(path: "g") { + rolling(window: {epoch: 3}, step: {epoch: 4}) { + list { + start + end + earliestTime + latestTime + } + } + } +} + """ + correct = { + "graph": { + "rolling": { + "list": [{"start": 2, "end": 5, "earliestTime": 2, "latestTime": 4}] + } + } + } + run_graphql_test(query, correct, graph) + + # testing if a very large step returns nothing + query = """ + { + graph(path: "g") { + rolling(window: {epoch: 3}, step: {epoch:1000}) { + list { + start + end + earliestTime + latestTime + } + } + } +} + """ + correct = {"graph": {"rolling": {"list": []}}} + run_graphql_test(query, correct, graph) + + query = """ + { + graph(path: "g") { + rolling(window: {epoch: 1}, step: {epoch:1}) { + count + } + } + } + """ + correct = {"graph": {"rolling": {"count": 5}}} + run_graphql_test(query, correct, graph) + + query = """ + { + graph(path: "g") { + rolling(window: {epoch: 1}, step: {epoch: 1}) { + page(limit: 2, offset: 2) { + earliestTime + latestTime + } + } + } +} + """ + correct = {"graph": {"rolling": {"page": [{"earliestTime": 5, "latestTime": 5}]}}} + run_graphql_test(query, correct, graph) + + +def test_node(): + graph = Graph() + create_graph_epoch(graph) + query = """ +{ + graph(path: "g") { + node(name:"1"){ + rolling(window:{epoch:1},step:{epoch:1}){ + list{ + start + end + degree + earliestTime + } + count + page(limit:3,offset:1){ + start + degree + } + } + before(time:4){ + expanding(step:{epoch:1}){ + list{ + end + degree + } + count + page(limit:1,offset:2){ + end + degree + } + } + } + + } + } +} + """ + correct = { + "graph": { + "node": { + "rolling": { + "list": [ + {"start": 1, "end": 2, "degree": 1, "earliestTime": 1}, + {"start": 2, "end": 3, "degree": 2, "earliestTime": 2}, + {"start": 3, "end": 4, "degree": 2, "earliestTime": 3}, + {"start": 4, "end": 5, "degree": 1, "earliestTime": 4}, + {"start": 5, "end": 6, "degree": 0, "earliestTime": None}, + ], + "count": 5, + "page": [{"start": 4, "degree": 1}, {"start": 5, "degree": 0}], + }, + "before": { + "expanding": { + "list": [ + {"end": 2, "degree": 1}, + {"end": 3, "degree": 2}, + {"end": 4, "degree": 2}, + ], + "count": 3, + "page": [{"end": 4, "degree": 2}], + } + }, + } + } + } + run_graphql_test(query, correct, graph) + + +def test_nodes(): + graph = Graph() + create_graph_epoch(graph) + + query = """ +{ + graph(path: "g") { + nodes { + rolling(window: {epoch: 1}, step: {epoch: 1}) { + list { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + } + } + count + page(limit: 3, offset: 1) { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + } + } + } + after(time: 1) { + expanding(step: {epoch: 1}) { + list { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + latestTime + } + } + count + page(limit: 2, offset: 1) { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + latestTime + } + } + } + } + } + } +} + """ + correct = { + "graph": { + "nodes": { + "rolling": { + "list": [ + { + "page": [ + { + "id": "1", + "degree": 1, + "start": 1, + "end": 2, + "earliestTime": 1, + } + ] + }, + { + "page": [ + { + "id": "1", + "degree": 2, + "start": 2, + "end": 3, + "earliestTime": 2, + } + ] + }, + { + "page": [ + { + "id": "1", + "degree": 2, + "start": 3, + "end": 4, + "earliestTime": 3, + } + ] + }, + { + "page": [ + { + "id": "1", + "degree": 1, + "start": 4, + "end": 5, + "earliestTime": 4, + } + ] + }, + { + "page": [ + { + "id": "6", + "degree": 1, + "start": 5, + "end": 6, + "earliestTime": 5, + } + ] + }, + ], + "count": 5, + "page": [ + { + "page": [ + { + "id": "1", + "degree": 1, + "start": 4, + "end": 5, + "earliestTime": 4, + } + ] + }, + { + "page": [ + { + "id": "6", + "degree": 1, + "start": 5, + "end": 6, + "earliestTime": 5, + } + ] + }, + ], + }, + "after": { + "expanding": { + "list": [ + { + "page": [ + { + "id": "1", + "degree": 2, + "start": None, + "end": 3, + "earliestTime": 1, + "latestTime": 2, + } + ] + }, + { + "page": [ + { + "id": "1", + "degree": 2, + "start": None, + "end": 4, + "earliestTime": 1, + "latestTime": 3, + } + ] + }, + { + "page": [ + { + "id": "1", + "degree": 2, + "start": None, + "end": 5, + "earliestTime": 1, + "latestTime": 4, + } + ] + }, + { + "page": [ + { + "id": "1", + "degree": 2, + "start": None, + "end": 6, + "earliestTime": 1, + "latestTime": 4, + } + ] + }, + ], + "count": 4, + "page": [ + { + "page": [ + { + "id": "1", + "degree": 2, + "start": None, + "end": 5, + "earliestTime": 1, + "latestTime": 4, + } + ] + }, + { + "page": [ + { + "id": "1", + "degree": 2, + "start": None, + "end": 6, + "earliestTime": 1, + "latestTime": 4, + } + ] + }, + ], + } + }, + } + } + } + run_graphql_test(query, correct, graph) + + +def test_path(): + graph = Graph() + create_graph_epoch(graph) + + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + rolling(window: {epoch: 1}, step: {epoch: 1}) { + list { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + } + } + count + page(limit: 3, offset: 1) { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + } + } + } + after(time: 1) { + expanding(step: {epoch: 1}) { + list { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + latestTime + } + } + count + page(limit: 2, offset: 1) { + page(limit: 1, offset: 0) { + id + degree + start + end + earliestTime + latestTime + } + } + } + } + } + } + } +} + """ + correct = { + "graph": { + "node": { + "neighbours": { + "rolling": { + "list": [ + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 1, + "end": 2, + "earliestTime": 1, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 2, + "end": 3, + "earliestTime": 2, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 3, + "end": 4, + "earliestTime": 3, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 0, + "start": 4, + "end": 5, + "earliestTime": None, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 0, + "start": 5, + "end": 6, + "earliestTime": None, + } + ] + }, + ], + "count": 5, + "page": [ + { + "page": [ + { + "id": "2", + "degree": 0, + "start": 4, + "end": 5, + "earliestTime": None, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 0, + "start": 5, + "end": 6, + "earliestTime": None, + } + ] + }, + ], + }, + "after": { + "expanding": { + "list": [ + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 2, + "end": 3, + "earliestTime": 2, + "latestTime": 2, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 2, + "end": 4, + "earliestTime": 2, + "latestTime": 3, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 2, + "end": 5, + "earliestTime": 2, + "latestTime": 3, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 2, + "end": 6, + "earliestTime": 2, + "latestTime": 3, + } + ] + }, + ], + "count": 4, + "page": [ + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 2, + "end": 5, + "earliestTime": 2, + "latestTime": 3, + } + ] + }, + { + "page": [ + { + "id": "2", + "degree": 1, + "start": 2, + "end": 6, + "earliestTime": 2, + "latestTime": 3, + } + ] + }, + ], + } + }, + } + } + } + } + run_graphql_test(query, correct, graph) + + +def test_edge(): + graph = Graph() + create_graph_epoch(graph) + + query = """ + { + graph(path: "g") { + edge(src:"1",dst:"2"){ + rolling(window:{epoch:1},step:{epoch:1}){ + list{ + start + end + earliestTime + } + count + page(limit:3,offset:1){ + start + end + earliestTime + } + } + after(time:1){ + expanding(step:{epoch:1}){ + list{ + start + end + earliestTime + latestTime + } + count + page(limit:2,offset:1){ + start + end + earliestTime + latestTime + } + } + } + + } + } +} + """ + correct = { + "graph": { + "edge": { + "rolling": { + "list": [ + {"start": 1, "end": 2, "earliestTime": 1}, + {"start": 2, "end": 3, "earliestTime": 2}, + {"start": 3, "end": 4, "earliestTime": 3}, + {"start": 4, "end": 5, "earliestTime": None}, + {"start": 5, "end": 6, "earliestTime": None}, + ], + "count": 5, + "page": [ + {"start": 4, "end": 5, "earliestTime": None}, + {"start": 5, "end": 6, "earliestTime": None}, + ], + }, + "after": { + "expanding": { + "list": [ + { + "start": None, + "end": 3, + "earliestTime": 1, + "latestTime": 2, + }, + { + "start": None, + "end": 4, + "earliestTime": 1, + "latestTime": 3, + }, + { + "start": None, + "end": 5, + "earliestTime": 1, + "latestTime": 3, + }, + { + "start": None, + "end": 6, + "earliestTime": 1, + "latestTime": 3, + }, + ], + "count": 4, + "page": [ + { + "start": None, + "end": 5, + "earliestTime": 1, + "latestTime": 3, + }, + { + "start": None, + "end": 6, + "earliestTime": 1, + "latestTime": 3, + }, + ], + } + }, + } + } + } + run_graphql_test(query, correct, graph) + + +def test_edges(): + graph = Graph() + create_graph_epoch(graph) + + query = """ +{ + graph(path: "g") { + edges { + rolling(window: {epoch: 1}, step: {epoch: 1}) { + list { + page(limit: 1, offset: 0) { + id + start + end + earliestTime + } + } + count + page(limit: 3, offset: 1) { + page(limit: 1, offset: 0) { + id + start + end + earliestTime + } + } + } + after(time: 1) { + expanding(step: {epoch: 1}) { + list { + page(limit: 1, offset: 0) { + id + start + end + earliestTime + latestTime + } + } + count + page(limit: 2, offset: 1) { + page(limit: 1, offset: 0) { + id + start + end + earliestTime + latestTime + } + } + } + } + } + } +} + """ + correct = { + "graph": { + "edges": { + "rolling": { + "list": [ + { + "page": [ + { + "id": ["1", "2"], + "start": 1, + "end": 2, + "earliestTime": 1, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": 2, + "end": 3, + "earliestTime": 2, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": 3, + "end": 4, + "earliestTime": 3, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": 4, + "end": 5, + "earliestTime": None, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": 5, + "end": 6, + "earliestTime": None, + } + ] + }, + ], + "count": 5, + "page": [ + { + "page": [ + { + "id": ["1", "2"], + "start": 4, + "end": 5, + "earliestTime": None, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": 5, + "end": 6, + "earliestTime": None, + } + ] + }, + ], + }, + "after": { + "expanding": { + "list": [ + { + "page": [ + { + "id": ["1", "2"], + "start": None, + "end": 3, + "earliestTime": 1, + "latestTime": 2, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": None, + "end": 4, + "earliestTime": 1, + "latestTime": 3, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": None, + "end": 5, + "earliestTime": 1, + "latestTime": 3, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": None, + "end": 6, + "earliestTime": 1, + "latestTime": 3, + } + ] + }, + ], + "count": 4, + "page": [ + { + "page": [ + { + "id": ["1", "2"], + "start": None, + "end": 5, + "earliestTime": 1, + "latestTime": 3, + } + ] + }, + { + "page": [ + { + "id": ["1", "2"], + "start": None, + "end": 6, + "earliestTime": 1, + "latestTime": 3, + } + ] + }, + ], + } + }, + } + } + } + run_graphql_test(query, correct, graph) + + +def test_zero_step(): + graph = Graph() + create_graph_epoch(graph) + queries_and_exceptions = [] + zero_exception = "Failed to parse time string: 0 size step is not supported" + # graph fail test + query = """ + { + graph(path: "g") { + rolling(window:{duration:"1 day"},step:{duration:"0 day"}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + rolling(window:{epoch:100},step:{epoch:0}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + rolling(window:{duration:"0 day"}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + rolling(window:{epoch:0}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + expanding(step:{duration:"0 day"}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + expanding(step:{epoch:0}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + # node tests + query = """ + { + graph(path: "g") { + node(name: "1") { + rolling(window:{duration:"1 day"},step:{duration:"0 year"}){ + list { + earliestTime + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + node(name: "1") { + rolling(window:{epoch:100},step:{epoch:0}){ + list { + earliestTime + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + node(name: "1") { + rolling(window:{duration:"0 day"}){ + list { + earliestTime + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + node(name: "1") { + rolling(window:{epoch:0}){ + list { + earliestTime + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + node(name: "1") { + expanding(step:{duration:"0 day"}){ + list { + earliestTime + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + node(name: "1") { + expanding(step:{epoch:0}){ + list { + earliestTime + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + # nodes tests + query = """ + { + graph(path: "g") { + nodes { + list { + rolling(window:{duration:"1 day"},step:{duration:"0 year"}){ + list { + earliestTime + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + nodes { + list { + rolling(window:{epoch:100},step:{epoch:0}){ + list { + earliestTime + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + nodes { + list { + rolling(window:{duration:"0 day"}){ + list { + earliestTime + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + nodes { + list { + rolling(window:{epoch:0}){ + list { + earliestTime + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + nodes { + list { + expanding(step:{duration:"0 day"}){ + list { + earliestTime + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + nodes { + list { + expanding(step:{epoch:0}){ + list { + earliestTime + } + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + # path from nodes tests + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + rolling(window:{duration:"1 day"},step:{duration:"0 year"}){ + list { + list { + earliestTime + } + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + rolling(window:{epoch:100},step:{epoch:0}){ + list { + list { + earliestTime + } + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + rolling(window:{duration:"0 year"}){ + list { + list { + earliestTime + } + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + rolling(window:{epoch:0}){ + list { + list { + earliestTime + } + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + expanding(step:{duration:"0 year"}){ + list { + list { + earliestTime + } + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + node(name: "1") { + neighbours { + expanding(step:{epoch:0}){ + list { + list { + earliestTime + } + } + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + # edge tests + query = """ +{ + graph(path: "g") { + edge(src: "1", dst: "2") { + rolling(window:{duration:"1 day"},step:{duration:"0 year"}){ + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edge(src: "1", dst: "2") { + rolling(window:{epoch:100},step:{epoch:0}){ + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edge(src: "1", dst: "2") { + rolling(window:{duration:"0 year"}){ + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edge(src: "1", dst: "2") { + rolling(window:{epoch:0}){ + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edge(src: "1", dst: "2") { + expanding(step:{duration:"0 year"}){ + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + edge(src: "1", dst: "2") { + expanding(step:{epoch:0}){ + list { + earliestTime + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + + # edges tests + query = """ +{ + graph(path: "g") { + edges { + rolling(window:{duration:"1 day"},step:{duration:"0 year"}){ + list { + list{ + earliestTime + } + + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edges { + rolling(window:{epoch:100},step:{epoch:0}){ + list { + list{ + earliestTime + } + + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edges { + rolling(window:{duration:"0 year"}){ + list { + list{ + earliestTime + } + + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edges { + rolling(window:{epoch:0}){ + list { + list{ + earliestTime + } + + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ +{ + graph(path: "g") { + edges { + expanding(step:{duration:"0 year"}){ + list { + list{ + earliestTime + } + + } + } + } + } +} + """ + queries_and_exceptions.append((query, zero_exception)) + + query = """ + { + graph(path: "g") { + edges { + expanding(step:{epoch:0}){ + list { + list{ + earliestTime + } + + } + } + } + } + } + """ + queries_and_exceptions.append((query, zero_exception)) + run_group_graphql_error_test(queries_and_exceptions, graph) + + +def test_wrong_window(): + graph = Graph() + create_graph_epoch(graph) + queries_and_exceptions = [] + mismatch_exception = "Your window and step must be of the same type: duration (string) or epoch (int)" + parse_exception = "Failed to parse time string: one of the tokens in the interval string supposed to be a number couldn't be parsed" + parse_exception2 = "Failed to parse time string: 'monthdas' is not a valid unit" + too_many_exception = "Invalid value for argument \\" + # graph fail test + query = """ + { + graph(path: "g") { + rolling(window:{duration:"1 day"},step:{epoch:100}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, mismatch_exception)) + + query = """ + { + graph(path: "g") { + rolling(window:{epoch:100},step:{duration:"1 day"}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, mismatch_exception)) + + query = """ + { + graph(path: "g") { + rolling(window:{duration:"1dasdas day"}){ + list{ + earliestTime + } + } + } + } + """ + queries_and_exceptions.append((query, parse_exception)) + + query = """ + { + graph(path: "g") { + rolling(window:{duration:"1 day"},step:{duration:"1 monthdas"}){ + list{ + earliestTime + } + } + } +} + """ + queries_and_exceptions.append((query, parse_exception2)) + + query = """ + { + graph(path: "g") { + rolling(window:{duration:"1 day",epoch:11}){ + list{ + earliestTime + } + } + } +} + """ + queries_and_exceptions.append((query, too_many_exception)) + + # node tests + query = """ + { + graph(path: "g") { + node(name: "1") { + rolling(window: {duration: "1 day"}, step: {epoch: 11}) { + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + query = """ + { + graph(path: "g") { + node(name: "1") { + rolling(window: {epoch: 100}, step: {duration: "1 day"}) { + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + # nodes tests + query = """ + { + graph(path: "g") { + nodes { + list { + rolling(window: {duration: "1 day"}, step: {epoch: 11}) { + list { + earliestTime + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + query = """ +{ + graph(path: "g") { + nodes { + list { + rolling(window: {epoch: 100}, step: {duration: "1 day"}) { + list { + earliestTime + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + # path from nodes tests + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + rolling(window: {duration: "1 day"}, step: {epoch: 11}) { + list { + list { + earliestTime + } + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + query = """ +{ + graph(path: "g") { + node(name: "1") { + neighbours { + rolling(window: {epoch: 100}, step: {duration: "1 day"}) { + list { + list { + earliestTime + } + } + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + # edge tests + query = """ +{ + graph(path: "g") { + edge(src: "1", dst: "2") { + rolling(window: {duration: "1 day"}, step: {epoch: 11}) { + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + query = """ +{ + graph(path: "g") { + edge(src: "1", dst: "2") { + rolling(window: {epoch: 100}, step: {duration: "1 day"}) { + list { + earliestTime + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + # edges tests + query = """ +{ + graph(path: "g") { + edges { + rolling(window: {duration: "1 day"}, step: {epoch: 11}) { + list { + list{ + earliestTime + } + + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + + query = """ +{ + graph(path: "g") { + edges { + rolling(window: {epoch: 100}, step: {duration: "1 day"}) { + list { + list{ + earliestTime + } + + } + } + } + } +} + """ + queries_and_exceptions.append((query, mismatch_exception)) + run_group_graphql_error_test(queries_and_exceptions, graph) diff --git a/python/tests/test_base_install/test_index.py b/python/tests/test_base_install/test_index.py index f8f67db61b..162b337bb7 100644 --- a/python/tests/test_base_install/test_index.py +++ b/python/tests/test_base_install/test_index.py @@ -781,7 +781,9 @@ def test_search_edges_for_src_is_not_in(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.src().name().is_not_in(["N10", "N11", "N12", "N13", "N14"]) + filter_expr = ( + filter.Edge.src().name().is_not_in(["N10", "N11", "N12", "N13", "N14"]) + ) results = search_edges(g, filter_expr) assert [ ("N1", "N2"), diff --git a/raphtory-graphql/src/config/cache_config.rs b/raphtory-graphql/src/config/cache_config.rs index eb9320a605..da97275717 100644 --- a/raphtory-graphql/src/config/cache_config.rs +++ b/raphtory-graphql/src/config/cache_config.rs @@ -1,7 +1,7 @@ use serde::Deserialize; pub const DEFAULT_CAPACITY: u64 = 30; -pub const DEFAULT_TTI_SECONDS: u64 = 900; +pub const DEFAULT_TTI_SECONDS: u64 = 1000000000; #[derive(Debug, Deserialize, PartialEq, Clone, serde::Serialize)] pub struct CacheConfig { diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index 9dc974b493..9bc5eccfbd 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -21,6 +21,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use tokio::task::spawn_blocking; use tracing::{error, warn}; use walkdir::WalkDir; @@ -94,6 +95,23 @@ impl Data { .map(|graph| (graph, graph_folder)) } + pub async fn get_graph_async( + &self, + path: &str, + ) -> Result<(GraphWithVectors, ExistingGraphFolder), Arc> { + let data = self.clone(); + let graph_folder = ExistingGraphFolder::try_from(data.work_dir.clone(), path)?; + let path = path.into(); + // TODO: we should use the async cache apis + spawn_blocking(move || { + data.cache + .try_get_with(path, || data.read_graph_from_folder(&graph_folder)) + .map(|graph| (graph, graph_folder)) + }) + .await + .unwrap() + } + pub async fn insert_graph( &self, path: &str, diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index 1d89c30a78..5e32eeb77d 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -1232,28 +1232,32 @@ mod graphql_test { let req = r#" { - namespace(path: "") { - path - graphs { - path - name - metadata { - nodeCount - edgeCount - properties { - key - value - } - } - } - children { - path - } - parent { - path - } + namespace(path: "") { + path + graphs { + list { + path + name + metadata { + nodeCount + edgeCount + properties { + key + value } } + } + } + children { + list { + path + } + } + parent { + path + } + } +} "#; let req = Request::new(req); @@ -1264,7 +1268,7 @@ mod graphql_test { json!({ "namespace": { "path": "", - "graphs": [ + "graphs": {"list":[ { "path": "graph", "name": "graph", @@ -1279,8 +1283,8 @@ mod graphql_test { ] } }, - ], - "children": [], + ]}, + "children":{"list":[]}, "parent": null }, }), @@ -1297,28 +1301,32 @@ mod graphql_test { let req = r#" { - namespace(path: "") { - path - graphs { - path - name - metadata { - nodeCount - edgeCount - properties { - key - value - } - } - } - children { - path - } - parent { - path - } + namespace(path: "") { + path + graphs { + list { + path + name + metadata { + nodeCount + edgeCount + properties { + key + value } } + } + } + children { + list { + path + } + } + parent { + path + } + } +} "#; let req = Request::new(req); @@ -1329,7 +1337,7 @@ mod graphql_test { json!({ "namespace": { "path": "", - "graphs": [ + "graphs": {"list":[ { "path": "graph", "name": "graph", @@ -1358,8 +1366,8 @@ mod graphql_test { ] } }, - ], - "children": [], + ]}, + "children":{"list":[]}, "parent": null }, }), diff --git a/raphtory-graphql/src/model/algorithms/document.rs b/raphtory-graphql/src/model/algorithms/document.rs index 24d5888f9f..faf1bbfcf4 100644 --- a/raphtory-graphql/src/model/algorithms/document.rs +++ b/raphtory-graphql/src/model/algorithms/document.rs @@ -1,4 +1,4 @@ -use crate::model::graph::{edge::Edge, node::Node}; +use crate::model::graph::{edge::GqlEdge, node::GqlNode}; use dynamic_graphql::{SimpleObject, Union}; use raphtory::{ core::Lifespan, @@ -7,31 +7,32 @@ use raphtory::{ }; #[derive(SimpleObject)] -struct Graph { +struct DocumentGraph { name: String, // TODO: maybe return the graph as well here } -impl From for Graph { +impl From for DocumentGraph { fn from(value: String) -> Self { Self { name: value } } } #[derive(Union)] +#[graphql(name = "DocumentEntity")] enum GqlDocumentEntity { - Node(Node), - Edge(Edge), - Graph(Graph), + DocNode(GqlNode), + DocEdge(GqlEdge), + DocGraph(DocumentGraph), } impl From> for GqlDocumentEntity { fn from(value: DocumentEntity) -> Self { match value { - DocumentEntity::Graph { name, .. } => Self::Graph(Graph { + DocumentEntity::Graph { name, .. } => Self::DocGraph(DocumentGraph { name: name.unwrap(), }), - DocumentEntity::Node(node) => Self::Node(Node::from(node)), - DocumentEntity::Edge(edge) => Self::Edge(Edge::from(edge)), + DocumentEntity::Node(node) => Self::DocNode(GqlNode::from(node)), + DocumentEntity::Edge(edge) => Self::DocEdge(GqlEdge::from(edge)), } } } diff --git a/raphtory-graphql/src/model/graph/edge.rs b/raphtory-graphql/src/model/graph/edge.rs index 5e10ef2b1b..61b0e269b1 100644 --- a/raphtory-graphql/src/model/graph/edge.rs +++ b/raphtory-graphql/src/model/graph/edge.rs @@ -1,23 +1,31 @@ use crate::model::graph::{ - edges::GqlEdges, filtering::EdgeViewCollection, node::Node, property::GqlProperties, + edges::GqlEdges, + filtering::EdgeViewCollection, + node::GqlNode, + property::GqlProperties, + windowset::GqlEdgeWindowSet, + WindowDuration, + WindowDuration::{Duration, Epoch}, }; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{ - core::utils::errors::GraphError, + core::utils::errors::{GraphError, GraphError::MismatchedIntervalTypes}, db::{ api::view::{DynamicGraph, EdgeViewOps, IntoDynamic, StaticGraphViewOps}, graph::edge::EdgeView, }, prelude::{LayerOps, TimeOps}, }; +use tokio::task::spawn_blocking; #[derive(ResolvedObject, Clone)] -pub struct Edge { +#[graphql(name = "Edge")] +pub struct GqlEdge { pub(crate) ee: EdgeView, } impl - From> for Edge + From> for GqlEdge { fn from(value: EdgeView) -> Self { Self { @@ -30,7 +38,7 @@ impl } } -impl Edge { +impl GqlEdge { pub(crate) fn from_ref< G: StaticGraphViewOps + IntoDynamic, GH: StaticGraphViewOps + IntoDynamic, @@ -42,56 +50,118 @@ impl Edge { } #[ResolvedObjectFields] -impl Edge { +impl GqlEdge { //////////////////////// // LAYERS AND WINDOWS // //////////////////////// - async fn default_layer(&self) -> Edge { - self.ee.default_layer().into() - } - - async fn layers(&self, names: Vec) -> Edge { - self.ee.valid_layers(names).into() - } - - async fn exclude_layers(&self, names: Vec) -> Edge { - self.ee.exclude_valid_layers(names).into() - } - - async fn layer(&self, name: String) -> Edge { - self.ee.valid_layers(name).into() + async fn default_layer(&self) -> GqlEdge { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.default_layer().into()) + .await + .unwrap() + } + + async fn layers(&self, names: Vec) -> GqlEdge { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.valid_layers(names).into()) + .await + .unwrap() + } + + async fn exclude_layers(&self, names: Vec) -> GqlEdge { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.exclude_valid_layers(names).into()) + .await + .unwrap() + } + + async fn layer(&self, name: String) -> GqlEdge { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.valid_layers(name).into()) + .await + .unwrap() + } + async fn exclude_layer(&self, name: String) -> GqlEdge { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.exclude_valid_layers(name).into()) + .await + .unwrap() + } + + async fn rolling( + &self, + window: WindowDuration, + step: Option, + ) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match window { + Duration(window_duration) => match step { + Some(step) => match step { + Duration(step_duration) => Ok(GqlEdgeWindowSet::new( + self_clone + .ee + .rolling(window_duration, Some(step_duration))?, + )), + Epoch(_) => Err(MismatchedIntervalTypes), + }, + None => Ok(GqlEdgeWindowSet::new( + self_clone.ee.rolling(window_duration, None)?, + )), + }, + Epoch(window_duration) => match step { + Some(step) => match step { + Duration(_) => Err(MismatchedIntervalTypes), + Epoch(step_duration) => Ok(GqlEdgeWindowSet::new( + self_clone + .ee + .rolling(window_duration, Some(step_duration))?, + )), + }, + None => Ok(GqlEdgeWindowSet::new( + self_clone.ee.rolling(window_duration, None)?, + )), + }, + }) + .await + .unwrap() } - async fn exclude_layer(&self, name: String) -> Edge { - self.ee.exclude_valid_layers(name).into() + async fn expanding(&self, step: WindowDuration) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match step { + Duration(step) => Ok(GqlEdgeWindowSet::new(self_clone.ee.expanding(step)?)), + Epoch(step) => Ok(GqlEdgeWindowSet::new(self_clone.ee.expanding(step)?)), + }) + .await + .unwrap() } - async fn window(&self, start: i64, end: i64) -> Edge { + async fn window(&self, start: i64, end: i64) -> GqlEdge { self.ee.window(start, end).into() } - async fn at(&self, time: i64) -> Edge { + async fn at(&self, time: i64) -> GqlEdge { self.ee.at(time).into() } - async fn latest(&self) -> Edge { + async fn latest(&self) -> GqlEdge { self.ee.latest().into() } - async fn snapshot_at(&self, time: i64) -> Edge { + async fn snapshot_at(&self, time: i64) -> GqlEdge { self.ee.snapshot_at(time).into() } - async fn snapshot_latest(&self) -> Edge { + async fn snapshot_latest(&self) -> GqlEdge { self.ee.snapshot_latest().into() } - async fn before(&self, time: i64) -> Edge { + async fn before(&self, time: i64) -> GqlEdge { self.ee.before(time).into() } - async fn after(&self, time: i64) -> Edge { + async fn after(&self, time: i64) -> GqlEdge { self.ee.after(time).into() } @@ -107,9 +177,8 @@ impl Edge { self.ee.shrink_end(end).into() } - async fn apply_views(&self, views: Vec) -> Result { - let mut return_view: Edge = self.ee.clone().into(); - + async fn apply_views(&self, views: Vec) -> Result { + let mut return_view: GqlEdge = self.ee.clone().into(); for view in views { let mut count = 0; if let Some(_) = view.default_layer { @@ -176,28 +245,42 @@ impl Edge { return Err(GraphError::TooManyViewsSet); } } - Ok(return_view) } async fn earliest_time(&self) -> Option { - self.ee.earliest_time() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.earliest_time()) + .await + .unwrap() } async fn first_update(&self) -> Option { - self.ee.history().first().cloned() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.history().first().cloned()) + .await + .unwrap() } async fn latest_time(&self) -> Option { - self.ee.latest_time() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.latest_time()) + .await + .unwrap() } async fn last_update(&self) -> Option { - self.ee.history().last().cloned() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.history().last().cloned()) + .await + .unwrap() } async fn time(&self) -> Result { - self.ee.time().map(|x| x.into()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.time().map(|x| x.into())) + .await + .unwrap() } async fn start(&self) -> Option { @@ -208,13 +291,16 @@ impl Edge { self.ee.end() } - async fn src(&self) -> Node { + async fn src(&self) -> GqlNode { self.ee.src().into() } - async fn dst(&self) -> Node { + async fn dst(&self) -> GqlNode { self.ee.dst().into() } + async fn nbr(&self) -> GqlNode { + self.ee.nbr().into() + } async fn id(&self) -> Vec { let (src_name, dst_name) = self.ee.id(); @@ -226,11 +312,17 @@ impl Edge { } async fn layer_names(&self) -> Vec { - self.ee - .layer_names() - .into_iter() - .map(|x| x.into()) - .collect() + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .ee + .layer_names() + .into_iter() + .map(|x| x.into()) + .collect() + }) + .await + .unwrap() } async fn layer_name(&self) -> Result { @@ -246,30 +338,41 @@ impl Edge { } async fn history(&self) -> Vec { - self.ee.history() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.history()) + .await + .unwrap() } async fn deletions(&self) -> Vec { - self.ee.deletions() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.deletions()) + .await + .unwrap() } async fn is_valid(&self) -> bool { - self.ee.is_valid() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.is_valid()) + .await + .unwrap() } async fn is_active(&self) -> bool { - self.ee.is_active() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.is_active()) + .await + .unwrap() } async fn is_deleted(&self) -> bool { - self.ee.is_deleted() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ee.is_deleted()) + .await + .unwrap() } async fn is_self_loop(&self) -> bool { self.ee.is_self_loop() } - - async fn nbr(&self) -> Node { - self.ee.nbr().into() - } } diff --git a/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs index ac5db3b9df..48d6c5e7a2 100644 --- a/raphtory-graphql/src/model/graph/edges.rs +++ b/raphtory-graphql/src/model/graph/edges.rs @@ -1,11 +1,17 @@ use crate::model::{ - graph::{edge::Edge, filtering::EdgesViewCollection}, + graph::{ + edge::GqlEdge, + filtering::EdgesViewCollection, + windowset::GqlEdgesWindowSet, + WindowDuration, + WindowDuration::{Duration, Epoch}, + }, sorting::{EdgeSortBy, SortByTime}, }; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use itertools::Itertools; use raphtory::{ - core::utils::errors::GraphError, + core::utils::errors::{GraphError, GraphError::MismatchedIntervalTypes}, db::{ api::view::{internal::OneHopFilter, DynamicGraph}, graph::edges::Edges, @@ -14,8 +20,10 @@ use raphtory::{ }; use raphtory_api::iter::IntoDynBoxed; use std::{cmp::Ordering, sync::Arc}; +use tokio::task::spawn_blocking; -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "Edges")] pub(crate) struct GqlEdges { pub(crate) ee: Edges<'static, DynamicGraph>, } @@ -31,8 +39,8 @@ impl GqlEdges { Self { ee: edges.into() } } - fn iter(&self) -> Box + '_> { - let iter = self.ee.iter().map(Edge::from_ref); + fn iter(&self) -> Box + '_> { + let iter = self.ee.iter().map(GqlEdge::from_ref); Box::new(iter) } } @@ -47,19 +55,79 @@ impl GqlEdges { self.update(self.ee.default_layer()) } async fn layers(&self, names: Vec) -> Self { - self.update(self.ee.valid_layers(names)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.ee.valid_layers(names))) + .await + .unwrap() } async fn exclude_layers(&self, names: Vec) -> Self { - self.update(self.ee.exclude_valid_layers(names)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.ee.exclude_valid_layers(names))) + .await + .unwrap() } async fn layer(&self, name: String) -> Self { - self.update(self.ee.valid_layers(name)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.ee.valid_layers(name))) + .await + .unwrap() } async fn exclude_layer(&self, name: String) -> Self { - self.update(self.ee.exclude_valid_layers(name)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.ee.exclude_valid_layers(name))) + .await + .unwrap() + } + + async fn rolling( + &self, + window: WindowDuration, + step: Option, + ) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match window { + Duration(window_duration) => match step { + Some(step) => match step { + Duration(step_duration) => Ok(GqlEdgesWindowSet::new( + self_clone + .ee + .rolling(window_duration, Some(step_duration))?, + )), + Epoch(_) => Err(MismatchedIntervalTypes), + }, + None => Ok(GqlEdgesWindowSet::new( + self_clone.ee.rolling(window_duration, None)?, + )), + }, + Epoch(window_duration) => match step { + Some(step) => match step { + Duration(_) => Err(MismatchedIntervalTypes), + Epoch(step_duration) => Ok(GqlEdgesWindowSet::new( + self_clone + .ee + .rolling(window_duration, Some(step_duration))?, + )), + }, + None => Ok(GqlEdgesWindowSet::new( + self_clone.ee.rolling(window_duration, None)?, + )), + }, + }) + .await + .unwrap() + } + + async fn expanding(&self, step: WindowDuration) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match step { + Duration(step) => Ok(GqlEdgesWindowSet::new(self_clone.ee.expanding(step)?)), + Epoch(step) => Ok(GqlEdgesWindowSet::new(self_clone.ee.expanding(step)?)), + }) + .await + .unwrap() } async fn window(&self, start: i64, end: i64) -> Self { @@ -102,7 +170,6 @@ impl GqlEdges { async fn apply_views(&self, views: Vec) -> Result { let mut return_view: GqlEdges = self.update(self.ee.clone()); - for view in views { let mut count = 0; if let Some(_) = view.default_layer { @@ -182,60 +249,66 @@ impl GqlEdges { } async fn sorted(&self, sort_bys: Vec) -> Self { - let sorted: Arc<[_]> = self - .ee - .iter() - .sorted_by(|first_edge, second_edge| { - sort_bys - .clone() - .into_iter() - .fold(Ordering::Equal, |current_ordering, sort_by| { - current_ordering.then_with(|| { - let ordering = if sort_by.src == Some(true) { - first_edge.src().id().partial_cmp(&second_edge.src().id()) - } else if sort_by.dst == Some(true) { - first_edge.dst().id().partial_cmp(&second_edge.dst().id()) - } else if let Some(sort_by_time) = sort_by.time { - let (first_time, second_time) = match sort_by_time { - SortByTime::Latest => { - (first_edge.latest_time(), second_edge.latest_time()) - } - SortByTime::Earliest => { - (first_edge.earliest_time(), second_edge.earliest_time()) - } + let self_clone = self.clone(); + spawn_blocking(move || { + let sorted: Arc<[_]> = self_clone + .ee + .iter() + .sorted_by(|first_edge, second_edge| { + sort_bys.clone().into_iter().fold( + Ordering::Equal, + |current_ordering, sort_by| { + current_ordering.then_with(|| { + let ordering = if sort_by.src == Some(true) { + first_edge.src().id().partial_cmp(&second_edge.src().id()) + } else if sort_by.dst == Some(true) { + first_edge.dst().id().partial_cmp(&second_edge.dst().id()) + } else if let Some(sort_by_time) = sort_by.time { + let (first_time, second_time) = match sort_by_time { + SortByTime::Latest => { + (first_edge.latest_time(), second_edge.latest_time()) + } + SortByTime::Earliest => ( + first_edge.earliest_time(), + second_edge.earliest_time(), + ), + }; + first_time.partial_cmp(&second_time) + } else if let Some(sort_by_property) = sort_by.property { + let first_prop_maybe = + first_edge.properties().get(&*sort_by_property); + let second_prop_maybe = + second_edge.properties().get(&*sort_by_property); + first_prop_maybe.partial_cmp(&second_prop_maybe) + } else { + None }; - first_time.partial_cmp(&second_time) - } else if let Some(sort_by_property) = sort_by.property { - let first_prop_maybe = - first_edge.properties().get(&*sort_by_property); - let second_prop_maybe = - second_edge.properties().get(&*sort_by_property); - first_prop_maybe.partial_cmp(&second_prop_maybe) - } else { - None - }; - if let Some(ordering) = ordering { - if sort_by.reverse == Some(true) { - ordering.reverse() + if let Some(ordering) = ordering { + if sort_by.reverse == Some(true) { + ordering.reverse() + } else { + ordering + } } else { - ordering + Ordering::Equal } - } else { - Ordering::Equal - } - }) - }) - }) - .map(|edge_view| edge_view.edge) - .collect(); - self.update(Edges::new( - self.ee.current_filter().clone(), - self.ee.base_graph().clone(), - Arc::new(move || { - let sorted = sorted.clone(); - (0..sorted.len()).map(move |i| sorted[i]).into_dyn_boxed() - }), - )) + }) + }, + ) + }) + .map(|edge_view| edge_view.edge) + .collect(); + self_clone.update(Edges::new( + self_clone.ee.current_filter().clone(), + self_clone.ee.base_graph().clone(), + Arc::new(move || { + let sorted = sorted.clone(); + (0..sorted.len()).map(move |i| sorted[i]).into_dyn_boxed() + }), + )) + }) + .await + .unwrap() } //////////////////////// @@ -255,15 +328,26 @@ impl GqlEdges { ///////////////// async fn count(&self) -> usize { - self.iter().count() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.iter().count()) + .await + .unwrap() } - async fn page(&self, limit: usize, offset: usize) -> Vec { - let start = offset * limit; - self.iter().skip(start).take(limit).collect() + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone.iter().skip(start).take(limit).collect() + }) + .await + .unwrap() } - async fn list(&self) -> Vec { - self.iter().collect() + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.iter().collect()) + .await + .unwrap() } } diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 5b326435f0..23a7a79f6b 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -32,10 +32,8 @@ pub struct GraphViewCollection { pub layer: Option, pub exclude_layer: Option, pub subgraph: Option>, - pub subgraph_id: Option>, pub subgraph_node_types: Option>, pub exclude_nodes: Option>, - pub exclude_nodes_id: Option>, pub window: Option, pub at: Option, pub latest: Option, diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index a75a2ab25b..613fd47e53 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -2,12 +2,15 @@ use crate::{ data::Data, model::{ graph::{ - edge::Edge, + edge::GqlEdge, edges::GqlEdges, filtering::{EdgeFilter, GraphViewCollection, NodeFilter}, - node::Node, + node::GqlNode, nodes::GqlNodes, property::GqlProperties, + windowset::GqlGraphWindowSet, + WindowDuration, + WindowDuration::{Duration, Epoch}, }, plugins::graph_algorithm_plugin::GraphAlgorithmPlugin, schema::graph_schema::GraphSchema, @@ -20,7 +23,9 @@ use itertools::Itertools; use raphtory::{ core::{ entities::nodes::node_ref::{AsNodeRef, NodeRef}, - utils::errors::{GraphError, InvalidPathReason::PathNotParsable}, + utils::errors::{ + GraphError, GraphError::MismatchedIntervalTypes, InvalidPathReason::PathNotParsable, + }, }, db::{ api::{ @@ -44,8 +49,10 @@ use std::{ convert::{Into, TryInto}, sync::Arc, }; +use tokio::{spawn, task::spawn_blocking}; -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "Graph")] pub(crate) struct GqlGraph { path: ExistingGraphFolder, graph: DynamicGraph, @@ -89,7 +96,10 @@ impl GqlGraph { //////////////////////// async fn unique_layers(&self) -> Vec { - self.graph.unique_layers().map_into().collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.unique_layers().map_into().collect()) + .await + .unwrap() } async fn default_layer(&self) -> GqlGraph { @@ -97,11 +107,17 @@ impl GqlGraph { } async fn layers(&self, names: Vec) -> GqlGraph { - self.apply(|g| g.valid_layers(names.clone())) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.apply(|g| g.valid_layers(names.clone()))) + .await + .unwrap() } async fn exclude_layers(&self, names: Vec) -> GqlGraph { - self.apply(|g| g.exclude_valid_layers(names.clone())) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.apply(|g| g.exclude_valid_layers(names.clone()))) + .await + .unwrap() } async fn layer(&self, name: String) -> GqlGraph { @@ -113,30 +129,88 @@ impl GqlGraph { } async fn subgraph(&self, nodes: Vec) -> GqlGraph { - self.apply(|g| g.subgraph(nodes.clone())) - } - - async fn subgraph_id(&self, nodes: Vec) -> GqlGraph { - let nodes: Vec = nodes.iter().map(|v| v.as_node_ref()).collect(); - self.apply(|g| g.subgraph(nodes.clone())) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.apply(|g| g.subgraph(nodes.clone()))) + .await + .unwrap() } async fn subgraph_node_types(&self, node_types: Vec) -> GqlGraph { - self.apply(|g| g.subgraph_node_types(node_types.clone())) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.apply(|g| g.subgraph_node_types(node_types.clone()))) + .await + .unwrap() } async fn exclude_nodes(&self, nodes: Vec) -> GqlGraph { - let nodes: Vec = nodes.iter().map(|v| v.as_node_ref()).collect(); - self.apply(|g| g.exclude_nodes(nodes.clone())) + let self_clone = self.clone(); + spawn_blocking(move || { + let nodes: Vec = nodes.iter().map(|v| v.as_node_ref()).collect(); + self_clone.apply(|g| g.exclude_nodes(nodes.clone())) + }) + .await + .unwrap() } - async fn exclude_nodes_id(&self, nodes: Vec) -> GqlGraph { - let nodes: Vec = nodes.iter().map(|v| v.as_node_ref()).collect(); - self.apply(|g| g.exclude_nodes(nodes.clone())) + async fn rolling( + &self, + window: WindowDuration, + step: Option, + ) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match window { + Duration(window_duration) => match step { + Some(step) => match step { + Duration(step_duration) => Ok(GqlGraphWindowSet::new( + self_clone + .graph + .rolling(window_duration, Some(step_duration))?, + self_clone.path.clone(), + )), + Epoch(_) => Err(MismatchedIntervalTypes), + }, + None => Ok(GqlGraphWindowSet::new( + self_clone.graph.rolling(window_duration, None)?, + self_clone.path.clone(), + )), + }, + Epoch(window_duration) => match step { + Some(step) => match step { + Duration(_) => Err(MismatchedIntervalTypes), + Epoch(step_duration) => Ok(GqlGraphWindowSet::new( + self_clone + .graph + .rolling(window_duration, Some(step_duration))?, + self_clone.path.clone(), + )), + }, + None => Ok(GqlGraphWindowSet::new( + self_clone.graph.rolling(window_duration, None)?, + self_clone.path.clone(), + )), + }, + }) + .await + .unwrap() + } + + async fn expanding(&self, step: WindowDuration) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match step { + Duration(step) => Ok(GqlGraphWindowSet::new( + self_clone.graph.expanding(step)?, + self_clone.path.clone(), + )), + Epoch(step) => Ok(GqlGraphWindowSet::new( + self_clone.graph.expanding(step)?, + self_clone.path.clone(), + )), + }) + .await + .unwrap() } /// Return a graph containing only the activity between `start` and `end` measured as milliseconds from epoch - async fn window(&self, start: i64, end: i64) -> GqlGraph { self.apply(|g| g.window(start, end)) } @@ -146,7 +220,10 @@ impl GqlGraph { } async fn latest(&self) -> GqlGraph { - self.apply(|g| g.latest()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.apply(|g| g.latest())) + .await + .unwrap() } async fn snapshot_at(&self, time: i64) -> GqlGraph { @@ -182,23 +259,29 @@ impl GqlGraph { //////////////////////// async fn created(&self) -> Result { - self.path.created() + self.path.created_async().await } async fn last_opened(&self) -> Result { - self.path.last_opened() + self.path.created_async().await } async fn last_updated(&self) -> Result { - self.path.last_updated() + self.path.last_updated_async().await } async fn earliest_time(&self) -> Option { - self.graph.earliest_time() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.earliest_time()) + .await + .unwrap() } async fn latest_time(&self) -> Option { - self.graph.latest_time() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.latest_time()) + .await + .unwrap() } async fn start(&self) -> Option { @@ -210,28 +293,38 @@ impl GqlGraph { } async fn earliest_edge_time(&self, include_negative: Option) -> Option { - let include_negative = include_negative.unwrap_or(true); - let all_edges = self - .graph - .edges() - .earliest_time() - .into_iter() - .filter_map(|edge_time| edge_time.filter(|&time| (include_negative || time >= 0))) - .min(); - all_edges + let self_clone = self.clone(); + spawn_blocking(move || { + let include_negative = include_negative.unwrap_or(true); + let all_edges = self_clone + .graph + .edges() + .earliest_time() + .into_iter() + .filter_map(|edge_time| edge_time.filter(|&time| include_negative || time >= 0)) + .min(); + all_edges + }) + .await + .unwrap() } async fn latest_edge_time(&self, include_negative: Option) -> Option { - let include_negative = include_negative.unwrap_or(true); - let all_edges = self - .graph - .edges() - .latest_time() - .into_iter() - .filter_map(|edge_time| edge_time.filter(|&time| (include_negative || time >= 0))) - .max(); + let self_clone = self.clone(); + spawn_blocking(move || { + let include_negative = include_negative.unwrap_or(true); + let all_edges = self_clone + .graph + .edges() + .latest_time() + .into_iter() + .filter_map(|edge_time| edge_time.filter(|&time| include_negative || time >= 0)) + .max(); - all_edges + all_edges + }) + .await + .unwrap() } //////////////////////// @@ -239,15 +332,24 @@ impl GqlGraph { //////////////////////// async fn count_edges(&self) -> usize { - self.graph.count_edges() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.count_edges()) + .await + .unwrap() } async fn count_temporal_edges(&self) -> usize { - self.graph.count_temporal_edges() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.count_temporal_edges()) + .await + .unwrap() } async fn count_nodes(&self) -> usize { - self.graph.count_nodes() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.count_nodes()) + .await + .unwrap() } //////////////////////// @@ -255,61 +357,53 @@ impl GqlGraph { //////////////////////// async fn has_node(&self, name: String) -> bool { - self.graph.has_node(name) - } - - async fn has_node_id(&self, id: u64) -> bool { - self.graph.has_node(id) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.has_node(name)) + .await + .unwrap() } async fn has_edge(&self, src: String, dst: String, layer: Option) -> bool { - match layer { - Some(name) => self + let self_clone = self.clone(); + spawn_blocking(move || match layer { + Some(name) => self_clone .graph .layers(name) .map(|l| l.has_edge(src, dst)) .unwrap_or(false), - None => self.graph.has_edge(src, dst), - } - } - - async fn has_edge_id(&self, src: u64, dst: u64, layer: Option) -> bool { - match layer { - Some(name) => self - .graph - .layers(name) - .map(|l| l.has_edge(src, dst)) - .unwrap_or(false), - None => self.graph.has_edge(src, dst), - } + None => self_clone.graph.has_edge(src, dst), + }) + .await + .unwrap() } //////////////////////// //////// GETTERS /////// //////////////////////// - async fn node(&self, name: String) -> Option { - self.graph.node(name).map(|v| v.into()) - } - - async fn node_id(&self, id: u64) -> Option { - self.graph.node(id).map(|v| v.into()) + async fn node(&self, name: String) -> Option { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.node(name).map(|v| v.into())) + .await + .unwrap() } /// query (optionally a subset of) the nodes in the graph async fn nodes(&self, ids: Option>) -> GqlNodes { - match ids { - None => GqlNodes::new(self.graph.nodes()), - Some(ids) => GqlNodes::new(self.graph.nodes().id_filter(ids)), - } - } - - pub fn edge(&self, src: String, dst: String) -> Option { - self.graph.edge(src, dst).map(|e| e.into()) + let self_clone = self.clone(); + spawn_blocking(move || match ids { + None => GqlNodes::new(self_clone.graph.nodes()), + Some(ids) => GqlNodes::new(self_clone.graph.nodes().id_filter(ids)), + }) + .await + .unwrap() } - pub fn edge_id(&self, src: u64, dst: u64) -> Option { - self.graph.edge(src, dst).map(|e| e.into()) + async fn edge(&self, src: String, dst: String) -> Option { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graph.edge(src, dst).map(|e| e.into())) + .await + .unwrap() } async fn edges<'a>(&self) -> GqlEdges { @@ -332,7 +426,10 @@ impl GqlGraph { //if someone write non-utf characters as a filename async fn name(&self) -> Result { - self.path.get_graph_name() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.path.get_graph_name()) + .await + .unwrap() } async fn path(&self) -> Result { @@ -345,82 +442,110 @@ impl GqlGraph { } async fn namespace(&self) -> Result { - Ok(self - .path - .get_original_path() - .parent() - .and_then(|p| p.to_str().map(|s| s.to_string())) - .ok_or(PathNotParsable(self.path.to_error_path()))? - .to_owned()) + let self_clone = self.clone(); + spawn_blocking(move || { + Ok(self_clone + .path + .get_original_path() + .parent() + .and_then(|p| p.to_str().map(|s| s.to_string())) + .ok_or(PathNotParsable(self_clone.path.to_error_path()))? + .to_owned()) + }) + .await + .unwrap() } async fn schema(&self) -> GraphSchema { - GraphSchema::new(&self.graph) + let self_clone = self.clone(); + spawn_blocking(move || GraphSchema::new(&self_clone.graph)) + .await + .unwrap() } async fn algorithms(&self) -> GraphAlgorithmPlugin { self.graph.clone().into() } - async fn shared_neighbours(&self, selected_nodes: Vec) -> Vec { - if selected_nodes.is_empty() { - return vec![]; - } + async fn shared_neighbours(&self, selected_nodes: Vec) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + if selected_nodes.is_empty() { + return vec![]; + } - let neighbours: Vec>> = selected_nodes - .iter() - .filter_map(|n| self.graph.node(n)) - .map(|n| { - n.neighbours() - .collect() - .iter() - .map(|vv| vv.clone()) - .collect::>>() - }) - .collect(); - - let intersection = neighbours.iter().fold(None, |acc, n| match acc { - None => Some(n.clone()), - Some(acc) => Some(acc.intersection(n).map(|vv| vv.clone()).collect()), - }); - match intersection { - Some(intersection) => intersection.into_iter().map(|vv| vv.into()).collect(), - None => vec![], - } + let neighbours: Vec>> = selected_nodes + .iter() + .filter_map(|n| self_clone.graph.node(n)) + .map(|n| { + n.neighbours() + .collect() + .iter() + .map(|vv| vv.clone()) + .collect::>>() + }) + .collect(); + + let intersection = neighbours.iter().fold(None, |acc, n| match acc { + None => Some(n.clone()), + Some(acc) => Some(acc.intersection(n).map(|vv| vv.clone()).collect()), + }); + match intersection { + Some(intersection) => intersection.into_iter().map(|vv| vv.into()).collect(), + None => vec![], + } + }) + .await + .unwrap() } /// Export all nodes and edges from this graph view to another existing graph async fn export_to<'a>( - &'a self, + &self, ctx: &Context<'a>, path: String, ) -> Result> { let data = ctx.data_unchecked::(); - let other_g = data.get_graph(path.as_ref())?.0; - other_g.import_nodes(self.graph.nodes(), true)?; - other_g.import_edges(self.graph.edges(), true)?; - other_g.write_updates()?; - Ok(true) + let other_g = data.get_graph_async(path.as_ref()).await?.0; + let g = self.graph.clone(); + spawn_blocking(move || { + other_g.import_nodes(g.nodes(), true)?; + other_g.import_edges(g.edges(), true)?; + other_g.write_updates()?; + Ok(true) + }) + .await + .unwrap() } async fn node_filter(&self, filter: NodeFilter) -> Result { - filter.validate()?; - let filter: CompositeNodeFilter = filter.try_into()?; - let filtered_graph = self.graph.filter_nodes(filter)?; - Ok(GqlGraph::new( - self.path.clone(), - filtered_graph.into_dynamic(), - )) + let self_clone = self.clone(); + spawn_blocking(move || { + filter.validate()?; + let filter: CompositeNodeFilter = filter.try_into()?; + let filtered_graph = self_clone.graph.filter_nodes(filter)?; + Ok(GqlGraph::new( + self_clone.path.clone(), + filtered_graph.into_dynamic(), + )) + }) + .await + .unwrap() } async fn edge_filter(&self, filter: EdgeFilter) -> Result { - filter.validate()?; - let filter: CompositeEdgeFilter = filter.try_into()?; - let filtered_graph = self.graph.filter_edges(filter)?; - Ok(GqlGraph::new( - self.path.clone(), - filtered_graph.into_dynamic(), - )) + let self_clone = self.clone(); + spawn_blocking(move || { + filter.validate()?; + let filter: CompositeEdgeFilter = filter.try_into()?; + let filtered_graph = self_clone.graph.filter_edges(filter)?; + Ok(GqlGraph::new( + self_clone.path.clone(), + filtered_graph.into_dynamic(), + )) + }) + .await + .unwrap() } //////////////////////// @@ -431,19 +556,25 @@ impl GqlGraph { filter: NodeFilter, limit: usize, offset: usize, - ) -> Result, GraphError> { - filter.validate()?; - let f: CompositeNodeFilter = filter.try_into()?; - self.execute_search(|| { - Ok(self - .graph - .search_nodes(f, limit, offset) - .into_iter() - .flatten() - .map(|vv| vv.into()) - .collect()) + ) -> Result, GraphError> { + let self_clone = self.clone(); + spawn(async move { + filter.validate()?; + let f: CompositeNodeFilter = filter.try_into()?; + self_clone + .execute_search(|| { + Ok(self_clone + .graph + .search_nodes(f, limit, offset) + .into_iter() + .flatten() + .map(|vv| vv.into()) + .collect()) + }) + .await }) .await + .unwrap() } async fn search_edges( @@ -451,24 +582,29 @@ impl GqlGraph { filter: EdgeFilter, limit: usize, offset: usize, - ) -> Result, GraphError> { - filter.validate()?; - let f: CompositeEdgeFilter = filter.try_into()?; - self.execute_search(|| { - Ok(self - .graph - .search_edges(f, limit, offset) - .into_iter() - .flatten() - .map(|vv| vv.into()) - .collect()) + ) -> Result, GraphError> { + let self_clone = self.clone(); + spawn(async move { + filter.validate()?; + let f: CompositeEdgeFilter = filter.try_into()?; + self_clone + .execute_search(|| { + Ok(self_clone + .graph + .search_edges(f, limit, offset) + .into_iter() + .flatten() + .map(|vv| vv.into()) + .collect()) + }) + .await }) .await + .unwrap() } async fn apply_views(&self, views: Vec) -> Result { let mut return_view: GqlGraph = GqlGraph::new(self.path.clone(), self.graph.clone()); - for view in views { let mut count = 0; if let Some(_) = view.default_layer { @@ -495,10 +631,6 @@ impl GqlGraph { count += 1; return_view = return_view.subgraph(nodes).await; } - if let Some(nodes) = view.subgraph_id { - count += 1; - return_view = return_view.subgraph_id(nodes).await; - } if let Some(types) = view.subgraph_node_types { count += 1; return_view = return_view.subgraph_node_types(types).await; @@ -507,10 +639,6 @@ impl GqlGraph { count += 1; return_view = return_view.exclude_nodes(nodes).await; } - if let Some(nodes) = view.exclude_nodes_id { - count += 1; - return_view = return_view.exclude_nodes_id(nodes).await; - } if let Some(window) = view.window { count += 1; return_view = return_view.window(window.start, window.end).await; diff --git a/raphtory-graphql/src/model/graph/graphs.rs b/raphtory-graphql/src/model/graph/graphs.rs deleted file mode 100644 index 7d6782f0db..0000000000 --- a/raphtory-graphql/src/model/graph/graphs.rs +++ /dev/null @@ -1,70 +0,0 @@ -use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; -use itertools::Itertools; -use raphtory::core::utils::errors::GraphError; - -use crate::paths::ExistingGraphFolder; - -#[derive(ResolvedObject)] -pub(crate) struct GqlGraphs { - folders: Vec, -} - -impl GqlGraphs { - pub fn new(paths: Vec) -> Self { - Self { folders: paths } - } -} - -#[ResolvedObjectFields] -impl GqlGraphs { - //Name and path here do not return a result as we only want to let the user know about - //valid graph paths. No point blowing up if there is one busted fule - - async fn name(&self) -> Vec { - self.folders - .iter() - .filter_map(|folder| folder.get_graph_name().ok()) - .collect() - } - - async fn path(&self) -> Vec { - let paths = self - .folders - .iter() - .map(|folder| folder.get_original_path_str().to_owned()) - .collect_vec(); - paths - } - - async fn namespace(&self) -> Vec> { - let folders = self - .folders - .iter() - .map(|folder| { - folder - .get_original_path() - .parent() - .and_then(|p| p.to_str().map(|s| s.to_string())) - }) - .collect(); - folders - } - - async fn created(&self) -> Result, GraphError> { - self.folders.iter().map(|folder| folder.created()).collect() - } - - async fn last_opened(&self) -> Result, GraphError> { - self.folders - .iter() - .map(|folder| folder.last_opened()) - .collect() - } - - async fn last_updated(&self) -> Result, GraphError> { - self.folders - .iter() - .map(|folder| folder.last_updated()) - .collect() - } -} diff --git a/raphtory-graphql/src/model/graph/meta_graph.rs b/raphtory-graphql/src/model/graph/meta_graph.rs index b33d822694..bc7a3e5a1b 100644 --- a/raphtory-graphql/src/model/graph/meta_graph.rs +++ b/raphtory-graphql/src/model/graph/meta_graph.rs @@ -1,8 +1,9 @@ -use crate::{model::graph::property::GqlProp, paths::ExistingGraphFolder}; +use crate::{model::graph::property::GqlProperty, paths::ExistingGraphFolder}; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields, SimpleObject}; use raphtory::{core::utils::errors::GraphError, serialise::metadata::GraphMetadata}; +use tokio::task::spawn_blocking; -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] pub(crate) struct MetaGraph { folder: ExistingGraphFolder, } @@ -16,31 +17,40 @@ impl MetaGraph { #[ResolvedObjectFields] impl MetaGraph { async fn name(&self) -> Option { - self.folder.get_graph_name().ok() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.folder.get_graph_name().ok()) + .await + .unwrap() } async fn path(&self) -> String { self.folder.get_original_path_str().to_owned() } async fn created(&self) -> Result { - self.folder.created() + self.folder.created_async().await } async fn last_opened(&self) -> Result { - self.folder.last_opened() + self.folder.last_opened_async().await } async fn last_updated(&self) -> Result { - self.folder.last_updated() + self.folder.last_updated_async().await } async fn metadata(&self) -> Result { - let metadata = self.folder.read_metadata()?; - Ok(GqlGraphMetadata::from(metadata)) + let self_clone = self.clone(); + spawn_blocking(move || { + let metadata = self_clone.folder.read_metadata()?; + Ok(GqlGraphMetadata::from(metadata)) + }) + .await + .unwrap() } } #[derive(Clone, SimpleObject)] +#[graphql(name = "GraphMetadata")] pub(crate) struct GqlGraphMetadata { pub(crate) node_count: usize, pub(crate) edge_count: usize, - pub(crate) properties: Vec, + pub(crate) properties: Vec, } impl From for GqlGraphMetadata { @@ -51,7 +61,7 @@ impl From for GqlGraphMetadata { properties: metadata .properties .into_iter() - .map(|(key, prop)| GqlProp::new(key.to_string(), prop)) + .map(|(key, prop)| GqlProperty::new(key.to_string(), prop)) .collect(), } } diff --git a/raphtory-graphql/src/model/graph/meta_graphs.rs b/raphtory-graphql/src/model/graph/meta_graphs.rs new file mode 100644 index 0000000000..5c8aedea84 --- /dev/null +++ b/raphtory-graphql/src/model/graph/meta_graphs.rs @@ -0,0 +1,46 @@ +use crate::model::graph::meta_graph::MetaGraph; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use tokio::task::spawn_blocking; + +#[derive(ResolvedObject, Clone)] +pub(crate) struct MetaGraphs { + graphs: Vec, +} + +impl MetaGraphs { + pub(crate) fn new(graphs: Vec) -> Self { + Self { graphs } + } +} +#[ResolvedObjectFields] +impl MetaGraphs { + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graphs.clone()) + .await + .unwrap() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .graphs + .iter() + .map(|n| n.clone()) + .skip(start) + .take(limit) + .collect() + }) + .await + .unwrap() + } + + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.graphs.iter().count()) + .await + .unwrap() + } +} diff --git a/raphtory-graphql/src/model/graph/mod.rs b/raphtory-graphql/src/model/graph/mod.rs index e2d2152ce1..c7c73de7eb 100644 --- a/raphtory-graphql/src/model/graph/mod.rs +++ b/raphtory-graphql/src/model/graph/mod.rs @@ -1,13 +1,23 @@ +use dynamic_graphql::OneOfInput; + pub(crate) mod edge; mod edges; pub(crate) mod filtering; pub(crate) mod graph; -pub(crate) mod graphs; pub(crate) mod meta_graph; +mod meta_graphs; pub(crate) mod mutable_graph; pub(crate) mod namespace; +pub(crate) mod namespaces; pub(crate) mod node; mod nodes; mod path_from_node; pub(crate) mod property; pub(crate) mod vectorised_graph; +mod windowset; + +#[derive(OneOfInput, Clone)] +pub(crate) enum WindowDuration { + Duration(String), + Epoch(u64), +} diff --git a/raphtory-graphql/src/model/graph/mutable_graph.rs b/raphtory-graphql/src/model/graph/mutable_graph.rs index d43f8e6807..dc524834a7 100644 --- a/raphtory-graphql/src/model/graph/mutable_graph.rs +++ b/raphtory-graphql/src/model/graph/mutable_graph.rs @@ -1,6 +1,6 @@ use crate::{ graph::{GraphWithVectors, UpdateEmbeddings}, - model::graph::{edge::Edge, graph::GqlGraph, node::Node, property::Value}, + model::graph::{edge::GqlEdge, graph::GqlGraph, node::GqlNode, property::Value}, paths::ExistingGraphFolder, }; use dynamic_graphql::{InputObject, ResolvedObject, ResolvedObjectFields}; @@ -10,37 +10,40 @@ use raphtory::{ prelude::*, }; use raphtory_api::core::storage::arc_str::OptionAsStr; +use tokio::{spawn, task::spawn_blocking}; -#[derive(InputObject)] -pub struct GqlPropInput { +#[derive(InputObject, Clone)] +#[graphql(name = "PropertyInput")] +pub struct GqlPropertyInput { key: String, value: Value, } -#[derive(InputObject)] -pub struct TPropInput { +#[derive(InputObject, Clone)] +pub struct TemporalPropertyInput { time: i64, - properties: Option>, + properties: Option>, } -#[derive(InputObject)] +#[derive(InputObject, Clone)] pub struct NodeAddition { name: String, node_type: Option, - constant_properties: Option>, - updates: Option>, + constant_properties: Option>, + updates: Option>, } -#[derive(InputObject)] +#[derive(InputObject, Clone)] pub struct EdgeAddition { src: String, dst: String, layer: Option, - constant_properties: Option>, - updates: Option>, + constant_properties: Option>, + updates: Option>, } -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "MutableGraph")] pub struct GqlMutableGraph { path: ExistingGraphFolder, graph: GraphWithVectors, @@ -56,7 +59,7 @@ impl GqlMutableGraph { } fn as_properties( - properties: Vec, + properties: Vec, ) -> Result, GraphError> { let props: Result, GraphError> = properties .into_iter() @@ -88,16 +91,26 @@ impl GqlMutableGraph { &self, time: i64, name: String, - properties: Option>, + properties: Option>, node_type: Option, ) -> Result { - let prop_iter = as_properties(properties.unwrap_or(vec![]))?; - let node = self - .graph - .add_node(time, &name, prop_iter, node_type.as_str())?; + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + let node = spawn_blocking(move || { + let prop_iter = as_properties(properties.unwrap_or(vec![]))?; + self_clone + .graph + .add_node(time, &name, prop_iter, node_type.as_str()) + }) + .await + .unwrap()?; node.update_embeddings().await?; - self.graph.write_updates()?; - Ok(node.into()) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(node.into()) + }) + .await + .unwrap() } /// Create a new node or fail if it already exists @@ -105,42 +118,74 @@ impl GqlMutableGraph { &self, time: i64, name: String, - properties: Option>, + properties: Option>, node_type: Option, ) -> Result { - let prop_iter = as_properties(properties.unwrap_or(vec![]))?; - let node = self - .graph - .create_node(time, &name, prop_iter, node_type.as_str())?; + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + let node = spawn_blocking(move || { + let prop_iter = as_properties(properties.unwrap_or(vec![]))?; + self_clone + .graph + .create_node(time, &name, prop_iter, node_type.as_str()) + }) + .await + .unwrap()?; node.update_embeddings().await?; - self.graph.write_updates()?; - Ok(node.into()) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(node.into()) + }) + .await + .unwrap() } /// Add a batch of nodes async fn add_nodes(&self, nodes: Vec) -> Result { + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + + let nodes: Vec, GraphError>> = + spawn_blocking(move || { + nodes + .iter() + .map(|node| { + let node = node.clone(); + let name = node.name.as_str(); + + for prop in node.updates.unwrap_or(vec![]) { + let prop_iter = as_properties(prop.properties.unwrap_or(vec![]))?; + self_clone + .graph + .add_node(prop.time, name, prop_iter, None)?; + } + if let Some(node_type) = node.node_type.as_str() { + self_clone + .get_node_view(name.to_string())? + .set_node_type(node_type)?; + } + let constant_props = node.constant_properties.unwrap_or(vec![]); + if !constant_props.is_empty() { + let prop_iter = as_properties(constant_props)?; + self_clone + .get_node_view(name.to_string())? + .add_constant_properties(prop_iter)?; + } + self_clone.get_node_view(name.to_string()) + }) + .collect() + }) + .await + .unwrap(); for node in nodes { - let name = node.name.as_str(); - - for prop in node.updates.unwrap_or(vec![]) { - let prop_iter = as_properties(prop.properties.unwrap_or(vec![]))?; - self.graph.add_node(prop.time, name, prop_iter, None)?; - } - if let Some(node_type) = node.node_type.as_str() { - self.get_node_view(name)?.set_node_type(node_type)?; - } - let constant_props = node.constant_properties.unwrap_or(vec![]); - if !constant_props.is_empty() { - let prop_iter = as_properties(constant_props)?; - self.get_node_view(name)? - .add_constant_properties(prop_iter)?; - } - if let Ok(node) = self.get_node_view(name) { - let _ = node.update_embeddings().await; // FIXME: ideally this should call the embedding function just once!! - } + let _ = node?.update_embeddings().await; // FIXME: ideally this should call the embedding function just once!! } - self.graph.write_updates()?; - Ok(true) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Get a mutable existing edge @@ -155,40 +200,71 @@ impl GqlMutableGraph { time: i64, src: String, dst: String, - properties: Option>, + properties: Option>, layer: Option, ) -> Result { - let prop_iter = as_properties(properties.unwrap_or(vec![]))?; - let edge = self - .graph - .add_edge(time, src, dst, prop_iter, layer.as_str())?; + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + let edge = spawn_blocking(move || { + let prop_iter = as_properties(properties.unwrap_or(vec![]))?; + self_clone + .graph + .add_edge(time, src, dst, prop_iter, layer.as_str()) + }) + .await + .unwrap(); + let edge = edge?; let _ = edge.update_embeddings().await; - self.graph.write_updates()?; - Ok(edge.into()) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(edge.into()) + }) + .await + .unwrap() } /// Add a batch of edges async fn add_edges(&self, edges: Vec) -> Result { + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + + let edges: Vec, GraphError>> = + spawn_blocking(move || { + edges + .iter() + .map(|edge| { + let edge = edge.clone(); + let src = edge.src.as_str(); + let dst = edge.dst.as_str(); + let layer = edge.layer.as_str(); + for prop in edge.updates.unwrap_or(vec![]) { + let prop_iter = as_properties(prop.properties.unwrap_or(vec![]))?; + self_clone + .graph + .add_edge(prop.time, src, dst, prop_iter, layer)?; + } + let constant_props = edge.constant_properties.unwrap_or(vec![]); + if !constant_props.is_empty() { + let prop_iter = as_properties(constant_props)?; + self_clone + .get_edge_view(src.to_string(), dst.to_string())? + .add_constant_properties(prop_iter, layer)?; + } + self_clone.get_edge_view(src.to_string(), dst.to_string()) + }) + .collect() + }) + .await + .unwrap(); for edge in edges { - let src = edge.src.as_str(); - let dst = edge.dst.as_str(); - let layer = edge.layer.as_str(); - for prop in edge.updates.unwrap_or(vec![]) { - let prop_iter = as_properties(prop.properties.unwrap_or(vec![]))?; - self.graph.add_edge(prop.time, src, dst, prop_iter, layer)?; - } - let constant_props = edge.constant_properties.unwrap_or(vec![]); - if !constant_props.is_empty() { - let prop_iter = as_properties(constant_props)?; - self.get_edge_view(src, dst)? - .add_constant_properties(prop_iter, layer)?; - } - if let Ok(edge) = self.get_edge_view(src, dst) { - let _ = edge.update_embeddings().await; // FIXME: ideally this should call the embedding function just once!! - } + let _ = edge?.update_embeddings().await; // FIXME: ideally this should call the embedding function just once!! } - self.graph.write_updates()?; - Ok(true) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Mark an edge as deleted (creates the edge if it did not exist) @@ -200,46 +276,90 @@ impl GqlMutableGraph { dst: String, layer: Option, ) -> Result { - let edge = self.graph.delete_edge(time, src, dst, layer.as_str())?; + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + let edge = + spawn_blocking(move || self_clone.graph.delete_edge(time, src, dst, layer.as_str())) + .await + .unwrap(); + let edge = edge?; let _ = edge.update_embeddings().await; - self.graph.write_updates()?; - Ok(edge.into()) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(edge.into()) + }) + .await + .unwrap() } /// Add temporal properties to graph async fn add_properties( &self, t: i64, - properties: Vec, + properties: Vec, ) -> Result { - self.graph.add_properties(t, as_properties(properties)?)?; + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + spawn_blocking(move || { + self_clone + .graph + .add_properties(t, as_properties(properties)?) + }) + .await + .unwrap()?; self.update_graph_embeddings().await; - self.graph.write_updates()?; - Ok(true) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Add constant properties to graph (errors if the property already exists) async fn add_constant_properties( &self, - properties: Vec, + properties: Vec, ) -> Result { - self.graph - .add_constant_properties(as_properties(properties)?)?; + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + spawn_blocking(move || { + self_clone + .graph + .add_constant_properties(as_properties(properties)?) + }) + .await + .unwrap()?; self.update_graph_embeddings().await; - self.graph.write_updates()?; - Ok(true) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Update constant properties of the graph (overwrites existing values) async fn update_constant_properties( &self, - properties: Vec, + properties: Vec, ) -> Result { - self.graph - .update_constant_properties(as_properties(properties)?)?; + let self_clone = self.clone(); + let self_clone_2 = self.clone(); + spawn_blocking(move || { + self_clone + .graph + .update_constant_properties(as_properties(properties)?) + }) + .await + .unwrap()?; self.update_graph_embeddings().await; - self.graph.write_updates()?; - Ok(true) + spawn_blocking(move || { + self_clone_2.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } } @@ -251,27 +371,28 @@ impl GqlMutableGraph { .await; } - fn get_node_view(&self, name: &str) -> Result, GraphError> { + fn get_node_view(&self, name: String) -> Result, GraphError> { self.graph - .node(name) + .node(name.clone()) .ok_or_else(|| GraphError::NodeMissingError(GID::Str(name.to_owned()))) } fn get_edge_view( &self, - src: &str, - dst: &str, + src: String, + dst: String, ) -> Result, GraphError> { self.graph - .edge(&src, &dst) + .edge(src.clone(), dst.clone()) .ok_or(GraphError::EdgeMissingError { - src: GID::Str(src.to_owned()), - dst: GID::Str(dst.to_owned()), + src: GID::Str(src), + dst: GID::Str(dst), }) } } -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "MutableNode")] pub struct GqlMutableNode { node: NodeView, } @@ -290,57 +411,81 @@ impl GqlMutableNode { } /// Get the non-mutable `Node` - async fn node(&self) -> Node { + async fn node(&self) -> GqlNode { self.node.clone().into() } /// Add constant properties to the node (errors if the property already exists) async fn add_constant_properties( &self, - properties: Vec, + properties: Vec, ) -> Result { - self.node - .add_constant_properties(as_properties(properties)?)?; - let _ = self.node.update_embeddings().await; - self.node.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone + .node + .add_constant_properties(as_properties(properties)?)?; + let _ = self_clone.node.update_embeddings().await; + self_clone.node.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Set the node type (errors if the node already has a non-default type) async fn set_node_type(&self, new_type: String) -> Result { - self.node.set_node_type(&new_type)?; - let _ = self.node.update_embeddings().await; - self.node.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone.node.set_node_type(&new_type)?; + let _ = self_clone.node.update_embeddings().await; + self_clone.node.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Update constant properties of the node (overwrites existing property values) async fn update_constant_properties( &self, - properties: Vec, + properties: Vec, ) -> Result { - self.node - .update_constant_properties(as_properties(properties)?)?; - let _ = self.node.update_embeddings().await; - self.node.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone + .node + .update_constant_properties(as_properties(properties)?)?; + let _ = self_clone.node.update_embeddings().await; + self_clone.node.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Add temporal property updates to the node async fn add_updates( &self, time: i64, - properties: Option>, + properties: Option>, ) -> Result { - self.node - .add_updates(time, as_properties(properties.unwrap_or(vec![]))?)?; - let _ = self.node.update_embeddings().await; - self.node.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone + .node + .add_updates(time, as_properties(properties.unwrap_or(vec![]))?)?; + let _ = self_clone.node.update_embeddings().await; + self_clone.node.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } } -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "MutableEdge")] pub struct GqlMutableEdge { edge: EdgeView, } @@ -359,7 +504,7 @@ impl GqlMutableEdge { } /// Get the non-mutable edge for querying - async fn edge(&self) -> Edge { + async fn edge(&self) -> GqlEdge { self.edge.clone().into() } @@ -375,10 +520,15 @@ impl GqlMutableEdge { /// Mark the edge as deleted at time `time` async fn delete(&self, time: i64, layer: Option) -> Result { - self.edge.delete(time, layer.as_str())?; - let _ = self.edge.update_embeddings().await; - self.edge.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone.edge.delete(time, layer.as_str())?; + let _ = self_clone.edge.update_embeddings().await; + self_clone.edge.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Add constant properties to the edge (errors if the value already exists) @@ -387,14 +537,20 @@ impl GqlMutableEdge { /// need to be specified again. async fn add_constant_properties( &self, - properties: Vec, + properties: Vec, layer: Option, ) -> Result { - self.edge - .add_constant_properties(as_properties(properties)?, layer.as_str())?; - let _ = self.edge.update_embeddings().await; - self.edge.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone + .edge + .add_constant_properties(as_properties(properties)?, layer.as_str())?; + let _ = self_clone.edge.update_embeddings().await; + self_clone.edge.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Update constant properties of the edge (existing values are overwritten) @@ -403,14 +559,20 @@ impl GqlMutableEdge { /// need to be specified again. async fn update_constant_properties( &self, - properties: Vec, + properties: Vec, layer: Option, ) -> Result { - self.edge - .update_constant_properties(as_properties(properties)?, layer.as_str())?; - let _ = self.edge.update_embeddings().await; - self.edge.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone + .edge + .update_constant_properties(as_properties(properties)?, layer.as_str())?; + let _ = self_clone.edge.update_embeddings().await; + self_clone.edge.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } /// Add temporal property updates to the edge @@ -420,16 +582,21 @@ impl GqlMutableEdge { async fn add_updates( &self, time: i64, - properties: Option>, + properties: Option>, layer: Option, ) -> Result { - self.edge.add_updates( - time, - as_properties(properties.unwrap_or(vec![]))?, - layer.as_str(), - )?; - let _ = self.edge.update_embeddings().await; - self.edge.graph.write_updates()?; - Ok(true) + let self_clone = self.clone(); + spawn(async move { + self_clone.edge.add_updates( + time, + as_properties(properties.unwrap_or(vec![]))?, + layer.as_str(), + )?; + let _ = self_clone.edge.update_embeddings().await; + self_clone.edge.graph.write_updates()?; + Ok(true) + }) + .await + .unwrap() } } diff --git a/raphtory-graphql/src/model/graph/namespace.rs b/raphtory-graphql/src/model/graph/namespace.rs index e712781c6a..63695abe19 100644 --- a/raphtory-graphql/src/model/graph/namespace.rs +++ b/raphtory-graphql/src/model/graph/namespace.rs @@ -1,14 +1,16 @@ use crate::{ data::get_relative_path, - model::graph::meta_graph::MetaGraph, + model::graph::{meta_graph::MetaGraph, meta_graphs::MetaGraphs, namespaces::Namespaces}, paths::{valid_path, ExistingGraphFolder, ValidGraphFolder}, }; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use itertools::Itertools; +use raphtory::core::utils::errors::InvalidPathReason; use std::path::PathBuf; +use tokio::task::spawn_blocking; use walkdir::WalkDir; -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] pub(crate) struct Namespace { base_dir: PathBuf, current_dir: PathBuf, @@ -37,17 +39,14 @@ impl Namespace { .collect() } - pub(crate) fn get_all_children(&self) -> Vec { + pub(crate) fn get_all_namespaces(&self) -> Vec { + let base_path = self.base_dir.clone(); WalkDir::new(&self.current_dir) .into_iter() .filter_map(|e| { let entry = e.ok()?; - let file_name = entry.file_name().to_str()?; let path = entry.path(); - if path.is_dir() - && path != self.current_dir - && valid_path(self.current_dir.clone(), file_name, true).is_ok() - { + if path.is_dir() && get_relative_path(base_path.clone(), path, true).is_ok() { Some(Namespace::new(self.base_dir.clone(), path.to_path_buf())) } else { None @@ -59,51 +58,81 @@ impl Namespace { #[ResolvedObjectFields] impl Namespace { - async fn graphs(&self) -> Vec { - self.get_all_graph_folders() - .into_iter() - .sorted_by(|a, b| { - let a_as_valid_folder: ValidGraphFolder = a.clone().into(); - let b_as_valid_folder: ValidGraphFolder = b.clone().into(); - a_as_valid_folder - .get_original_path_str() - .cmp(b_as_valid_folder.get_original_path_str()) - }) - .map(|g| MetaGraph::new(g.clone())) - .collect() + async fn graphs(&self) -> MetaGraphs { + let self_clone = self.clone(); + spawn_blocking(move || { + MetaGraphs::new( + self_clone + .get_all_graph_folders() + .into_iter() + .sorted_by(|a, b| { + let a_as_valid_folder: ValidGraphFolder = a.clone().into(); + let b_as_valid_folder: ValidGraphFolder = b.clone().into(); + a_as_valid_folder + .get_original_path_str() + .cmp(b_as_valid_folder.get_original_path_str()) + }) + .map(|g| MetaGraph::new(g.clone())) + .collect(), + ) + }) + .await + .unwrap() } - async fn path(&self) -> Option { - get_relative_path(self.base_dir.clone(), self.current_dir.as_path(), true) - .ok() - .map(|s| s.to_string()) + async fn path(&self) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || { + get_relative_path( + self_clone.base_dir.clone(), + self_clone.current_dir.as_path(), + true, + ) + }) + .await + .unwrap() } async fn parent(&self) -> Option { - let parent = self.current_dir.parent()?.to_path_buf(); - if parent.starts_with(&self.base_dir) { - Some(Namespace::new(self.base_dir.clone(), parent)) - } else { - None - } + let self_clone = self.clone(); + spawn_blocking(move || { + let parent = self_clone.current_dir.parent()?.to_path_buf(); + if parent.starts_with(&self_clone.base_dir) { + Some(Namespace::new(self_clone.base_dir.clone(), parent)) + } else { + None + } + }) + .await + .unwrap() } - async fn children(&self) -> Vec { - WalkDir::new(&self.current_dir) - .max_depth(1) - .into_iter() - .filter_map(|e| { - let entry = e.ok()?; - let file_name = entry.file_name().to_str()?; - let path = entry.path(); - if path.is_dir() - && path != self.current_dir - && valid_path(self.current_dir.clone(), file_name, true).is_ok() - { - Some(Namespace::new(self.base_dir.clone(), path.to_path_buf())) - } else { - None - } - }) - .collect() + async fn children(&self) -> Namespaces { + let self_clone = self.clone(); + spawn_blocking(move || { + Namespaces::new( + WalkDir::new(&self_clone.current_dir) + .max_depth(1) + .into_iter() + .filter_map(|e| { + let entry = e.ok()?; + let file_name = entry.file_name().to_str()?; + let path = entry.path(); + if path.is_dir() + && path != self_clone.current_dir + && valid_path(self_clone.current_dir.clone(), file_name, true).is_ok() + { + Some(Namespace::new( + self_clone.base_dir.clone(), + path.to_path_buf(), + )) + } else { + None + } + }) + .collect(), + ) + }) + .await + .unwrap() } } diff --git a/raphtory-graphql/src/model/graph/namespaces.rs b/raphtory-graphql/src/model/graph/namespaces.rs new file mode 100644 index 0000000000..eb3fb9dd9b --- /dev/null +++ b/raphtory-graphql/src/model/graph/namespaces.rs @@ -0,0 +1,44 @@ +use crate::model::graph::namespace::Namespace; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use tokio::task::spawn_blocking; + +#[derive(ResolvedObject, Clone)] +pub(crate) struct Namespaces { + namespaces: Vec, +} + +impl Namespaces { + pub(crate) fn new(namespaces: Vec) -> Self { + Self { namespaces } + } +} + +#[ResolvedObjectFields] +impl Namespaces { + async fn list(&self) -> Vec { + self.namespaces.clone() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .namespaces + .iter() + .map(|n| n.clone()) + .skip(start) + .take(limit) + .collect() + }) + .await + .unwrap() + } + + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.namespaces.iter().count()) + .await + .unwrap() + } +} diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index bf4c5b2755..826fa4184b 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -1,25 +1,33 @@ use crate::model::graph::{ - edges::GqlEdges, filtering::NodeViewCollection, nodes::GqlNodes, - path_from_node::GqlPathFromNode, property::GqlProperties, + edges::GqlEdges, + filtering::NodeViewCollection, + nodes::GqlNodes, + path_from_node::GqlPathFromNode, + property::GqlProperties, + windowset::GqlNodeWindowSet, + WindowDuration, + WindowDuration::{Duration, Epoch}, }; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{ algorithms::components::{in_component, out_component}, - core::utils::errors::GraphError, + core::utils::errors::{GraphError, GraphError::MismatchedIntervalTypes}, db::{ api::{properties::dyn_props::DynProperties, view::*}, graph::node::NodeView, }, prelude::NodeStateOps, }; +use tokio::task::spawn_blocking; #[derive(ResolvedObject, Clone)] -pub struct Node { +#[graphql(name = "Node")] +pub struct GqlNode { pub(crate) vv: NodeView, } impl - From> for Node + From> for GqlNode { fn from(value: NodeView) -> Self { Self { @@ -33,7 +41,7 @@ impl } #[ResolvedObjectFields] -impl Node { +impl GqlNode { async fn id(&self) -> String { self.vv.id().to_string() } @@ -45,50 +53,110 @@ impl Node { //////////////////////// // LAYERS AND WINDOWS // //////////////////////// - async fn default_layer(&self) -> Node { + async fn default_layer(&self) -> GqlNode { self.vv.default_layer().into() } - async fn layers(&self, names: Vec) -> Node { - self.vv.valid_layers(names).into() + async fn layers(&self, names: Vec) -> GqlNode { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.valid_layers(names).into()) + .await + .unwrap() } - async fn exclude_layers(&self, names: Vec) -> Node { - self.vv.exclude_valid_layers(names).into() + async fn exclude_layers(&self, names: Vec) -> GqlNode { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.exclude_valid_layers(names).into()) + .await + .unwrap() } - async fn layer(&self, name: String) -> Node { + async fn layer(&self, name: String) -> GqlNode { self.vv.valid_layers(name).into() } - async fn exclude_layer(&self, name: String) -> Node { + async fn exclude_layer(&self, name: String) -> GqlNode { self.vv.exclude_valid_layers(name).into() } - async fn window(&self, start: i64, end: i64) -> Node { + async fn rolling( + &self, + window: WindowDuration, + step: Option, + ) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match window { + Duration(window_duration) => match step { + Some(step) => match step { + Duration(step_duration) => Ok(GqlNodeWindowSet::new( + self_clone + .vv + .rolling(window_duration, Some(step_duration))?, + )), + Epoch(_) => Err(MismatchedIntervalTypes), + }, + None => Ok(GqlNodeWindowSet::new( + self_clone.vv.rolling(window_duration, None)?, + )), + }, + Epoch(window_duration) => match step { + Some(step) => match step { + Duration(_) => Err(MismatchedIntervalTypes), + Epoch(step_duration) => Ok(GqlNodeWindowSet::new( + self_clone + .vv + .rolling(window_duration, Some(step_duration))?, + )), + }, + None => Ok(GqlNodeWindowSet::new( + self_clone.vv.rolling(window_duration, None)?, + )), + }, + }) + .await + .unwrap() + } + + async fn expanding(&self, step: WindowDuration) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match step { + Duration(step) => Ok(GqlNodeWindowSet::new(self_clone.vv.expanding(step)?)), + Epoch(step) => Ok(GqlNodeWindowSet::new(self_clone.vv.expanding(step)?)), + }) + .await + .unwrap() + } + + async fn window(&self, start: i64, end: i64) -> GqlNode { self.vv.window(start, end).into() } - async fn at(&self, time: i64) -> Node { + async fn at(&self, time: i64) -> GqlNode { self.vv.at(time).into() } - async fn latest(&self) -> Node { - self.vv.latest().into() + async fn latest(&self) -> GqlNode { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.latest().into()) + .await + .unwrap() } - async fn snapshot_at(&self, time: i64) -> Node { + async fn snapshot_at(&self, time: i64) -> GqlNode { self.vv.snapshot_at(time).into() } - async fn snapshot_latest(&self) -> Node { - self.vv.snapshot_latest().into() + async fn snapshot_latest(&self) -> GqlNode { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.snapshot_latest().into()) + .await + .unwrap() } - async fn before(&self, time: i64) -> Node { + async fn before(&self, time: i64) -> GqlNode { self.vv.before(time).into() } - async fn after(&self, time: i64) -> Node { + async fn after(&self, time: i64) -> GqlNode { self.vv.after(time).into() } @@ -104,9 +172,8 @@ impl Node { self.vv.shrink_end(end).into() } - async fn apply_views(&self, views: Vec) -> Result { - let mut return_view: Node = self.vv.clone().into(); - + async fn apply_views(&self, views: Vec) -> Result { + let mut return_view: GqlNode = self.vv.clone().into(); for view in views { let mut count = 0; if let Some(_) = view.default_layer { @@ -174,7 +241,6 @@ impl Node { return Err(GraphError::TooManyViewsSet); } } - Ok(return_view) } @@ -183,19 +249,31 @@ impl Node { //////////////////////// async fn earliest_time(&self) -> Option { - self.vv.earliest_time() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.earliest_time()) + .await + .unwrap() } async fn first_update(&self) -> Option { - self.vv.history().first().cloned() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.history().first().cloned()) + .await + .unwrap() } async fn latest_time(&self) -> Option { - self.vv.latest_time() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.latest_time()) + .await + .unwrap() } async fn last_update(&self) -> Option { - self.vv.history().last().cloned() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.history().last().cloned()) + .await + .unwrap() } async fn start(&self) -> Option { @@ -207,11 +285,17 @@ impl Node { } async fn history(&self) -> Vec { - self.vv.history() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.history()) + .await + .unwrap() } async fn is_active(&self) -> bool { - self.vv.is_active() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.is_active()) + .await + .unwrap() } //////////////////////// @@ -235,27 +319,42 @@ impl Node { /// Returns the number of edges connected to this node async fn degree(&self) -> usize { - self.vv.degree() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.degree()) + .await + .unwrap() } /// Returns the number edges with this node as the source async fn out_degree(&self) -> usize { - self.vv.out_degree() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.out_degree()) + .await + .unwrap() } /// Returns the number edges with this node as the destination async fn in_degree(&self) -> usize { - self.vv.in_degree() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.vv.in_degree()) + .await + .unwrap() } async fn in_component(&self) -> GqlNodes { - GqlNodes::new(in_component(self.vv.clone()).nodes()) + let self_clone = self.clone(); + spawn_blocking(move || GqlNodes::new(in_component(self_clone.vv.clone()).nodes())) + .await + .unwrap() } async fn out_component(&self) -> GqlNodes { - GqlNodes::new(out_component(self.vv.clone()).nodes()) + let self_clone = self.clone(); + spawn_blocking(move || GqlNodes::new(out_component(self_clone.vv.clone()).nodes())) + .await + .unwrap() } async fn edges(&self) -> GqlEdges { diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index 59081e0170..37b5576034 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -1,14 +1,17 @@ use crate::model::{ graph::{ filtering::{NodeFilter, NodesViewCollection}, - node::Node, + node::GqlNode, + windowset::GqlNodesWindowSet, + WindowDuration, + WindowDuration::{Duration, Epoch}, }, sorting::{NodeSortBy, SortByTime}, }; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use itertools::Itertools; use raphtory::{ - core::utils::errors::GraphError, + core::utils::errors::{GraphError, GraphError::MismatchedIntervalTypes}, db::{ api::{state::Index, view::DynamicGraph}, graph::{nodes::Nodes, views::filter::model::node_filter::CompositeNodeFilter}, @@ -17,8 +20,10 @@ use raphtory::{ }; use raphtory_api::core::entities::VID; use std::cmp::Ordering; +use tokio::task::spawn_blocking; -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "Nodes")] pub(crate) struct GqlNodes { pub(crate) nn: Nodes<'static, DynamicGraph>, } @@ -34,8 +39,8 @@ impl GqlNodes { Self { nn: nodes.into() } } - fn iter(&self) -> Box + '_> { - let iter = self.nn.iter_owned().map(Node::from); + fn iter(&self) -> Box + '_> { + let iter = self.nn.iter_owned().map(GqlNode::from); Box::new(iter) } } @@ -54,7 +59,10 @@ impl GqlNodes { } async fn exclude_layers(&self, names: Vec) -> Self { - self.update(self.nn.exclude_valid_layers(names)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.exclude_valid_layers(names))) + .await + .unwrap() } async fn layer(&self, name: String) -> Self { @@ -65,6 +73,54 @@ impl GqlNodes { self.update(self.nn.exclude_valid_layers(name)) } + async fn rolling( + &self, + window: WindowDuration, + step: Option, + ) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match window { + Duration(window_duration) => match step { + Some(step) => match step { + Duration(step_duration) => Ok(GqlNodesWindowSet::new( + self_clone + .nn + .rolling(window_duration, Some(step_duration))?, + )), + Epoch(_) => Err(MismatchedIntervalTypes), + }, + None => Ok(GqlNodesWindowSet::new( + self_clone.nn.rolling(window_duration, None)?, + )), + }, + Epoch(window_duration) => match step { + Some(step) => match step { + Duration(_) => Err(MismatchedIntervalTypes), + Epoch(step_duration) => Ok(GqlNodesWindowSet::new( + self_clone + .nn + .rolling(window_duration, Some(step_duration))?, + )), + }, + None => Ok(GqlNodesWindowSet::new( + self_clone.nn.rolling(window_duration, None)?, + )), + }, + }) + .await + .unwrap() + } + + async fn expanding(&self, step: WindowDuration) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match step { + Duration(step) => Ok(GqlNodesWindowSet::new(self_clone.nn.expanding(step)?)), + Epoch(step) => Ok(GqlNodesWindowSet::new(self_clone.nn.expanding(step)?)), + }) + .await + .unwrap() + } + async fn window(&self, start: i64, end: i64) -> Self { self.update(self.nn.window(start, end)) } @@ -74,7 +130,10 @@ impl GqlNodes { } async fn latest(&self) -> Self { - self.update(self.nn.latest()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.latest())) + .await + .unwrap() } async fn snapshot_at(&self, time: i64) -> Self { @@ -82,7 +141,10 @@ impl GqlNodes { } async fn snapshot_latest(&self) -> Self { - self.update(self.nn.snapshot_latest()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.snapshot_latest())) + .await + .unwrap() } async fn before(&self, time: i64) -> Self { @@ -106,19 +168,26 @@ impl GqlNodes { } async fn type_filter(&self, node_types: Vec) -> Self { - self.update(self.nn.type_filter(&node_types)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.type_filter(&node_types))) + .await + .unwrap() } async fn node_filter(&self, filter: NodeFilter) -> Result { - filter.validate()?; - let filter: CompositeNodeFilter = filter.try_into()?; - let filtered_nodes = self.nn.filter_nodes(filter)?; - Ok(self.update(filtered_nodes.into_dyn())) + let self_clone = self.clone(); + spawn_blocking(move || { + filter.validate()?; + let filter: CompositeNodeFilter = filter.try_into()?; + let filtered_nodes = self_clone.nn.filter_nodes(filter)?; + Ok(self_clone.update(filtered_nodes.into_dyn())) + }) + .await + .unwrap() } async fn apply_views(&self, views: Vec) -> Result { let mut return_view: GqlNodes = GqlNodes::new(self.nn.clone()); - for view in views { let mut count = 0; if let Some(_) = view.default_layer { @@ -203,50 +272,56 @@ impl GqlNodes { ///////////////// async fn sorted(&self, sort_bys: Vec) -> Self { - let sorted: Index = self - .nn - .iter() - .sorted_by(|first_node, second_node| { - sort_bys - .iter() - .fold(Ordering::Equal, |current_ordering, sort_by| { - current_ordering.then_with(|| { - let ordering = if sort_by.id == Some(true) { - first_node.id().partial_cmp(&second_node.id()) - } else if let Some(sort_by_time) = sort_by.time.as_ref() { - let (first_time, second_time) = match sort_by_time { - SortByTime::Latest => { - (first_node.latest_time(), second_node.latest_time()) - } - SortByTime::Earliest => { - (first_node.earliest_time(), second_node.earliest_time()) - } + let self_clone = self.clone(); + spawn_blocking(move || { + let sorted: Index = self_clone + .nn + .iter() + .sorted_by(|first_node, second_node| { + sort_bys + .iter() + .fold(Ordering::Equal, |current_ordering, sort_by| { + current_ordering.then_with(|| { + let ordering = if sort_by.id == Some(true) { + first_node.id().partial_cmp(&second_node.id()) + } else if let Some(sort_by_time) = sort_by.time.as_ref() { + let (first_time, second_time) = match sort_by_time { + SortByTime::Latest => { + (first_node.latest_time(), second_node.latest_time()) + } + SortByTime::Earliest => ( + first_node.earliest_time(), + second_node.earliest_time(), + ), + }; + first_time.partial_cmp(&second_time) + } else if let Some(sort_by_property) = sort_by.property.as_ref() { + let first_prop_maybe = + first_node.properties().get(sort_by_property); + let second_prop_maybe = + second_node.properties().get(sort_by_property); + first_prop_maybe.partial_cmp(&second_prop_maybe) + } else { + None }; - first_time.partial_cmp(&second_time) - } else if let Some(sort_by_property) = sort_by.property.as_ref() { - let first_prop_maybe = - first_node.properties().get(sort_by_property); - let second_prop_maybe = - second_node.properties().get(sort_by_property); - first_prop_maybe.partial_cmp(&second_prop_maybe) - } else { - None - }; - if let Some(ordering) = ordering { - if sort_by.reverse == Some(true) { - ordering.reverse() + if let Some(ordering) = ordering { + if sort_by.reverse == Some(true) { + ordering.reverse() + } else { + ordering + } } else { - ordering + Ordering::Equal } - } else { - Ordering::Equal - } + }) }) - }) - }) - .map(|node_view| node_view.node) - .collect(); - GqlNodes::new(self.nn.indexed(sorted)) + }) + .map(|node_view| node_view.node) + .collect(); + GqlNodes::new(self_clone.nn.indexed(sorted)) + }) + .await + .unwrap() } //////////////////////// @@ -266,19 +341,33 @@ impl GqlNodes { ///////////////// async fn count(&self) -> usize { - self.iter().count() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.iter().count()) + .await + .unwrap() } - async fn page(&self, limit: usize, offset: usize) -> Vec { - let start = offset * limit; - self.iter().skip(start).take(limit).collect() + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone.iter().skip(start).take(limit).collect() + }) + .await + .unwrap() } - async fn list(&self) -> Vec { - self.iter().collect() + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.iter().collect()) + .await + .unwrap() } async fn ids(&self) -> Vec { - self.nn.name().collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.nn.name().collect()) + .await + .unwrap() } } diff --git a/raphtory-graphql/src/model/graph/path_from_node.rs b/raphtory-graphql/src/model/graph/path_from_node.rs index f5befd9ff2..480e17452f 100644 --- a/raphtory-graphql/src/model/graph/path_from_node.rs +++ b/raphtory-graphql/src/model/graph/path_from_node.rs @@ -1,11 +1,19 @@ -use crate::model::graph::node::Node; +use crate::model::graph::{ + node::GqlNode, + windowset::GqlPathFromNodeWindowSet, + WindowDuration, + WindowDuration::{Duration, Epoch}, +}; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{ + core::utils::errors::{GraphError, GraphError::MismatchedIntervalTypes}, db::{api::view::DynamicGraph, graph::path::PathFromNode}, prelude::*, }; +use tokio::task::spawn_blocking; -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "PathFromNode")] pub(crate) struct GqlPathFromNode { pub(crate) nn: PathFromNode<'static, DynamicGraph, DynamicGraph>, } @@ -23,8 +31,8 @@ impl GqlPathFromNode { Self { nn: nodes.into() } } - fn iter(&self) -> Box + '_> { - let iter = self.nn.iter().map(Node::from); + fn iter(&self) -> Box + '_> { + let iter = self.nn.iter().map(GqlNode::from); Box::new(iter) } } @@ -36,11 +44,17 @@ impl GqlPathFromNode { //////////////////////// async fn layers(&self, names: Vec) -> Self { - self.update(self.nn.valid_layers(names)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.valid_layers(names))) + .await + .unwrap() } async fn exclude_layers(&self, names: Vec) -> Self { - self.update(self.nn.exclude_valid_layers(names)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.exclude_valid_layers(names))) + .await + .unwrap() } async fn layer(&self, name: String) -> Self { @@ -51,6 +65,61 @@ impl GqlPathFromNode { self.update(self.nn.exclude_valid_layers(name)) } + async fn rolling( + &self, + window: WindowDuration, + step: Option, + ) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match window { + Duration(window_duration) => match step { + Some(step) => match step { + Duration(step_duration) => Ok(GqlPathFromNodeWindowSet::new( + self_clone + .nn + .rolling(window_duration, Some(step_duration))?, + )), + Epoch(_) => Err(MismatchedIntervalTypes), + }, + None => Ok(GqlPathFromNodeWindowSet::new( + self_clone.nn.rolling(window_duration, None)?, + )), + }, + Epoch(window_duration) => match step { + Some(step) => match step { + Duration(_) => Err(MismatchedIntervalTypes), + Epoch(step_duration) => Ok(GqlPathFromNodeWindowSet::new( + self_clone + .nn + .rolling(window_duration, Some(step_duration))?, + )), + }, + None => Ok(GqlPathFromNodeWindowSet::new( + self_clone.nn.rolling(window_duration, None)?, + )), + }, + }) + .await + .unwrap() + } + + async fn expanding( + &self, + step: WindowDuration, + ) -> Result { + let self_clone = self.clone(); + spawn_blocking(move || match step { + Duration(step) => Ok(GqlPathFromNodeWindowSet::new( + self_clone.nn.expanding(step)?, + )), + Epoch(step) => Ok(GqlPathFromNodeWindowSet::new( + self_clone.nn.expanding(step)?, + )), + }) + .await + .unwrap() + } + async fn window(&self, start: i64, end: i64) -> Self { self.update(self.nn.window(start, end)) } @@ -60,14 +129,20 @@ impl GqlPathFromNode { } async fn snapshot_latest(&self) -> Self { - self.update(self.nn.snapshot_latest()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.snapshot_latest())) + .await + .unwrap() } async fn snapshot_at(&self, time: i64) -> Self { self.update(self.nn.snapshot_at(time)) } async fn latest(&self) -> Self { - self.update(self.nn.latest()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.latest())) + .await + .unwrap() } async fn before(&self, time: i64) -> Self { @@ -90,7 +165,10 @@ impl GqlPathFromNode { } async fn type_filter(&self, node_types: Vec) -> Self { - self.update(self.nn.type_filter(&node_types)) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.update(self_clone.nn.type_filter(&node_types))) + .await + .unwrap() } //////////////////////// @@ -110,19 +188,33 @@ impl GqlPathFromNode { ///////////////// async fn count(&self) -> usize { - self.iter().count() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.iter().count()) + .await + .unwrap() } - async fn page(&self, limit: usize, offset: usize) -> Vec { - let start = offset * limit; - self.iter().skip(start).take(limit).collect() + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone.iter().skip(start).take(limit).collect() + }) + .await + .unwrap() } - async fn list(&self) -> Vec { - self.iter().collect() + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.iter().collect()) + .await + .unwrap() } async fn ids(&self) -> Vec { - self.nn.name().collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.nn.name().collect()) + .await + .unwrap() } } diff --git a/raphtory-graphql/src/model/graph/property.rs b/raphtory-graphql/src/model/graph/property.rs index d5c95db7ab..6dc2cabea7 100644 --- a/raphtory-graphql/src/model/graph/property.rs +++ b/raphtory-graphql/src/model/graph/property.rs @@ -12,6 +12,7 @@ use raphtory_api::core::storage::arc_str::ArcStr; use rustc_hash::FxHashMap; use serde_json::Number; use std::{collections::HashMap, convert::TryFrom, sync::Arc}; +use tokio::task::spawn_blocking; #[derive(InputObject, Clone, Debug, Default)] pub struct ObjectEntry { @@ -72,11 +73,12 @@ fn value_to_prop(value: Value) -> Result { } #[derive(Clone, Debug, Scalar)] -pub struct GqlPropOutputVal(pub Prop); +#[graphql(name = "PropertyOutput")] +pub struct GqlPropertyOutputVal(pub Prop); -impl ScalarValue for GqlPropOutputVal { - fn from_value(value: GqlValue) -> Result { - Ok(GqlPropOutputVal(gql_to_prop(value)?)) +impl ScalarValue for GqlPropertyOutputVal { + fn from_value(value: GqlValue) -> Result { + Ok(GqlPropertyOutputVal(gql_to_prop(value)?)) } fn to_value(&self) -> GqlValue { @@ -142,25 +144,26 @@ fn prop_to_gql(prop: &Prop) -> GqlValue { } #[derive(Clone, ResolvedObject)] -pub(crate) struct GqlProp { +#[graphql(name = "Property")] +pub(crate) struct GqlProperty { key: String, prop: Prop, } -impl GqlProp { +impl GqlProperty { pub(crate) fn new(key: String, prop: Prop) -> Self { Self { key, prop } } } -impl From<(String, Prop)> for GqlProp { +impl From<(String, Prop)> for GqlProperty { fn from(value: (String, Prop)) -> Self { - GqlProp::new(value.0, value.1) + GqlProperty::new(value.0, value.1) } } #[ResolvedObjectFields] -impl GqlProp { +impl GqlProperty { async fn key(&self) -> String { self.key.clone() } @@ -169,100 +172,130 @@ impl GqlProp { self.prop.to_string() } - async fn value(&self) -> GqlPropOutputVal { - GqlPropOutputVal(self.prop.clone()) + async fn value(&self) -> GqlPropertyOutputVal { + GqlPropertyOutputVal(self.prop.clone()) } } -#[derive(ResolvedObject)] -pub(crate) struct GqlPropTuple { +#[derive(ResolvedObject, Clone)] +#[graphql(name = "PropertyTuple")] +pub(crate) struct GqlPropertyTuple { time: i64, prop: Prop, } -impl GqlPropTuple { +impl GqlPropertyTuple { pub(crate) fn new(time: i64, prop: Prop) -> Self { Self { time, prop } } } -impl From<(i64, Prop)> for GqlPropTuple { +impl From<(i64, Prop)> for GqlPropertyTuple { fn from(value: (i64, Prop)) -> Self { - GqlPropTuple::new(value.0, value.1) + GqlPropertyTuple::new(value.0, value.1) } } #[ResolvedObjectFields] -impl GqlPropTuple { +impl GqlPropertyTuple { async fn time(&self) -> i64 { self.time } async fn as_string(&self) -> String { - self.prop.to_string() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.prop.to_string()) + .await + .unwrap() } - async fn value(&self) -> GqlPropOutputVal { - GqlPropOutputVal(self.prop.clone()) + async fn value(&self) -> GqlPropertyOutputVal { + GqlPropertyOutputVal(self.prop.clone()) } } -#[derive(ResolvedObject)] -pub(crate) struct GqlTemporalProp { +#[derive(ResolvedObject, Clone)] +#[graphql(name = "TemporalProperty")] +pub(crate) struct GqlTemporalProperty { key: String, prop: TemporalPropertyView, } -impl GqlTemporalProp { +impl GqlTemporalProperty { pub(crate) fn new(key: String, prop: TemporalPropertyView) -> Self { Self { key, prop } } } -impl From<(String, TemporalPropertyView)> for GqlTemporalProp { +impl From<(String, TemporalPropertyView)> for GqlTemporalProperty { fn from(value: (String, TemporalPropertyView)) -> Self { - GqlTemporalProp::new(value.0, value.1) + GqlTemporalProperty::new(value.0, value.1) } } #[ResolvedObjectFields] -impl GqlTemporalProp { +impl GqlTemporalProperty { async fn key(&self) -> String { self.key.clone() } async fn history(&self) -> Vec { - self.prop.history().collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.prop.history().collect()) + .await + .unwrap() } async fn values(&self) -> Vec { - self.prop.values().map(|x| x.to_string()).collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.prop.values().map(|x| x.to_string()).collect()) + .await + .unwrap() } async fn at(&self, t: i64) -> Option { - self.prop.at(t).map(|x| x.to_string()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.prop.at(t).map(|x| x.to_string())) + .await + .unwrap() } async fn latest(&self) -> Option { - self.prop.latest().map(|x| x.to_string()) + let self_clone = self.clone(); + spawn_blocking(move || self_clone.prop.latest().map(|x| x.to_string())) + .await + .unwrap() } async fn unique(&self) -> Vec { - self.prop - .unique() - .into_iter() - .map(|x| x.to_string()) - .collect_vec() - } - - async fn ordered_dedupe(&self, latest_time: bool) -> Vec { - self.prop - .ordered_dedupe(latest_time) - .into_iter() - .map(|(k, p)| (k, p).into()) - .collect() + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .prop + .unique() + .into_iter() + .map(|x| x.to_string()) + .collect_vec() + }) + .await + .unwrap() + } + + async fn ordered_dedupe(&self, latest_time: bool) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .prop + .ordered_dedupe(latest_time) + .into_iter() + .map(|(k, p)| (k, p).into()) + .collect() + }) + .await + .unwrap() } } -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "Properties")] pub(crate) struct GqlProperties { props: DynProperties, } @@ -281,7 +314,8 @@ impl> From

for GqlProperties { } } -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "TemporalProperties")] pub(crate) struct GqlTemporalProperties { props: DynTemporalProperties, } @@ -296,7 +330,8 @@ impl From for GqlTemporalProperties { } } -#[derive(ResolvedObject)] +#[derive(ResolvedObject, Clone)] +#[graphql(name = "ConstantProperties")] pub(crate) struct GqlConstantProperties { props: DynConstProperties, } @@ -313,21 +348,36 @@ impl From for GqlConstantProperties { #[ResolvedObjectFields] impl GqlProperties { - async fn get(&self, key: &str) -> Option { - self.props.get(key).map(|p| (key.to_string(), p).into()) + async fn get(&self, key: String) -> Option { + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .props + .get(key.as_str()) + .map(|p| (key.to_string(), p).into()) + }) + .await + .unwrap() } - async fn contains(&self, key: &str) -> bool { - self.props.contains(key) + async fn contains(&self, key: String) -> bool { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.props.contains(key.as_str())) + .await + .unwrap() } async fn keys(&self) -> Vec { - self.props.keys().map(|k| k.into()).collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.props.keys().map(|k| k.into()).collect()) + .await + .unwrap() } - async fn values(&self, keys: Option>) -> Vec { - match keys { - Some(keys) => self + async fn values(&self, keys: Option>) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || match keys { + Some(keys) => self_clone .props .iter() .filter_map(|(k, p)| { @@ -339,40 +389,57 @@ impl GqlProperties { } }) .collect(), - None => self + None => self_clone .props .iter() .filter_map(|(k, p)| p.map(|prop| (k.to_string(), prop).into())) .collect(), - } + }) + .await + .unwrap() } async fn temporal(&self) -> GqlTemporalProperties { self.props.temporal().into() } - pub(crate) async fn constant(&self) -> GqlConstantProperties { + async fn constant(&self) -> GqlConstantProperties { self.props.constant().into() } } #[ResolvedObjectFields] impl GqlConstantProperties { - async fn get(&self, key: &str) -> Option { - self.props.get(key).map(|p| (key.to_string(), p).into()) + async fn get(&self, key: String) -> Option { + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .props + .get(key.as_str()) + .map(|p| (key.to_string(), p).into()) + }) + .await + .unwrap() } - async fn contains(&self, key: &str) -> bool { - self.props.contains(key) + async fn contains(&self, key: String) -> bool { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.props.contains(key.as_str())) + .await + .unwrap() } async fn keys(&self) -> Vec { - self.props.keys().map(|k| k.clone().into()).collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.props.keys().map(|k| k.clone().into()).collect()) + .await + .unwrap() } - pub(crate) async fn values(&self, keys: Option>) -> Vec { - match keys { - Some(keys) => self + pub(crate) async fn values(&self, keys: Option>) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || match keys { + Some(keys) => self_clone .props .iter() .filter_map(|(k, p)| { @@ -384,32 +451,49 @@ impl GqlConstantProperties { } }) .collect(), - None => self + None => self_clone .props .iter() .map(|(k, p)| (k.to_string(), p).into()) .collect(), - } + }) + .await + .unwrap() } } #[ResolvedObjectFields] impl GqlTemporalProperties { - async fn get(&self, key: &str) -> Option { - self.props.get(key).map(|p| (key.to_string(), p).into()) + async fn get(&self, key: String) -> Option { + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .props + .get(key.as_str()) + .map(|p| (key.to_string(), p).into()) + }) + .await + .unwrap() } - async fn contains(&self, key: &str) -> bool { - self.props.contains(key) + async fn contains(&self, key: String) -> bool { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.props.contains(key.as_str())) + .await + .unwrap() } async fn keys(&self) -> Vec { - self.props.keys().map(|k| k.into()).collect() + let self_clone = self.clone(); + spawn_blocking(move || self_clone.props.keys().map(|k| k.into()).collect()) + .await + .unwrap() } - async fn values(&self, keys: Option>) -> Vec { - match keys { - Some(keys) => self + async fn values(&self, keys: Option>) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || match keys { + Some(keys) => self_clone .props .iter() .filter_map(|(k, p)| { @@ -421,11 +505,13 @@ impl GqlTemporalProperties { } }) .collect(), - None => self + None => self_clone .props .iter() .map(|(k, p)| (k.to_string(), p).into()) .collect(), - } + }) + .await + .unwrap() } } diff --git a/raphtory-graphql/src/model/graph/vectorised_graph.rs b/raphtory-graphql/src/model/graph/vectorised_graph.rs index fd1a97bc78..8fdefd22c0 100644 --- a/raphtory-graphql/src/model/graph/vectorised_graph.rs +++ b/raphtory-graphql/src/model/graph/vectorised_graph.rs @@ -3,6 +3,7 @@ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{db::api::view::MaterializedGraph, vectors::vectorised_graph::VectorisedGraph}; #[derive(ResolvedObject)] +#[graphql(name = "VectorisedGraph")] pub(crate) struct GqlVectorisedGraph { graph: VectorisedGraph, } diff --git a/raphtory-graphql/src/model/graph/windowset.rs b/raphtory-graphql/src/model/graph/windowset.rs new file mode 100644 index 0000000000..88a15d9f4f --- /dev/null +++ b/raphtory-graphql/src/model/graph/windowset.rs @@ -0,0 +1,290 @@ +use crate::{ + model::graph::{ + edge::GqlEdge, edges::GqlEdges, graph::GqlGraph, node::GqlNode, nodes::GqlNodes, + path_from_node::GqlPathFromNode, + }, + paths::ExistingGraphFolder, +}; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use raphtory::db::{ + api::view::{DynamicGraph, WindowSet}, + graph::{edge::EdgeView, edges::Edges, node::NodeView, nodes::Nodes, path::PathFromNode}, +}; +use tokio::task::spawn_blocking; + +#[derive(ResolvedObject, Clone)] +#[graphql(name = "GraphWindowSet")] +pub(crate) struct GqlGraphWindowSet { + pub(crate) ws: WindowSet<'static, DynamicGraph>, + path: ExistingGraphFolder, +} + +impl GqlGraphWindowSet { + pub(crate) fn new(ws: WindowSet<'static, DynamicGraph>, path: ExistingGraphFolder) -> Self { + Self { ws, path } + } +} +#[ResolvedObjectFields] +impl GqlGraphWindowSet { + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().count()) + .await + .unwrap() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .ws + .clone() + .skip(start) + .take(limit) + .map(|g| GqlGraph::new(self_clone.path.clone(), g)) + .collect() + }) + .await + .unwrap() + } + + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .ws + .clone() + .map(|g| GqlGraph::new(self_clone.path.clone(), g)) + .collect() + }) + .await + .unwrap() + } +} + +#[derive(ResolvedObject, Clone)] +#[graphql(name = "NodeWindowSet")] +pub(crate) struct GqlNodeWindowSet { + pub(crate) ws: WindowSet<'static, NodeView>, +} + +impl GqlNodeWindowSet { + pub(crate) fn new(ws: WindowSet<'static, NodeView>) -> Self { + Self { ws } + } +} +#[ResolvedObjectFields] +impl GqlNodeWindowSet { + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().count()) + .await + .unwrap() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .ws + .clone() + .skip(start) + .take(limit) + .map(|n| n.into()) + .collect() + }) + .await + .unwrap() + } + + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().map(|n| n.into()).collect()) + .await + .unwrap() + } +} + +#[derive(ResolvedObject, Clone)] +#[graphql(name = "NodesWindowSet")] +pub(crate) struct GqlNodesWindowSet { + pub(crate) ws: WindowSet<'static, Nodes<'static, DynamicGraph, DynamicGraph>>, +} + +impl GqlNodesWindowSet { + pub(crate) fn new(ws: WindowSet<'static, Nodes>) -> Self { + Self { ws } + } +} +#[ResolvedObjectFields] +impl GqlNodesWindowSet { + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().count()) + .await + .unwrap() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .ws + .clone() + .skip(start) + .take(limit) + .map(|n| GqlNodes::new(n)) + .collect() + }) + .await + .unwrap() + } + + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().map(|n| GqlNodes::new(n)).collect()) + .await + .unwrap() + } +} + +#[derive(ResolvedObject, Clone)] +#[graphql(name = "PathFromNodeWindowSet")] +pub(crate) struct GqlPathFromNodeWindowSet { + pub(crate) ws: WindowSet<'static, PathFromNode<'static, DynamicGraph, DynamicGraph>>, +} + +impl GqlPathFromNodeWindowSet { + pub(crate) fn new(ws: WindowSet<'static, PathFromNode>) -> Self { + Self { ws } + } +} +#[ResolvedObjectFields] +impl GqlPathFromNodeWindowSet { + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().count()) + .await + .unwrap() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .ws + .clone() + .skip(start) + .take(limit) + .map(|n| GqlPathFromNode::new(n)) + .collect() + }) + .await + .unwrap() + } + + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + self_clone + .ws + .clone() + .map(|n| GqlPathFromNode::new(n)) + .collect() + }) + .await + .unwrap() + } +} + +#[derive(ResolvedObject, Clone)] +#[graphql(name = "EdgeWindowSet")] +pub(crate) struct GqlEdgeWindowSet { + pub(crate) ws: WindowSet<'static, EdgeView>, +} + +impl GqlEdgeWindowSet { + pub(crate) fn new(ws: WindowSet<'static, EdgeView>) -> Self { + Self { ws } + } +} +#[ResolvedObjectFields] +impl GqlEdgeWindowSet { + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().count()) + .await + .unwrap() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .ws + .clone() + .skip(start) + .take(limit) + .map(|e| e.into()) + .collect() + }) + .await + .unwrap() + } + + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().map(|e| e.into()).collect()) + .await + .unwrap() + } +} + +#[derive(ResolvedObject, Clone)] +#[graphql(name = "EdgesWindowSet")] +pub(crate) struct GqlEdgesWindowSet { + pub(crate) ws: WindowSet<'static, Edges<'static, DynamicGraph, DynamicGraph>>, +} + +impl GqlEdgesWindowSet { + pub(crate) fn new(ws: WindowSet<'static, Edges>) -> Self { + Self { ws } + } +} +#[ResolvedObjectFields] +impl GqlEdgesWindowSet { + async fn count(&self) -> usize { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().count()) + .await + .unwrap() + } + + async fn page(&self, limit: usize, offset: usize) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || { + let start = offset * limit; + self_clone + .ws + .clone() + .skip(start) + .take(limit) + .map(|e| GqlEdges::new(e)) + .collect() + }) + .await + .unwrap() + } + + async fn list(&self) -> Vec { + let self_clone = self.clone(); + spawn_blocking(move || self_clone.ws.clone().map(|e| GqlEdges::new(e)).collect()) + .await + .unwrap() + } +} diff --git a/raphtory-graphql/src/model/mod.rs b/raphtory-graphql/src/model/mod.rs index fd79de8f79..2ab9094d66 100644 --- a/raphtory-graphql/src/model/mod.rs +++ b/raphtory-graphql/src/model/mod.rs @@ -3,8 +3,8 @@ use crate::{ data::Data, model::{ graph::{ - graph::GqlGraph, graphs::GqlGraphs, mutable_graph::GqlMutableGraph, - namespace::Namespace, vectorised_graph::GqlVectorisedGraph, + graph::GqlGraph, mutable_graph::GqlMutableGraph, namespace::Namespace, + vectorised_graph::GqlVectorisedGraph, }, plugins::{mutation_plugin::MutationPlugin, query_plugin::QueryPlugin}, }, @@ -17,6 +17,7 @@ use dynamic_graphql::{ Result, Upload, }; +use crate::model::graph::namespaces::Namespaces; #[cfg(feature = "storage")] use raphtory::db::api::{storage::graph::storage_ops::GraphStorage, view::internal::CoreGraphOps}; use raphtory::{ @@ -65,6 +66,7 @@ pub enum GqlGraphError { } #[derive(Enum)] +#[graphql(name = "GraphType")] pub enum GqlGraphType { Persistent, Event, @@ -104,10 +106,10 @@ impl QueryRoot { Some(g.into()) } - async fn namespaces<'a>(ctx: &Context<'a>) -> Vec { + async fn namespaces<'a>(ctx: &Context<'a>) -> Namespaces { let data = ctx.data_unchecked::(); let root = Namespace::new(data.work_dir.clone(), data.work_dir.clone()); - root.get_all_children() + Namespaces::new(root.get_all_namespaces()) } async fn namespace<'a>( ctx: &Context<'a>, @@ -127,13 +129,6 @@ impl QueryRoot { Namespace::new(data.work_dir.clone(), data.work_dir.clone()) } - //To deprecate I think - async fn graphs<'a>(ctx: &Context<'a>) -> Result { - let data = ctx.data_unchecked::(); - let paths = data.get_all_graph_folders(); - Ok(GqlGraphs::new(paths)) - } - async fn plugins<'a>(ctx: &Context<'a>) -> QueryPlugin { let data = ctx.data_unchecked::(); data.get_global_plugins() diff --git a/raphtory-graphql/src/paths.rs b/raphtory-graphql/src/paths.rs index c4ac8b878a..9ba0597833 100644 --- a/raphtory-graphql/src/paths.rs +++ b/raphtory-graphql/src/paths.rs @@ -139,28 +139,43 @@ impl ValidGraphFolder { }) } - pub(crate) fn created(&self) -> Result { + pub fn created(&self) -> Result { fs::metadata(self.get_graph_path())?.created()?.to_millis() } - pub(crate) fn last_opened(&self) -> Result { + pub fn last_opened(&self) -> Result { fs::metadata(self.get_graph_path())?.accessed()?.to_millis() } - pub(crate) fn last_updated(&self) -> Result { + pub fn last_updated(&self) -> Result { fs::metadata(self.get_graph_path())?.modified()?.to_millis() } - pub(crate) fn get_original_path_str(&self) -> &str { + pub async fn created_async(&self) -> Result { + let metadata = tokio::fs::metadata(self.get_graph_path()).await?; + metadata.created()?.to_millis() + } + + pub async fn last_opened_async(&self) -> Result { + let metadata = tokio::fs::metadata(self.get_graph_path()).await?; + metadata.accessed()?.to_millis() + } + + pub async fn last_updated_async(&self) -> Result { + let metadata = tokio::fs::metadata(self.get_graph_path()).await?; + metadata.modified()?.to_millis() + } + + pub fn get_original_path_str(&self) -> &str { &self.original_path } - pub(crate) fn get_original_path(&self) -> &Path { + pub fn get_original_path(&self) -> &Path { &Path::new(&self.original_path) } /// This returns the PathBuf used to build multiple GraphError types - pub(crate) fn to_error_path(&self) -> PathBuf { + pub fn to_error_path(&self) -> PathBuf { self.original_path.to_owned().into() } } diff --git a/raphtory/src/core/utils/errors.rs b/raphtory/src/core/utils/errors.rs index 6eea199e59..afc52bb84b 100644 --- a/raphtory/src/core/utils/errors.rs +++ b/raphtory/src/core/utils/errors.rs @@ -172,7 +172,7 @@ pub enum GraphError { #[error("Tried to mutate constant property {name}: old value {old:?}, new value {new:?}")] ConstantPropertyMutationError { name: ArcStr, old: Prop, new: Prop }, - #[error("Failed to parse time string")] + #[error("Failed to parse time string: {source}")] ParseTime { #[from] source: ParseTimeError, @@ -385,6 +385,9 @@ pub enum GraphError { #[error("Failed to create index in ram")] FailedToCreateIndexInRam, + + #[error("Your window and step must be of the same type: duration (string) or epoch (int)")] + MismatchedIntervalTypes, } impl GraphError { diff --git a/raphtory/src/core/utils/time.rs b/raphtory/src/core/utils/time.rs index 3cf9e92068..214963bc26 100644 --- a/raphtory/src/core/utils/time.rs +++ b/raphtory/src/core/utils/time.rs @@ -28,6 +28,8 @@ pub mod error { ParseError(#[from] ParseError), #[error("negative interval is not supported")] NegativeInt, + #[error("0 size step is not supported")] + ZeroSizeStep, #[error("'{0}' is not a valid datetime, valid formats are RFC3339, RFC2822, %Y-%m-%d, %Y-%m-%dT%H:%M:%S%.3f, %Y-%m-%dT%H:%M:%S%, %Y-%m-%d %H:%M:%S%.3f and %Y-%m-%d %H:%M:%S%")] InvalidDateTimeString(String), } diff --git a/raphtory/src/db/api/properties/constant_props.rs b/raphtory/src/db/api/properties/constant_props.rs index aa29d0a2df..a2dc68aa15 100644 --- a/raphtory/src/db/api/properties/constant_props.rs +++ b/raphtory/src/db/api/properties/constant_props.rs @@ -8,6 +8,7 @@ use std::{ fmt::{Debug, Formatter}, }; +#[derive(Clone)] pub struct ConstantProperties<'a, P: ConstPropertiesOps> { pub(crate) props: P, _marker: std::marker::PhantomData<&'a P>, diff --git a/raphtory/src/db/api/properties/temporal_props.rs b/raphtory/src/db/api/properties/temporal_props.rs index d2cac88349..f542f081d3 100644 --- a/raphtory/src/db/api/properties/temporal_props.rs +++ b/raphtory/src/db/api/properties/temporal_props.rs @@ -119,7 +119,7 @@ impl IntoIterator for &TemporalPropertyView

{ hist.into_iter().zip(vals) } } - +#[derive(Clone)] pub struct TemporalProperties { pub(crate) props: P, } diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs index 0c98dcc37d..339ee23e69 100644 --- a/raphtory/src/db/api/view/graph.rs +++ b/raphtory/src/db/api/view/graph.rs @@ -675,7 +675,7 @@ impl SearchableGraphOps for G { } else { let index_path = path.join("index"); if index_path.exists() && index_path.read_dir()?.next().is_some() { - storage.get_or_create_index(Some(index_path.clone()))?; + storage.get_or_create_index(Some(path.clone()))?; } } diff --git a/raphtory/src/db/api/view/time.rs b/raphtory/src/db/api/view/time.rs index 5f85867391..0b5078c6b1 100644 --- a/raphtory/src/db/api/view/time.rs +++ b/raphtory/src/db/api/view/time.rs @@ -1,7 +1,7 @@ use crate::{ core::{ storage::timeindex::AsTime, - utils::time::{error::ParseTimeError, Interval, IntoTime}, + utils::time::{error::ParseTimeError, Interval, IntervalSize, IntoTime}, }, db::api::view::{ internal::{InternalMaterialize, OneHopFilter, TimeSemantics}, @@ -250,10 +250,9 @@ impl<'graph, V: OneHopFilter<'graph> + 'graph + InternalTimeOps<'graph>> TimeOps match (self.timeline_start(), self.timeline_end()) { (Some(start), Some(end)) => { let step: Interval = step.try_into()?; - - Ok(WindowSet::new(parent, start, end, step, None)) + WindowSet::new(parent, start, end, step, None) } - _ => Ok(WindowSet::empty(parent)), + _ => WindowSet::empty(parent), } } @@ -275,9 +274,9 @@ impl<'graph, V: OneHopFilter<'graph> + 'graph + InternalTimeOps<'graph>> TimeOps Some(step) => step.try_into()?, None => window, }; - Ok(WindowSet::new(parent, start, end, step, Some(window))) + WindowSet::new(parent, start, end, step, Some(window)) } - _ => Ok(WindowSet::empty(parent)), + _ => WindowSet::empty(parent), } } } @@ -293,19 +292,37 @@ pub struct WindowSet<'graph, T> { } impl<'graph, T: TimeOps<'graph> + Clone + 'graph> WindowSet<'graph, T> { - fn new(view: T, start: i64, end: i64, step: Interval, window: Option) -> Self { + fn new( + view: T, + start: i64, + end: i64, + step: Interval, + window: Option, + ) -> Result { + match step.size { + IntervalSize::Discrete(v) => { + if v == 0 { + return Err(ParseTimeError::ZeroSizeStep); + } + } + IntervalSize::Temporal { millis, months } => { + if millis == 0 && months == 0 { + return Err(ParseTimeError::ZeroSizeStep); + } + } + }; let cursor_start = start + step; - Self { + Ok(Self { view, cursor: cursor_start, end, step, window, _marker: PhantomData, - } + }) } - fn empty(view: T) -> Self { + fn empty(view: T) -> Result { // timeline_start is greater than end, so no windows to return, even with end inclusive WindowSet::new(view, 1, 0, Default::default(), None) } @@ -353,7 +370,16 @@ impl<'graph, T: TimeOps<'graph> + Clone + 'graph> Iterator for WindowSet<'graph, fn next(&mut self) -> Option { if self.cursor < self.end + self.step { let window_end = self.cursor; + let window_start = self.window.map(|w| window_end - w); + if let Some(start) = window_start { + //this is required because if we have steps > window size you can end up overstepping + // the end by so much in the final window that there is no data inside + if start >= self.end { + // this is >= because the end passed through is already +1 + return None; + } + } let window = self.view.internal_window(window_start, Some(window_end)); self.cursor = self.cursor + self.step; Some(window) @@ -361,12 +387,34 @@ impl<'graph, T: TimeOps<'graph> + Clone + 'graph> Iterator for WindowSet<'graph, None } } + fn size_hint(&self) -> (usize, Option) { + let len = self.len(); + (len, Some(len)) + } +} +impl<'graph, T: TimeOps<'graph> + Clone + 'graph> ExactSizeIterator for WindowSet<'graph, T> { + //unfortunately because Interval can change size, there is no nice divide option + fn len(&self) -> usize { + let mut cursor = self.cursor; + let mut count = 0; + while cursor < self.end + self.step { + let window_start = self.window.map(|w| cursor - w); + if let Some(start) = window_start { + if start >= self.end { + break; + } + } + count += 1; + cursor = cursor + self.step; + } + count + } } #[cfg(test)] mod time_tests { use crate::{ - core::utils::time::TryIntoTime, + core::utils::time::{error::ParseTimeError, TryIntoTime}, db::{ api::{ mutation::AdditionOps, @@ -378,6 +426,7 @@ mod time_tests { test_storage, }; use itertools::Itertools; + use std::num::ParseIntError; // start inclusive, end exclusive fn graph_with_timeline(start: i64, end: i64) -> Graph { @@ -527,6 +576,68 @@ mod time_tests { // assert_bounds(windows, expected); } + #[test] + fn test_errors() { + let start = "2020-06-06 00:00:00".try_into_time().unwrap(); + let end = "2020-06-07 23:59:59.999".try_into_time().unwrap(); + let graph = graph_with_timeline(start, end); + match graph.rolling("1 day", Some("0 days")) { + Ok(_) => { + panic!("Expected error, but got Ok") + } + Err(e) => { + assert_eq!(e, ParseTimeError::ZeroSizeStep) + } + } + match graph.rolling(1, Some(0)) { + Ok(_) => { + panic!("Expected error, but got Ok") + } + Err(e) => { + assert_eq!(e, ParseTimeError::ZeroSizeStep) + } + } + match graph.expanding("0 day") { + Ok(_) => { + panic!("Expected error, but got Ok") + } + Err(e) => { + assert_eq!(e, ParseTimeError::ZeroSizeStep) + } + } + match graph.expanding(0) { + Ok(_) => { + panic!("Expected error, but got Ok") + } + Err(e) => { + assert_eq!(e, ParseTimeError::ZeroSizeStep) + } + } + + match graph.expanding("0fead day") { + Ok(_) => { + panic!("Expected error, but got Ok") + } + Err(e) => { + assert!(matches!(e, ParseTimeError::ParseInt { .. })) + } + } + + match graph.expanding("0 dadasasdy") { + Ok(_) => { + panic!("Expected error, but got Ok") + } + Err(e) => { + assert!(matches!(e, ParseTimeError::InvalidUnit { .. })) + } + } + + assert_eq!( + graph.rolling("1 day", Some("1000 days")).unwrap().count(), + 0 + ) + } + #[test] fn expanding_dates() { let start = "2020-06-06 00:00:00".try_into_time().unwrap(); diff --git a/raphtory/src/search/graph_index.rs b/raphtory/src/search/graph_index.rs index c519222cc9..cabdee1a7c 100644 --- a/raphtory/src/search/graph_index.rs +++ b/raphtory/src/search/graph_index.rs @@ -130,6 +130,8 @@ impl GraphIndex { pub fn load_from_path(path: &PathBuf) -> Result { let tmp_path = TempDir::new_in(path)?; + let path = path.join("index"); + let path = path.as_path(); if path.is_file() { GraphIndex::unzip_index(path, tmp_path.path())?; } else {