From 3573dc576bdf179ae4cddd96519b67dc9b9e0950 Mon Sep 17 00:00:00 2001 From: ljeub-pometry <97447091+ljeub-pometry@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:27:23 +0100 Subject: [PATCH] Feature/gql sort nodes (#1912) * add sorting support for nodes in GraphQl * add node sorting tests * fix the python tests (submodules should start with test_ * implement sorting for list properties * non-trivial test for list sorting * fix warnings * fmt --- .../edit_graph/test_archive_graph.py | 0 .../edit_graph/test_copy_graph.py | 0 .../edit_graph/test_delete_graph.py | 0 .../edit_graph/test_get_graph.py | 0 .../edit_graph/test_graphql.py | 0 .../edit_graph/test_move_graph.py | 0 .../edit_graph/test_new_graph.py | 0 .../edit_graph/test_receive_graph.py | 0 .../edit_graph/test_send_graph.py | 0 .../edit_graph/test_upload_graph.py | 0 .../misc/test_components.py | 0 .../misc/test_graphql_vectors.py | 0 .../misc/test_index_off.py | 0 .../misc/test_latest.py | 0 .../misc/test_snapshot.py | 0 .../misc/test_tracing.py | 0 .../test_edge_sorting.py | 99 ++-- .../test_graph_file_time_stats.py | 0 .../test_graph_nodes_edges_property_filter.py | 0 .../tests/test_graphql/test_node_sorting.py | 422 ++++++++++++++++++ .../test_nodes_property_filter.py | 0 .../{graphql => test_graphql}/test_schema.py | 0 .../update_graph/test_batch_updates.py | 0 .../update_graph/test_edge_updates.py | 0 .../update_graph/test_graph_updates.py | 0 .../update_graph/test_node_updates.py | 0 python/tests/utils.py | 39 ++ raphtory-graphql/src/model/graph/edges.rs | 26 +- raphtory-graphql/src/model/graph/nodes.rs | 64 ++- raphtory-graphql/src/model/mod.rs | 1 + raphtory-graphql/src/model/sorting.rs | 24 + raphtory/src/core/mod.rs | 1 + raphtory/src/db/api/state/mod.rs | 3 +- raphtory/src/db/graph/nodes.rs | 9 + 34 files changed, 599 insertions(+), 89 deletions(-) rename python/tests/{graphql => test_graphql}/edit_graph/test_archive_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_copy_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_delete_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_get_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_graphql.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_move_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_new_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_receive_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_send_graph.py (100%) rename python/tests/{graphql => test_graphql}/edit_graph/test_upload_graph.py (100%) rename python/tests/{graphql => test_graphql}/misc/test_components.py (100%) rename python/tests/{graphql => test_graphql}/misc/test_graphql_vectors.py (100%) rename python/tests/{graphql => test_graphql}/misc/test_index_off.py (100%) rename python/tests/{graphql => test_graphql}/misc/test_latest.py (100%) rename python/tests/{graphql => test_graphql}/misc/test_snapshot.py (100%) rename python/tests/{graphql => test_graphql}/misc/test_tracing.py (100%) rename python/tests/{graphql => test_graphql}/test_edge_sorting.py (83%) rename python/tests/{graphql => test_graphql}/test_graph_file_time_stats.py (100%) rename python/tests/{graphql => test_graphql}/test_graph_nodes_edges_property_filter.py (100%) create mode 100644 python/tests/test_graphql/test_node_sorting.py rename python/tests/{graphql => test_graphql}/test_nodes_property_filter.py (100%) rename python/tests/{graphql => test_graphql}/test_schema.py (100%) rename python/tests/{graphql => test_graphql}/update_graph/test_batch_updates.py (100%) rename python/tests/{graphql => test_graphql}/update_graph/test_edge_updates.py (100%) rename python/tests/{graphql => test_graphql}/update_graph/test_graph_updates.py (100%) rename python/tests/{graphql => test_graphql}/update_graph/test_node_updates.py (100%) create mode 100644 raphtory-graphql/src/model/sorting.rs diff --git a/python/tests/graphql/edit_graph/test_archive_graph.py b/python/tests/test_graphql/edit_graph/test_archive_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_archive_graph.py rename to python/tests/test_graphql/edit_graph/test_archive_graph.py diff --git a/python/tests/graphql/edit_graph/test_copy_graph.py b/python/tests/test_graphql/edit_graph/test_copy_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_copy_graph.py rename to python/tests/test_graphql/edit_graph/test_copy_graph.py diff --git a/python/tests/graphql/edit_graph/test_delete_graph.py b/python/tests/test_graphql/edit_graph/test_delete_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_delete_graph.py rename to python/tests/test_graphql/edit_graph/test_delete_graph.py diff --git a/python/tests/graphql/edit_graph/test_get_graph.py b/python/tests/test_graphql/edit_graph/test_get_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_get_graph.py rename to python/tests/test_graphql/edit_graph/test_get_graph.py diff --git a/python/tests/graphql/edit_graph/test_graphql.py b/python/tests/test_graphql/edit_graph/test_graphql.py similarity index 100% rename from python/tests/graphql/edit_graph/test_graphql.py rename to python/tests/test_graphql/edit_graph/test_graphql.py diff --git a/python/tests/graphql/edit_graph/test_move_graph.py b/python/tests/test_graphql/edit_graph/test_move_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_move_graph.py rename to python/tests/test_graphql/edit_graph/test_move_graph.py diff --git a/python/tests/graphql/edit_graph/test_new_graph.py b/python/tests/test_graphql/edit_graph/test_new_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_new_graph.py rename to python/tests/test_graphql/edit_graph/test_new_graph.py diff --git a/python/tests/graphql/edit_graph/test_receive_graph.py b/python/tests/test_graphql/edit_graph/test_receive_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_receive_graph.py rename to python/tests/test_graphql/edit_graph/test_receive_graph.py diff --git a/python/tests/graphql/edit_graph/test_send_graph.py b/python/tests/test_graphql/edit_graph/test_send_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_send_graph.py rename to python/tests/test_graphql/edit_graph/test_send_graph.py diff --git a/python/tests/graphql/edit_graph/test_upload_graph.py b/python/tests/test_graphql/edit_graph/test_upload_graph.py similarity index 100% rename from python/tests/graphql/edit_graph/test_upload_graph.py rename to python/tests/test_graphql/edit_graph/test_upload_graph.py diff --git a/python/tests/graphql/misc/test_components.py b/python/tests/test_graphql/misc/test_components.py similarity index 100% rename from python/tests/graphql/misc/test_components.py rename to python/tests/test_graphql/misc/test_components.py diff --git a/python/tests/graphql/misc/test_graphql_vectors.py b/python/tests/test_graphql/misc/test_graphql_vectors.py similarity index 100% rename from python/tests/graphql/misc/test_graphql_vectors.py rename to python/tests/test_graphql/misc/test_graphql_vectors.py diff --git a/python/tests/graphql/misc/test_index_off.py b/python/tests/test_graphql/misc/test_index_off.py similarity index 100% rename from python/tests/graphql/misc/test_index_off.py rename to python/tests/test_graphql/misc/test_index_off.py diff --git a/python/tests/graphql/misc/test_latest.py b/python/tests/test_graphql/misc/test_latest.py similarity index 100% rename from python/tests/graphql/misc/test_latest.py rename to python/tests/test_graphql/misc/test_latest.py diff --git a/python/tests/graphql/misc/test_snapshot.py b/python/tests/test_graphql/misc/test_snapshot.py similarity index 100% rename from python/tests/graphql/misc/test_snapshot.py rename to python/tests/test_graphql/misc/test_snapshot.py diff --git a/python/tests/graphql/misc/test_tracing.py b/python/tests/test_graphql/misc/test_tracing.py similarity index 100% rename from python/tests/graphql/misc/test_tracing.py rename to python/tests/test_graphql/misc/test_tracing.py diff --git a/python/tests/graphql/test_edge_sorting.py b/python/tests/test_graphql/test_edge_sorting.py similarity index 83% rename from python/tests/graphql/test_edge_sorting.py rename to python/tests/test_graphql/test_edge_sorting.py index 03ef7c87a4..396f09608a 100644 --- a/python/tests/graphql/test_edge_sorting.py +++ b/python/tests/test_graphql/test_edge_sorting.py @@ -1,13 +1,8 @@ -import tempfile - import pytest -from raphtory.graphql import GraphServer from raphtory import Graph, PersistentGraph -import json -import re -PORT = 1737 +from utils import run_graphql_test def create_test_graph(g): @@ -72,40 +67,12 @@ def create_test_graph(g): return g -def run_graphql_test(query, expected_output, graph): - create_test_graph(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) - - response = client.query(query) - - # Convert response to a dictionary if needed and compare - response_dict = json.loads(response) if isinstance(response, str) else response - assert response_dict == expected_output - - -def run_graphql_error_test(query, expected_error_message, graph): - create_test_graph(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) - - 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 "" +EVENT_GRAPH = create_test_graph(Graph()) - assert ( - error_message == expected_error_message - ), f"Expected '{expected_error_message}', but got '{error_message}'" +PERSISTENT_GRAPH = create_test_graph(PersistentGraph()) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_nothing(graph): query = """ query { @@ -140,10 +107,10 @@ def test_graph_edge_sort_by_nothing(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_src(graph): query = """ query { @@ -178,10 +145,10 @@ def test_graph_edge_sort_by_src(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_dst(graph): query = """ query { @@ -216,10 +183,10 @@ def test_graph_edge_sort_by_dst(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_earliest_time(graph): query = """ query { @@ -254,10 +221,10 @@ def test_graph_edge_sort_by_earliest_time(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_earliest_time_reversed(graph): query = """ query { @@ -293,10 +260,10 @@ def test_graph_edge_sort_by_earliest_time_reversed(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH]) def test_graph_edge_sort_by_latest_time(graph): query = """ query { @@ -331,10 +298,10 @@ def test_graph_edge_sort_by_latest_time(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [PersistentGraph]) +@pytest.mark.parametrize("graph", [PERSISTENT_GRAPH]) def test_graph_edge_sort_by_latest_time_persistent_graph(graph): query = """ query { @@ -370,10 +337,10 @@ def test_graph_edge_sort_by_latest_time_persistent_graph(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_eprop1(graph): query = """ query { @@ -408,10 +375,10 @@ def test_graph_edge_sort_by_eprop1(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_eprop2(graph): query = """ query { @@ -446,10 +413,10 @@ def test_graph_edge_sort_by_eprop2(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_eprop3(graph): query = """ query { @@ -484,10 +451,10 @@ def test_graph_edge_sort_by_eprop3(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_eprop4(graph): query = """ query { @@ -522,10 +489,10 @@ def test_graph_edge_sort_by_eprop4(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_eprop5(graph): query = """ query { @@ -560,10 +527,10 @@ def test_graph_edge_sort_by_eprop5(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_nonexistent_prop(graph): query = """ query { @@ -598,10 +565,10 @@ def test_graph_edge_sort_by_nonexistent_prop(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_combined(graph): query = """ query { @@ -636,10 +603,10 @@ def test_graph_edge_sort_by_combined(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [Graph, PersistentGraph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_sort_by_combined_2(graph): query = """ query { @@ -674,4 +641,4 @@ def test_graph_edge_sort_by_combined_2(graph): } } } - run_graphql_test(query, expected_output, graph()) + run_graphql_test(query, expected_output, graph) diff --git a/python/tests/graphql/test_graph_file_time_stats.py b/python/tests/test_graphql/test_graph_file_time_stats.py similarity index 100% rename from python/tests/graphql/test_graph_file_time_stats.py rename to python/tests/test_graphql/test_graph_file_time_stats.py diff --git a/python/tests/graphql/test_graph_nodes_edges_property_filter.py b/python/tests/test_graphql/test_graph_nodes_edges_property_filter.py similarity index 100% rename from python/tests/graphql/test_graph_nodes_edges_property_filter.py rename to python/tests/test_graphql/test_graph_nodes_edges_property_filter.py diff --git a/python/tests/test_graphql/test_node_sorting.py b/python/tests/test_graphql/test_node_sorting.py new file mode 100644 index 0000000000..3fbecb01ae --- /dev/null +++ b/python/tests/test_graphql/test_node_sorting.py @@ -0,0 +1,422 @@ +import pytest +from raphtory import Graph, PersistentGraph + +from utils import run_graphql_test + + +def create_test_graph(g): + g.add_node( + 3, + "a", + properties={ + "prop1": 60, + "prop2": 0.4, + "prop3": "xyz123", + "prop4": True, + "prop5": [1, 2, 3], + }, + ) + g.add_node( + 2, + "b", + properties={ + "prop1": 10, + "prop2": 1.7, + "prop3": "xyz123", + "prop4": True, + "prop5": [3, 4, 5], + }, + ) + g.add_node( + 1, + "d", + properties={ + "prop1": 30, + "prop2": 6.4, + "prop3": "xyz123", + "prop4": False, + "prop5": [50, 30], + }, + ) + g.add_node( + 1, + "b", + properties={ + "prop1": 80, + "prop2": 3.3, + "prop3": "xyz1234", + "prop4": False, + }, + ) + g.add_node( + 4, + "c", + properties={ + "prop1": 100, + "prop2": -2.3, + "prop3": "ayz123", + "prop5": [10, 20, 30], + }, + ) + return g + + +EVENT_GRAPH = create_test_graph(Graph()) + +PERSISTENT_GRAPH = create_test_graph(PersistentGraph()) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_node_sort_by_nothing(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: []) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "a"}, {"id": "b"}, {"id": "d"}, {"id": "c"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_node_sort_by_id(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{id:true}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "a"}, {"id": "b"}, {"id": "c"}, {"id": "d"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_earliest_time(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{time: EARLIEST}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "b"}, {"id": "d"}, {"id": "a"}, {"id": "c"}]} + } + } + } + + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_earliest_time_reversed(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{time: EARLIEST, reverse: true}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "c"}, {"id": "a"}, {"id": "b"}, {"id": "d"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH]) +def test_graph_nodes_sort_by_latest_time(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{time: LATEST}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "d"}, {"id": "b"}, {"id": "a"}, {"id": "c"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_latest_time(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{time: LATEST}]) { + list { + id + } + } + } + } + } + """ + # In the persistent graph all nodes will have the same latest_time + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "a"}, {"id": "b"}, {"id": "d"}, {"id": "c"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_prop1(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{property: "prop1"}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "b"}, {"id": "d"}, {"id": "a"}, {"id": "c"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_prop2(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{property: "prop2"}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "c"}, {"id": "a"}, {"id": "b"}, {"id": "d"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_prop3(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{property: "prop3"}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "c"}, {"id": "a"}, {"id": "b"}, {"id": "d"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_prop4(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{property: "prop4"}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "c"}, {"id": "d"}, {"id": "a"}, {"id": "b"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_prop5(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{property: "prop5"}]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "a"}, {"id": "b"}, {"id": "c"}, {"id": "d"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_nonexistent_prop(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted(sortBys: [{ property: "i_dont_exist" }, { property: "prop2", reverse: true }]) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "d"}, {"id": "b"}, {"id": "a"}, {"id": "c"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_nodes_sort_by_combined(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted( + sortBys: [{property: "prop3", reverse: true}, {property: "prop4"}, {time: EARLIEST}] + ) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "d"}, {"id": "b"}, {"id": "a"}, {"id": "c"}]} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_graph_node_sort_by_combined_2(graph): + query = """ + { + graph(path: "g") { + nodes { + sorted( + sortBys: [{time: EARLIEST}, {property: "prop3"}, {id: true, reverse: true}] + ) { + list { + id + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "sorted": {"list": [{"id": "d"}, {"id": "b"}, {"id": "a"}, {"id": "c"}]} + } + } + } + run_graphql_test(query, expected_output, graph) diff --git a/python/tests/graphql/test_nodes_property_filter.py b/python/tests/test_graphql/test_nodes_property_filter.py similarity index 100% rename from python/tests/graphql/test_nodes_property_filter.py rename to python/tests/test_graphql/test_nodes_property_filter.py diff --git a/python/tests/graphql/test_schema.py b/python/tests/test_graphql/test_schema.py similarity index 100% rename from python/tests/graphql/test_schema.py rename to python/tests/test_graphql/test_schema.py diff --git a/python/tests/graphql/update_graph/test_batch_updates.py b/python/tests/test_graphql/update_graph/test_batch_updates.py similarity index 100% rename from python/tests/graphql/update_graph/test_batch_updates.py rename to python/tests/test_graphql/update_graph/test_batch_updates.py diff --git a/python/tests/graphql/update_graph/test_edge_updates.py b/python/tests/test_graphql/update_graph/test_edge_updates.py similarity index 100% rename from python/tests/graphql/update_graph/test_edge_updates.py rename to python/tests/test_graphql/update_graph/test_edge_updates.py diff --git a/python/tests/graphql/update_graph/test_graph_updates.py b/python/tests/test_graphql/update_graph/test_graph_updates.py similarity index 100% rename from python/tests/graphql/update_graph/test_graph_updates.py rename to python/tests/test_graphql/update_graph/test_graph_updates.py diff --git a/python/tests/graphql/update_graph/test_node_updates.py b/python/tests/test_graphql/update_graph/test_node_updates.py similarity index 100% rename from python/tests/graphql/update_graph/test_node_updates.py rename to python/tests/test_graphql/update_graph/test_node_updates.py diff --git a/python/tests/utils.py b/python/tests/utils.py index a65e637bcd..50b70f9d7b 100644 --- a/python/tests/utils.py +++ b/python/tests/utils.py @@ -1,9 +1,17 @@ +import json +import re +import tempfile import time from typing import TypeVar, Callable +import pytest + +from raphtory.graphql import GraphServer B = TypeVar("B") +PORT = 1737 + def measure(name: str, f: Callable[..., B], *args, print_result: bool = True) -> B: start_time = time.time() @@ -24,3 +32,34 @@ def measure(name: str, f: Callable[..., B], *args, print_result: bool = True) -> print(f"Running {name}: time: {elapsed_time_display:.3f}{time_unit}") return result + + +def run_graphql_test(query, expected_output, 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) + + response = client.query(query) + + # Convert response to a dictionary if needed and compare + response_dict = json.loads(response) if isinstance(response, str) else response + assert response_dict == expected_output + + +def run_graphql_error_test(query, expected_error_message, 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) + + 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/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs index 1b19cf1900..ec0d9b4591 100644 --- a/raphtory-graphql/src/model/graph/edges.rs +++ b/raphtory-graphql/src/model/graph/edges.rs @@ -1,5 +1,8 @@ -use crate::model::graph::edge::Edge; -use dynamic_graphql::{Enum, InputObject, ResolvedObject, ResolvedObjectFields}; +use crate::model::{ + graph::edge::Edge, + sorting::{EdgeSortBy, SortByTime}, +}; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use itertools::Itertools; use raphtory::{ db::{ @@ -33,21 +36,6 @@ impl GqlEdges { } } -#[derive(InputObject, Clone, Debug, Eq, PartialEq)] -pub struct EdgeSortBy { - pub reverse: Option, - pub src: Option, - pub dst: Option, - pub time: Option, - pub property: Option, -} - -#[derive(Enum, Clone, Debug, Eq, PartialEq)] -pub enum EdgeSortByTime { - Latest, - Earliest, -} - #[ResolvedObjectFields] impl GqlEdges { //////////////////////// @@ -132,10 +120,10 @@ impl GqlEdges { 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 { - EdgeSortByTime::Latest => { + SortByTime::Latest => { (first_edge.latest_time(), second_edge.latest_time()) } - EdgeSortByTime::Earliest => { + SortByTime::Earliest => { (first_edge.earliest_time(), second_edge.earliest_time()) } }; diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index 4ee679f6d3..efaa301ff5 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -1,10 +1,19 @@ -use crate::model::graph::{node::Node, FilterCondition, Operator}; +use crate::model::{ + graph::{node::Node, FilterCondition, Operator}, + sorting::{NodeSortBy, SortByTime}, +}; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use itertools::Itertools; use raphtory::{ core::utils::errors::GraphError, - db::{api::view::DynamicGraph, graph::nodes::Nodes}, + db::{ + api::{state::Index, view::DynamicGraph}, + graph::nodes::Nodes, + }, prelude::*, }; +use raphtory_api::core::entities::VID; +use std::cmp::Ordering; #[derive(ResolvedObject)] pub(crate) struct GqlNodes { @@ -217,6 +226,57 @@ impl GqlNodes { } } + ///////////////// + //// Sorting //// + ///////////////// + + 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()) + } + }; + 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() + } else { + ordering + } + } else { + Ordering::Equal + } + }) + }) + }) + .map(|node_view| node_view.node) + .collect(); + GqlNodes::new(self.nn.indexed(sorted)) + } + //////////////////////// //// TIME QUERIES ////// //////////////////////// diff --git a/raphtory-graphql/src/model/mod.rs b/raphtory-graphql/src/model/mod.rs index 88b54afb22..d055bdd8b3 100644 --- a/raphtory-graphql/src/model/mod.rs +++ b/raphtory-graphql/src/model/mod.rs @@ -33,6 +33,7 @@ pub mod algorithms; pub(crate) mod graph; pub mod plugins; pub(crate) mod schema; +pub(crate) mod sorting; #[derive(Debug)] pub struct MissingGraph; diff --git a/raphtory-graphql/src/model/sorting.rs b/raphtory-graphql/src/model/sorting.rs new file mode 100644 index 0000000000..84808f7f6a --- /dev/null +++ b/raphtory-graphql/src/model/sorting.rs @@ -0,0 +1,24 @@ +use dynamic_graphql::{Enum, InputObject}; + +#[derive(InputObject, Clone, Debug, Eq, PartialEq)] +pub struct EdgeSortBy { + pub reverse: Option, + pub src: Option, + pub dst: Option, + pub time: Option, + pub property: Option, +} + +#[derive(InputObject, Clone, Debug, Eq, PartialEq)] +pub struct NodeSortBy { + pub reverse: Option, + pub id: Option, + pub time: Option, + pub property: Option, +} + +#[derive(Enum, Clone, Debug, Eq, PartialEq)] +pub enum SortByTime { + Latest, + Earliest, +} diff --git a/raphtory/src/core/mod.rs b/raphtory/src/core/mod.rs index 90463e5e5a..f16f79fa26 100644 --- a/raphtory/src/core/mod.rs +++ b/raphtory/src/core/mod.rs @@ -170,6 +170,7 @@ impl PartialOrd for Prop { (Prop::Bool(a), Prop::Bool(b)) => a.partial_cmp(b), (Prop::NDTime(a), Prop::NDTime(b)) => a.partial_cmp(b), (Prop::DTime(a), Prop::DTime(b)) => a.partial_cmp(b), + (Prop::List(a), Prop::List(b)) => a.partial_cmp(b), _ => None, } } diff --git a/raphtory/src/db/api/state/mod.rs b/raphtory/src/db/api/state/mod.rs index 4e9fe1e083..d76c2fafb3 100644 --- a/raphtory/src/db/api/state/mod.rs +++ b/raphtory/src/db/api/state/mod.rs @@ -7,8 +7,7 @@ pub(crate) mod ops; pub use group_by::{NodeGroups, NodeStateGroupBy}; pub use lazy_node_state::LazyNodeState; -pub(crate) use node_state::Index; -pub use node_state::NodeState; +pub use node_state::{Index, NodeState}; pub use node_state_ops::NodeStateOps; pub use node_state_ord_ops::{AsOrderedNodeStateOps, OrderedNodeStateOps}; pub use ops::node::NodeOp; diff --git a/raphtory/src/db/graph/nodes.rs b/raphtory/src/db/graph/nodes.rs index 6e1ad8d05f..60df8ae302 100644 --- a/raphtory/src/db/graph/nodes.rs +++ b/raphtory/src/db/graph/nodes.rs @@ -141,6 +141,15 @@ where } } + pub fn indexed(&self, index: Index) -> Nodes<'graph, G, GH> { + Nodes::new_filtered( + self.base_graph.clone(), + self.graph.clone(), + Some(index), + self.node_types_filter.clone(), + ) + } + #[inline] pub(crate) fn iter_refs(&self) -> impl Iterator + Send + Sync + 'graph { let g = self.graph.core_graph().lock();