From 06dbc2e93750602d1a2415b35f535111902a3e64 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 2 Oct 2025 20:03:00 +0100 Subject: [PATCH 1/8] redesign, refactor property filters --- python/python/raphtory/__init__.pyi | 56 - .../python/raphtory/algorithms/__init__.pyi | 3 - python/python/raphtory/filter/__init__.pyi | 32 +- .../test_edge_property_filter_semantics.py | 8 +- .../test_node_property_filter_semantics.py | 8 +- .../test_filters/test_edge_property_filter.py | 46 +- .../test_filters/test_exploded_edge_filter.py | 20 +- .../test_filters/test_node_property_filter.py | 58 +- .../test_graphdb/test_persistent_graph.py | 2 +- .../test_graph_nodes_property_filter.py | 2 +- .../test_nodes_property_filter.py | 43 +- python/tests/test_base_install/test_index.py | 84 +- .../entities/properties/prop/prop_type.rs | 7 + raphtory-graphql/schema.graphql | 67 +- raphtory-graphql/src/model/graph/filtering.rs | 147 +- raphtory/src/db/graph/views/filter/mod.rs | 278 +-- .../views/filter/model/property_filter.rs | 1676 +++++++++-------- raphtory/src/python/filter/mod.rs | 2 - .../python/filter/property_filter_builders.rs | 271 +-- raphtory/src/search/edge_filter_executor.rs | 4 +- .../search/exploded_edge_filter_executor.rs | 4 +- raphtory/src/search/mod.rs | 6 +- raphtory/src/search/node_filter_executor.rs | 4 +- 23 files changed, 1381 insertions(+), 1447 deletions(-) diff --git a/python/python/raphtory/__init__.pyi b/python/python/raphtory/__init__.pyi index 0a17666de6..e7d3659c82 100644 --- a/python/python/raphtory/__init__.pyi +++ b/python/python/raphtory/__init__.pyi @@ -49,7 +49,6 @@ __all__ = [ "IndexSpec", "Prop", "version", - "DiskGraphStorage", "graphql", "algorithms", "graph_loader", @@ -1376,17 +1375,6 @@ class Graph(GraphView): MutableNode: The node object with the specified id, or None if the node does not exist """ - def persist_as_disk_graph(self, graph_dir: str | PathLike) -> DiskGraphStorage: - """ - save graph in disk_graph format and memory map the result - - Arguments: - graph_dir (str | PathLike): folder where the graph will be saved - - Returns: - DiskGraphStorage: the persisted graph storage - """ - def persistent_graph(self) -> PersistentGraph: """ View graph with persistent semantics @@ -1424,17 +1412,6 @@ class Graph(GraphView): bytes: """ - def to_disk_graph(self, graph_dir: str | PathLike) -> Graph: - """ - Persist graph on disk - - Arguments: - graph_dir (str | PathLike): the folder where the graph will be persisted - - Returns: - Graph: a view of the persisted graph - """ - def to_parquet(self, graph_dir: str | PathLike): """ Persist graph to parquet files @@ -2209,7 +2186,6 @@ class PersistentGraph(GraphView): bytes: """ - def to_disk_graph(self, graph_dir): ... def update_metadata(self, metadata: dict) -> None: """ Updates metadata of the graph. @@ -6142,35 +6118,3 @@ class Prop(object): def u8(value): ... def version(): ... - -class DiskGraphStorage(object): - def __repr__(self): - """Return repr(self).""" - - def append_node_temporal_properties(self, location, chunk_size=20000000): ... - def graph_dir(self): ... - @staticmethod - def load_from_dir(graph_dir): ... - @staticmethod - def load_from_pandas(graph_dir, edge_df, time_col, src_col, dst_col): ... - @staticmethod - def load_from_parquets( - graph_dir, - layer_parquet_cols, - node_properties=None, - chunk_size=10000000, - t_props_chunk_size=10000000, - num_threads=4, - node_type_col=None, - node_id_col=None, - ): ... - def load_node_metadata(self, location, col_names=None, chunk_size=None): ... - def load_node_types(self, location, col_name, chunk_size=None): ... - def merge_by_sorted_gids(self, other, graph_dir): - """ - Merge this graph with another `DiskGraph`. Note that both graphs should have nodes that are - sorted by their global ids or the resulting graph will be nonsense! - """ - - def to_events(self): ... - def to_persistent(self): ... diff --git a/python/python/raphtory/algorithms/__init__.pyi b/python/python/raphtory/algorithms/__init__.pyi index 02ea5eba3e..3873e9b001 100644 --- a/python/python/raphtory/algorithms/__init__.pyi +++ b/python/python/raphtory/algorithms/__init__.pyi @@ -70,7 +70,6 @@ __all__ = [ "max_weight_matching", "Matching", "Infected", - "connected_components", ] def dijkstra_single_source_shortest_paths( @@ -894,5 +893,3 @@ class Infected(object): Returns: int: """ - -def connected_components(graph): ... diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 2c89c6da12..94427efba7 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -32,7 +32,6 @@ __all__ = [ "ExplodedEdge", "Property", "Metadata", - "TemporalPropertyFilterBuilder", ] class FilterExpr(object): @@ -75,11 +74,13 @@ class PropertyFilterOps(object): def avg(self): ... def contains(self, value): ... def ends_with(self, value): ... + def first(self): ... def fuzzy_search(self, prop_value, levenshtein_distance, prefix_match): ... def is_in(self, values): ... def is_none(self): ... def is_not_in(self, values): ... def is_some(self): ... + def last(self): ... def len(self): ... def max(self): ... def min(self): ... @@ -184,32 +185,3 @@ class Metadata(PropertyFilterOps): Arguments: name (str): the name of the property to filter """ - -class TemporalPropertyFilterBuilder(object): - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self 1, 4), - (filter.ExplodedEdge.property("weight").temporal().latest() <= 2, 4), - (filter.ExplodedEdge.property("weight").temporal().latest() >= 3, 2), - (filter.ExplodedEdge.property("weight").temporal().latest().is_in([1, 2]), 4), - (filter.ExplodedEdge.property("weight").temporal().latest().is_not_in([3]), 4), - (filter.ExplodedEdge.property("weight").temporal().latest().is_some(), 6), - (filter.ExplodedEdge.property("weight").temporal().latest().is_none(), 0), + (filter.ExplodedEdge.property("weight").temporal().last() == 2, 2), + (filter.ExplodedEdge.property("weight").temporal().last() != 3, 4), + (filter.ExplodedEdge.property("weight").temporal().last() < 3, 4), + (filter.ExplodedEdge.property("weight").temporal().last() > 1, 4), + (filter.ExplodedEdge.property("weight").temporal().last() <= 2, 4), + (filter.ExplodedEdge.property("weight").temporal().last() >= 3, 2), + (filter.ExplodedEdge.property("weight").temporal().last().is_in([1, 2]), 4), + (filter.ExplodedEdge.property("weight").temporal().last().is_not_in([3]), 4), + (filter.ExplodedEdge.property("weight").temporal().last().is_some(), 6), + (filter.ExplodedEdge.property("weight").temporal().last().is_none(), 0), (filter.ExplodedEdge.property("p20").temporal().first().starts_with("Old"), 1), (filter.ExplodedEdge.property("p20").temporal().first().ends_with("boat"), 1), ] 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 8b2d0fe5b4..8164e4a309 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 @@ -155,7 +155,7 @@ def check(graph): expected_ids = [] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().latest().starts_with("P") + filter_expr = filter.Node.property("p10").temporal().last().starts_with("P") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "3"] assert result_ids == expected_ids @@ -198,7 +198,7 @@ def check(graph): expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().latest().ends_with("ship") + filter_expr = filter.Node.property("p10").temporal().last().ends_with("ship") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["2"] assert result_ids == expected_ids @@ -236,7 +236,7 @@ def check(graph): expected_ids = ["1", "2", "3"] assert result_ids == expected_ids - filter_expr = filter.Node.property("p10").temporal().latest().contains("Paper") + filter_expr = filter.Node.property("p10").temporal().last().contains("Paper") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "2", "3"] assert result_ids == expected_ids @@ -269,9 +269,7 @@ def check(graph): expected_ids = ["1", "3"] assert result_ids == expected_ids - filter_expr = ( - filter.Node.property("p10").temporal().latest().not_contains("ship") - ) + filter_expr = filter.Node.property("p10").temporal().last().not_contains("ship") result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["1", "3"] assert result_ids == expected_ids @@ -361,9 +359,9 @@ def check(graph): @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) -def test_filter_nodes_for_temporal_latest_property_sum(): +def test_filter_nodes_for_temporal_last_property_sum(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().latest().sum() == 12 + filter_expr = filter.Node.property("prop6").temporal().last().sum() == 12 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -372,9 +370,9 @@ def check(graph): @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) -def test_filter_nodes_for_temporal_latest_property_avg(): +def test_filter_nodes_for_temporal_last_property_avg(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().latest().avg() == 4.0 + filter_expr = filter.Node.property("prop6").temporal().last().avg() == 4.0 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -383,9 +381,9 @@ def check(graph): @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) -def test_filter_nodes_for_temporal_latest_property_min(): +def test_filter_nodes_for_temporal_last_property_min(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().latest().min() == 3 + filter_expr = filter.Node.property("prop6").temporal().last().min() == 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -394,9 +392,9 @@ def check(graph): @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) -def test_filter_nodes_for_temporal_latest_property_max(): +def test_filter_nodes_for_temporal_last_property_max(): def check(graph): - filter_expr = filter.Node.property("prop6").temporal().latest().max() == 5 + filter_expr = filter.Node.property("prop6").temporal().last().max() == 5 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -405,11 +403,11 @@ def check(graph): @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) -def test_filter_nodes_for_temporal_latest_property_len(): +def test_filter_nodes_for_temporal_last_property_len(): def check(graph): - filter_expr = filter.Node.property( - "prop6" - ).temporal().latest().len() == Prop.u64(3) + filter_expr = filter.Node.property("prop6").temporal().last().len() == Prop.u64( + 3 + ) result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a"] assert result_ids == expected_ids @@ -728,9 +726,9 @@ def check(graph): @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) -def test_filter_nodes_for_temporary_property_latest_any(): +def test_filter_nodes_for_temporary_property_last_any(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().latest().any() == 3 + filter_expr = filter.Node.property("prop8").temporal().last().any() == 3 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "d"] assert result_ids == expected_ids @@ -739,14 +737,14 @@ def check(graph): @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) -def test_filter_nodes_for_temporary_property_latest_all(): +def test_filter_nodes_for_temporary_property_last_all(): def check(graph): - filter_expr = filter.Node.property("prop8").temporal().latest().all() > 1 + filter_expr = filter.Node.property("prop8").temporal().last().all() > 1 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["a", "d"] assert result_ids == expected_ids - filter_expr = filter.Node.property("prop8").temporal().latest().all() > 2 + filter_expr = filter.Node.property("prop8").temporal().last().all() > 2 result_ids = sorted(graph.filter(filter_expr).nodes.id) expected_ids = ["d"] assert result_ids == expected_ids @@ -1074,7 +1072,7 @@ def test_filter_nodes_for_temporal_property_ne(): def check(graph): filter_expr = filter.Node.property("prop1").temporal() != [60] result_ids = sorted(graph.filter(filter_expr).nodes.id) - expected_ids = ['b', 'c', 'd'] + expected_ids = ["b", "c", "d"] assert result_ids == expected_ids return check @@ -1084,18 +1082,18 @@ def check(graph): def test_filter_nodes_for_temporal_property_fails(): def check(graph): filter_expr = filter.Node.property("prop1").temporal() == 60 - msg = "Invalid filter: temporal() == / != expects a list value (e.g. [1,2,3])" + msg = "Wrong type for property prop1: expected List(I64) but actual type is I64" with pytest.raises( - Exception, - match=re.escape(msg), + Exception, + match=re.escape(msg), ): graph.filter(filter_expr).nodes.id filter_expr = filter.Node.property("prop1").temporal() == "pometry" - msg = "Invalid filter: temporal() == / != expects a list value (e.g. [1,2,3])" + msg = "Wrong type for property prop1: expected List(I64) but actual type is Str" with pytest.raises( - Exception, - match=re.escape(msg), + Exception, + match=re.escape(msg), ): graph.filter(filter_expr).nodes.id diff --git a/python/tests/test_base_install/test_graphdb/test_persistent_graph.py b/python/tests/test_base_install/test_graphdb/test_persistent_graph.py index 3b7369ce32..c80a54c631 100644 --- a/python/tests/test_base_install/test_graphdb/test_persistent_graph.py +++ b/python/tests/test_base_install/test_graphdb/test_persistent_graph.py @@ -256,7 +256,7 @@ def test_filtering_valid(): g.add_edge(2, 1, 3, layer="blue", properties={"weight": 2}) g.add_edge(3, 1, 3, layer="red", properties={"weight": 3}) - f = filter.Edge.property("weight").temporal().latest() < 3 + f = filter.Edge.property("weight").temporal().last() < 3 e = g.filter(f).edge(1, 2) assert e is None # assert list(e.properties.temporal.get("weight").values) == [1,2] -- this would be the case for filter_edge_layer? diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index 6a5332d040..f6b238df81 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -953,7 +953,7 @@ def test_graph_nodes_property_filter_starts_with(graph): filter: { temporalProperty: { name: "prop3", - temporal: ALL, + ops: [ALL], operator: STARTS_WITH value: { str: "abc1" } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index c77589cca6..e2859e4ccd 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -904,7 +904,7 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): filter: { temporalProperty: { name: "prop3", - temporal: FIRST, + ops: [FIRST] operator: STARTS_WITH value: { str: "abc" } } @@ -940,7 +940,7 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): filter: { temporalProperty: { name: "prop3", - temporal: ALL, + ops: [ALL] operator: STARTS_WITH value: { str: "abc1" } } @@ -967,8 +967,8 @@ def test_nodes_property_filter_list_agg(graph): property: { name: "prop5" operator: EQUAL + ops: [SUM] value: { i64: 6 } - listAgg:SUM } }) { nodes { @@ -993,8 +993,8 @@ def test_nodes_property_filter_list_qualifier(graph): property: { name: "prop5" operator: EQUAL + ops: [ANY] value: { i64: 6 } - elemQualifier: ANY } }) { nodes { @@ -1022,9 +1022,8 @@ def test_nodes_temporal_property_filter_agg(graph): nodeFilter(filter: { temporalProperty: { name: "p2" - temporal:VALUES operator: LESS_THAN - listAgg: AVG + ops: [AVG] value: { f64: 10.0 } } }) { @@ -1041,3 +1040,35 @@ def test_nodes_temporal_property_filter_agg(graph): "graph": {"nodeFilter": {"nodes": {"list": [{"name": "2"}, {"name": "3"}]}}} } run_graphql_test(query, expected_output, graph) + + +EVENT_GRAPH = create_test_graph(Graph()) +PERSISTENT_GRAPH = create_test_graph(PersistentGraph()) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_temporal_property_filter_any_avg(graph): + query = """ + query { + graph(path: "g") { + nodeFilter(filter: { + temporalProperty: { + name: "prop5" + operator: LESS_THAN + ops: [ANY, AVG] + value: { f64: 10.0 } + } + }) { + nodes { + list { + name + } + } + } + } + } + """ + expected_output = { + "graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} + } + run_graphql_test(query, expected_output, graph) diff --git a/python/tests/test_base_install/test_index.py b/python/tests/test_base_install/test_index.py index a6780de3a0..92c44eda7d 100644 --- a/python/tests/test_base_install/test_index.py +++ b/python/tests/test_base_install/test_index.py @@ -534,38 +534,38 @@ def test_search_nodes_for_property_temporal_any_is_none(): assert [] == results -def test_search_nodes_for_property_temporal_latest_eq(): +def test_search_nodes_for_property_temporal_last_eq(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest() == 1 + filter_expr = filter.Node.property("p1").temporal().last() == 1 results = search_nodes(g, filter_expr) assert ["N1", "N3", "N4", "N6", "N7"] == results -def test_search_nodes_for_property_temporal_latest_ne(): +def test_search_nodes_for_property_temporal_last_ne(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest() != 2 + filter_expr = filter.Node.property("p1").temporal().last() != 2 results = search_nodes(g, filter_expr) assert ["N1", "N10", "N11", "N12", "N13", "N3", "N4", "N6", "N7"] == results -def test_search_nodes_for_property_temporal_latest_lt(): +def test_search_nodes_for_property_temporal_last_lt(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest() < 2 + filter_expr = filter.Node.property("p1").temporal().last() < 2 results = search_nodes(g, filter_expr) assert ["N1", "N3", "N4", "N6", "N7"] == results -def test_search_nodes_for_property_temporal_latest_le(): +def test_search_nodes_for_property_temporal_last_le(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest() <= 3 + filter_expr = filter.Node.property("p1").temporal().last() <= 3 results = search_nodes(g, filter_expr) assert [ "N1", @@ -584,47 +584,47 @@ def test_search_nodes_for_property_temporal_latest_le(): ] == results -def test_search_nodes_for_property_temporal_latest_gt(): +def test_search_nodes_for_property_temporal_last_gt(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest() > 1 + filter_expr = filter.Node.property("p1").temporal().last() > 1 results = search_nodes(g, filter_expr) assert ["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"] == results -def test_search_nodes_for_property_temporal_latest_ge(): +def test_search_nodes_for_property_temporal_last_ge(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest() >= 2 + filter_expr = filter.Node.property("p1").temporal().last() >= 2 results = search_nodes(g, filter_expr) assert ["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"] == results -def test_search_nodes_for_property_temporal_latest_is_in(): +def test_search_nodes_for_property_temporal_last_is_in(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest().is_in([2]) + filter_expr = filter.Node.property("p1").temporal().last().is_in([2]) results = search_nodes(g, filter_expr) assert ["N2", "N5", "N8", "N9"] == results -def test_search_nodes_for_property_temporal_latest_is_not_in(): +def test_search_nodes_for_property_temporal_last_is_not_in(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest().is_not_in([2]) + filter_expr = filter.Node.property("p1").temporal().last().is_not_in([2]) results = search_nodes(g, filter_expr) assert ["N1", "N10", "N11", "N12", "N13", "N3", "N4", "N6", "N7"] == results -def test_search_nodes_for_property_temporal_latest_is_some(): +def test_search_nodes_for_property_temporal_last_is_some(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest().is_some() + filter_expr = filter.Node.property("p1").temporal().last().is_some() results = search_nodes(g, filter_expr) assert [ "N1", @@ -654,11 +654,11 @@ def test_search_nodes_for_composite_filter(): @pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_nodes_for_property_temporal_latest_is_none(): +def test_search_nodes_for_property_temporal_last_is_none(): g = Graph() g = init_graph(g) - filter_expr = filter.Node.property("p1").temporal().latest().is_none() + filter_expr = filter.Node.property("p1").temporal().last().is_none() results = search_nodes(g, filter_expr) assert [] == results @@ -1343,11 +1343,11 @@ def test_search_edges_for_property_temporal_any_is_none(): assert [] == results -def test_search_edges_for_property_temporal_latest_eq(): +def test_search_edges_for_property_temporal_last_eq(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest() == 1 + filter_expr = filter.Edge.property("p1").temporal().last() == 1 results = search_edges(g, filter_expr) assert [ ("N1", "N2"), @@ -1358,11 +1358,11 @@ def test_search_edges_for_property_temporal_latest_eq(): ] == results -def test_search_edges_for_property_temporal_latest_ne(): +def test_search_edges_for_property_temporal_last_ne(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest() != 2 + filter_expr = filter.Edge.property("p1").temporal().last() != 2 results = search_edges(g, filter_expr) assert [ ("N1", "N2"), @@ -1377,11 +1377,11 @@ def test_search_edges_for_property_temporal_latest_ne(): ] == results -def test_search_edges_for_property_temporal_latest_lt(): +def test_search_edges_for_property_temporal_last_lt(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest() < 2 + filter_expr = filter.Edge.property("p1").temporal().last() < 2 results = search_edges(g, filter_expr) assert [ ("N1", "N2"), @@ -1392,11 +1392,11 @@ def test_search_edges_for_property_temporal_latest_lt(): ] == results -def test_search_edges_for_property_temporal_latest_le(): +def test_search_edges_for_property_temporal_last_le(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest() <= 3 + filter_expr = filter.Edge.property("p1").temporal().last() <= 3 results = search_edges(g, filter_expr) assert [ ("N1", "N2"), @@ -1415,11 +1415,11 @@ def test_search_edges_for_property_temporal_latest_le(): ] == results -def test_search_edges_for_property_temporal_latest_gt(): +def test_search_edges_for_property_temporal_last_gt(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest() > 1 + filter_expr = filter.Edge.property("p1").temporal().last() > 1 results = search_edges(g, filter_expr) assert [ ("N10", "N11"), @@ -1433,11 +1433,11 @@ def test_search_edges_for_property_temporal_latest_gt(): ] == results -def test_search_edges_for_property_temporal_latest_ge(): +def test_search_edges_for_property_temporal_last_ge(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest() >= 2 + filter_expr = filter.Edge.property("p1").temporal().last() >= 2 results = search_edges(g, filter_expr) assert [ ("N10", "N11"), @@ -1451,20 +1451,20 @@ def test_search_edges_for_property_temporal_latest_ge(): ] == results -def test_search_edges_for_property_temporal_latest_is_in(): +def test_search_edges_for_property_temporal_last_is_in(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest().is_in([2]) + filter_expr = filter.Edge.property("p1").temporal().last().is_in([2]) results = search_edges(g, filter_expr) assert [("N2", "N3"), ("N5", "N6"), ("N8", "N9"), ("N9", "N10")] == results -def test_search_edges_for_property_temporal_latest_is_not_in(): +def test_search_edges_for_property_temporal_last_is_not_in(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest().is_not_in([2]) + filter_expr = filter.Edge.property("p1").temporal().last().is_not_in([2]) results = search_edges(g, filter_expr) assert [ ("N1", "N2"), @@ -1479,11 +1479,11 @@ def test_search_edges_for_property_temporal_latest_is_not_in(): ] == results -def test_search_edges_for_property_temporal_latest_is_some(): +def test_search_edges_for_property_temporal_last_is_some(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest().is_some() + filter_expr = filter.Edge.property("p1").temporal().last().is_some() results = search_edges(g, filter_expr) assert [ ("N1", "N2"), @@ -1503,11 +1503,11 @@ def test_search_edges_for_property_temporal_latest_is_some(): @pytest.mark.skip(reason="Ignoring this test temporarily") -def test_search_edges_for_property_temporal_latest_is_none(): +def test_search_edges_for_property_temporal_last_is_none(): g = Graph() g = init_edges_graph(g) - filter_expr = filter.Edge.property("p1").temporal().latest().is_none() + filter_expr = filter.Edge.property("p1").temporal().last().is_none() results = search_edges(g, filter_expr) assert [] == results @@ -1517,7 +1517,7 @@ def test_search_edges_for_composite_filter(): g = init_edges_graph(g) filter1 = filter.Edge.src().name() == "N13" - filter2 = filter.Edge.property("p1").temporal().latest() == 3 + filter2 = filter.Edge.property("p1").temporal().last() == 3 results = search_edges(g, filter1 & filter2) assert [("N13", "N14")] == results @@ -1527,6 +1527,6 @@ def test_search_edges_for_composite_filter_pg(): g = init_edges_graph(g) filter1 = filter.Edge.src().name() == "N13" - filter2 = filter.Edge.property("p1").temporal().latest() == 3 + filter2 = filter.Edge.property("p1").temporal().last() == 3 results = search_edges(g, filter1 & filter2) assert [("N13", "N14")] == results diff --git a/raphtory-api/src/core/entities/properties/prop/prop_type.rs b/raphtory-api/src/core/entities/properties/prop/prop_type.rs index 8bd80958ee..cb29ceffdb 100644 --- a/raphtory-api/src/core/entities/properties/prop/prop_type.rs +++ b/raphtory-api/src/core/entities/properties/prop/prop_type.rs @@ -78,6 +78,13 @@ impl Display for PropType { } impl PropType { + pub fn inner(&self) -> Option<&PropType> { + match self { + PropType::List(inner) => Some(inner.as_ref()), + _ => None, + } + } + pub fn map(fields: impl IntoIterator, PropType)>) -> Self { let map: HashMap<_, _> = fields.into_iter().map(|(k, v)| (k.into(), v)).collect(); PropType::Map(Arc::from(map)) diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 2867c0ef03..f95736a85f 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -1053,19 +1053,6 @@ type LayerSchema { edges: [EdgeSchema!]! } -enum ListAgg { - LEN - SUM - AVG - MIN - MAX -} - -enum ListElemQualifier { - ANY - ALL -} - type MetaGraph { """ Returns the graph name. @@ -1134,13 +1121,9 @@ input MetadataFilterExpr { """ value: Value """ - List aggregate - """ - listAgg: ListAgg - """ - List qualifier + Ops List. """ - elemQualifier: ListElemQualifier + ops: [OpName!] } type MutRoot { @@ -1891,6 +1874,18 @@ input ObjectEntry { value: Value! } +enum OpName { + FIRST + LAST + ANY + ALL + LEN + SUM + AVG + MIN + MAX +} + enum Operator { """ Equality operator. @@ -2161,13 +2156,9 @@ input PropertyFilterExpr { """ value: Value """ - List aggregate - """ - listAgg: ListAgg - """ - List qualifier + Ops List. """ - elemQualifier: ListElemQualifier + ops: [OpName!] } input PropertyInput { @@ -2331,10 +2322,6 @@ input TemporalPropertyFilterExpr { """ name: String! """ - Type of temporal property. Choose from: any, latest. - """ - temporal: TemporalType! - """ Operator. """ operator: Operator! @@ -2343,13 +2330,9 @@ input TemporalPropertyFilterExpr { """ value: Value """ - List aggregate - """ - listAgg: ListAgg + Ops List. """ - List qualifier - """ - elemQualifier: ListElemQualifier + ops: [OpName!] } input TemporalPropertyInput { @@ -2363,20 +2346,6 @@ input TemporalPropertyInput { properties: [PropertyInput!] } -enum TemporalType { - """ - Any. - """ - ANY - """ - Latest. - """ - LATEST - FIRST - ALL - VALUES -} - scalar Upload input Value @oneOf { diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 7ca96e2051..6e859f7e82 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -11,9 +11,7 @@ use raphtory::{ edge_filter::CompositeEdgeFilter, filter_operator::FilterOperator, node_filter::CompositeNodeFilter, - property_filter::{ - ListAgg, ListElemQualifier, PropertyFilter, PropertyFilterValue, PropertyRef, Temporal, - }, + property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, Filter, FilterValue, }, errors::GraphError, @@ -426,23 +424,23 @@ pub enum GqlListElemQualifier { All, } -impl From for ListElemQualifier { +impl From for Op { fn from(q: GqlListElemQualifier) -> Self { match q { - GqlListElemQualifier::Any => ListElemQualifier::Any, - GqlListElemQualifier::All => ListElemQualifier::All, + GqlListElemQualifier::Any => Op::Any, + GqlListElemQualifier::All => Op::All, } } } -impl From for ListAgg { +impl From for Op { fn from(a: GqlListAgg) -> Self { match a { - GqlListAgg::Len => ListAgg::Len, - GqlListAgg::Sum => ListAgg::Sum, - GqlListAgg::Avg => ListAgg::Avg, - GqlListAgg::Min => ListAgg::Min, - GqlListAgg::Max => ListAgg::Max, + GqlListAgg::Len => Op::Len, + GqlListAgg::Sum => Op::Sum, + GqlListAgg::Avg => Op::Avg, + GqlListAgg::Min => Op::Min, + GqlListAgg::Max => Op::Max, } } } @@ -455,21 +453,13 @@ pub struct PropertyFilterExpr { pub operator: Operator, /// Value. pub value: Option, - /// List aggregate - pub list_agg: Option, - /// List qualifier - pub elem_qualifier: Option, + /// Ops List. + pub ops: Option>, } impl PropertyFilterExpr { pub fn validate(&self) -> Result<(), GraphError> { - validate_operator_value_pair(self.operator, self.value.as_ref())?; - if self.elem_qualifier.is_some() && self.list_agg.is_some() { - return Err(GraphError::InvalidGqlFilter( - "List aggregation and element qualifier cannot be used together".into(), - )); - } - Ok(()) + validate_operator_value_pair(self.operator, self.value.as_ref()) } } @@ -481,21 +471,13 @@ pub struct MetadataFilterExpr { pub operator: Operator, /// Value. pub value: Option, - /// List aggregate - pub list_agg: Option, - /// List qualifier - pub elem_qualifier: Option, + /// Ops List. + pub ops: Option>, } impl MetadataFilterExpr { pub fn validate(&self) -> Result<(), GraphError> { - validate_operator_value_pair(self.operator, self.value.as_ref())?; - if self.elem_qualifier.is_some() && self.list_agg.is_some() { - return Err(GraphError::InvalidGqlFilter( - "List aggregation and element qualifier cannot be used together".into(), - )); - } - Ok(()) + validate_operator_value_pair(self.operator, self.value.as_ref()) } } @@ -503,49 +485,47 @@ impl MetadataFilterExpr { pub struct TemporalPropertyFilterExpr { /// Name. pub name: String, - /// Type of temporal property. Choose from: any, latest. - pub temporal: TemporalType, /// Operator. pub operator: Operator, /// Value. pub value: Option, - /// List aggregate - pub list_agg: Option, - /// List qualifier - pub elem_qualifier: Option, + /// Ops List. + pub ops: Option>, } impl TemporalPropertyFilterExpr { pub fn validate(&self) -> Result<(), GraphError> { - validate_operator_value_pair(self.operator, self.value.as_ref())?; - - if let TemporalType::Values = self.temporal { - if self.list_agg.is_none() { - return Err(GraphError::InvalidGqlFilter( - "temporal: VALUES must be used with a list_agg (len/sum/avg/min/max)".into(), - )); - } - if self.elem_qualifier.is_some() { - return Err(GraphError::InvalidGqlFilter( - "Element qualifiers (any/all) are not supported with temporal VALUES aggregation" - .into(), - )); - } - } - - Ok(()) + validate_operator_value_pair(self.operator, self.value.as_ref()) } } #[derive(Enum, Copy, Clone, Debug)] -pub enum TemporalType { - /// Any. - Any, - /// Latest. - Latest, +pub enum OpName { First, + Last, + Any, All, - Values, + Len, + Sum, + Avg, + Min, + Max, +} + +impl From for Op { + fn from(o: OpName) -> Self { + match o { + OpName::First => Op::First, + OpName::Last => Op::Last, + OpName::Any => Op::Any, + OpName::All => Op::All, + OpName::Len => Op::Len, + OpName::Sum => Op::Sum, + OpName::Avg => Op::Avg, + OpName::Min => Op::Min, + OpName::Max => Op::Max, + } + } } fn field_value(value: Value, operator: Operator) -> Result { @@ -838,8 +818,7 @@ fn build_property_filter( prop_ref: PropertyRef, operator: Operator, value: Option<&Value>, - list_agg: Option, - list_elem_qualifier: Option, + ops: Vec, ) -> Result, GraphError> { let prop = value.cloned().map(Prop::try_from).transpose()?; @@ -857,8 +836,7 @@ fn build_property_filter( prop_ref, prop_value, operator: operator.into(), - list_agg: list_agg.map(Into::into), - list_elem_qualifier: list_elem_qualifier.map(Into::into), + ops, _phantom: PhantomData, }) } @@ -867,12 +845,15 @@ impl TryFrom for PropertyFilter { type Error = GraphError; fn try_from(expr: PropertyFilterExpr) -> Result { + expr.validate()?; + + let ops: Vec<_> = expr.ops.into_iter().flatten().map(Into::into).collect(); + build_property_filter( PropertyRef::Property(expr.name), expr.operator, expr.value.as_ref(), - expr.list_agg, - expr.elem_qualifier, + ops, ) } } @@ -881,12 +862,15 @@ impl TryFrom for PropertyFilter { type Error = GraphError; fn try_from(expr: MetadataFilterExpr) -> Result { + expr.validate()?; + + let ops: Vec<_> = expr.ops.into_iter().flatten().map(Into::into).collect(); + build_property_filter( PropertyRef::Metadata(expr.name), expr.operator, expr.value.as_ref(), - expr.list_agg, - expr.elem_qualifier, + ops, ) } } @@ -895,12 +879,15 @@ impl TryFrom for PropertyFilter { type Error = GraphError; fn try_from(expr: TemporalPropertyFilterExpr) -> Result { + expr.validate()?; + + let ops: Vec<_> = expr.ops.into_iter().flatten().map(Into::into).collect(); + build_property_filter( - PropertyRef::TemporalProperty(expr.name, expr.temporal.into()), + PropertyRef::TemporalProperty(expr.name), expr.operator, expr.value.as_ref(), - expr.list_agg, - expr.elem_qualifier, + ops, ) } } @@ -937,18 +924,6 @@ impl From for FilterOperator { } } -impl From for Temporal { - fn from(temporal: TemporalType) -> Self { - match temporal { - TemporalType::Any => Temporal::Any, - TemporalType::Latest => Temporal::Latest, - TemporalType::First => Temporal::First, - TemporalType::All => Temporal::All, - TemporalType::Values => Temporal::Values, - } - } -} - fn validate_id_operator_value_pair(operator: Operator, value: &Value) -> Result<(), GraphError> { use Operator::*; diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 2b500e03d2..67df94252e 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -22,7 +22,7 @@ mod test_fluent_builder_apis { use crate::db::graph::views::filter::model::{ edge_filter::{CompositeEdgeFilter, EdgeFilter, EdgeFilterOps}, node_filter::{CompositeNodeFilter, NodeFilter, NodeFilterBuilderOps}, - property_filter::{PropertyFilter, PropertyFilterOps, PropertyRef, Temporal}, + property_filter::{ElemQualifierOps, Op, PropertyFilter, PropertyFilterOps, PropertyRef}, ComposableFilter, Filter, PropertyFilterFactory, TryAsCompositeFilter, }; @@ -52,21 +52,21 @@ mod test_fluent_builder_apis { fn test_node_any_temporal_property_filter_build() { let filter_expr = NodeFilter::property("p").temporal().any().eq("raphtory"); let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::TemporalProperty("p".to_string(), Temporal::Any), - "raphtory", - )); + let node_property_filter2 = CompositeNodeFilter::Property( + PropertyFilter::eq(PropertyRef::TemporalProperty("p".to_string()), "raphtory") + .with_op(Op::Any), + ); assert_eq!(node_property_filter, node_property_filter2); } #[test] fn test_node_latest_temporal_property_filter_build() { - let filter_expr = NodeFilter::property("p").temporal().latest().eq("raphtory"); + let filter_expr = NodeFilter::property("p").temporal().last().eq("raphtory"); let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::TemporalProperty("p".to_string(), Temporal::Latest), - "raphtory", - )); + let node_property_filter2 = CompositeNodeFilter::Property( + PropertyFilter::eq(PropertyRef::TemporalProperty("p".to_string()), "raphtory") + .with_op(Op::Last), + ); assert_eq!(node_property_filter, node_property_filter2); } @@ -97,7 +97,7 @@ mod test_fluent_builder_apis { .temporal() .any() .eq(5u64) - .or(NodeFilter::property("p4").temporal().latest().eq(7u64)), + .or(NodeFilter::property("p4").temporal().last().eq(7u64)), ) .or(NodeFilter::node_type().eq("raphtory")) .or(NodeFilter::property("p5").eq(9u64)) @@ -124,14 +124,20 @@ mod test_fluent_builder_apis { ))), )), Box::new(CompositeNodeFilter::Or( - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::TemporalProperty("p3".to_string(), Temporal::Any), - 5u64, - ))), - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::TemporalProperty("p4".to_string(), Temporal::Latest), - 7u64, - ))), + Box::new(CompositeNodeFilter::Property( + PropertyFilter::eq( + PropertyRef::TemporalProperty("p3".to_string()), + 5u64, + ) + .with_op(Op::Any), + )), + Box::new(CompositeNodeFilter::Property( + PropertyFilter::eq( + PropertyRef::TemporalProperty("p4".to_string()), + 7u64, + ) + .with_op(Op::Last), + )), )), )), Box::new(CompositeNodeFilter::Node(Filter::eq( @@ -176,7 +182,7 @@ mod test_fluent_builder_apis { .temporal() .any() .eq(5u64) - .or(EdgeFilter::property("p4").temporal().latest().eq(7u64)), + .or(EdgeFilter::property("p4").temporal().last().eq(7u64)), ) .or(EdgeFilter::src().name().eq("raphtory")) .or(EdgeFilter::property("p5").eq(9u64)) @@ -200,14 +206,14 @@ mod test_fluent_builder_apis { ))), )), Box::new(CompositeEdgeFilter::Or( - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::TemporalProperty("p3".into(), Temporal::Any), - 5u64, - ))), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::TemporalProperty("p4".into(), Temporal::Latest), - 7u64, - ))), + Box::new(CompositeEdgeFilter::Property( + PropertyFilter::eq(PropertyRef::TemporalProperty("p3".into()), 5u64) + .with_op(Op::Any), + )), + Box::new(CompositeEdgeFilter::Property( + PropertyFilter::eq(PropertyRef::TemporalProperty("p4".into()), 7u64) + .with_op(Op::Last), + )), )), )), Box::new(CompositeEdgeFilter::Edge(Filter::eq("src", "raphtory"))), @@ -650,7 +656,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics() { - let filter = NodeFilter::property("p1").temporal().latest().eq(1u64); + let filter = NodeFilter::property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -670,7 +676,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics_for_secondary_indexes() { - let filter = NodeFilter::property("p1").temporal().latest().eq(1u64); + let filter = NodeFilter::property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1", "N16", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph_for_secondary_indexes, @@ -1188,7 +1194,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").temporal().latest().eq(1u64); + let filter = EdgeFilter::property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -1209,7 +1215,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics_for_secondary_indexes() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").temporal().latest().eq(1u64); + let filter = EdgeFilter::property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1->N2", "N16->N15", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( @@ -3262,7 +3268,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p10") .temporal() - .latest() + .last() .starts_with("Pape"); let expected_results: Vec<&str> = vec!["1", "2", "3"]; assert_filter_nodes_results( @@ -3282,7 +3288,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p10") .temporal() - .latest() + .last() .starts_with("Yohan"); let expected_results: Vec<&str> = vec![]; assert_filter_nodes_results( @@ -3382,7 +3388,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p10") .temporal() - .latest() + .last() .ends_with("ane"); let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( @@ -3402,7 +3408,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p10") .temporal() - .latest() + .last() .ends_with("Jerry"); let expected_results: Vec<&str> = vec![]; assert_filter_nodes_results( @@ -3502,7 +3508,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p10") .temporal() - .latest() + .last() .contains("Paper"); let expected_results: Vec<&str> = vec!["1", "2", "3"]; assert_filter_nodes_results( @@ -3602,7 +3608,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p10") .temporal() - .latest() + .last() .not_contains("ship"); let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( @@ -5173,12 +5179,12 @@ pub(crate) mod test_filters { apply_assertion(filter, &expected); } - // ------ Temporal latest: SUM ------ + // ------ Temporal last: SUM ------ #[test] - fn test_node_property_temporal_latest_sum_u8s() { + fn test_node_property_temporal_last_sum_u8s() { let filter = NodeFilter::property("p_u8s") .temporal() - .latest() + .last() .sum() .eq(Prop::U64(10)); let expected = vec!["n1"]; @@ -5186,10 +5192,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_sum_u16s() { + fn test_node_property_temporal_last_sum_u16s() { let filter = NodeFilter::property("p_u16s") .temporal() - .latest() + .last() .sum() .eq(Prop::U64(6)); let expected = vec!["n3", "n10"]; @@ -5197,10 +5203,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_sum_u32s() { + fn test_node_property_temporal_last_sum_u32s() { let filter = NodeFilter::property("p_u32s") .temporal() - .latest() + .last() .sum() .eq(Prop::U64(10)); let expected = vec!["n1"]; @@ -5208,10 +5214,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_sum_u64s() { + fn test_node_property_temporal_last_sum_u64s() { let filter = NodeFilter::property("p_u64s") .temporal() - .latest() + .last() .sum() .eq(Prop::U64(60)); let expected = vec!["n4"]; @@ -5219,10 +5225,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_sum_i32s() { + fn test_node_property_temporal_last_sum_i32s() { let filter = NodeFilter::property("p_i32s") .temporal() - .latest() + .last() .sum() .eq(Prop::I64(60)); let expected = vec!["n4"]; @@ -5230,10 +5236,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_sum_i64s() { + fn test_node_property_temporal_last_sum_i64s() { let filter = NodeFilter::property("p_i64s") .temporal() - .latest() + .last() .sum() .eq(Prop::I64(0)); let expected = vec!["n3", "n10"]; @@ -5241,10 +5247,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_sum_f32s() { + fn test_node_property_temporal_last_sum_f32s() { let filter = NodeFilter::property("p_f32s") .temporal() - .latest() + .last() .sum() .eq(Prop::F64(6.5)); let expected = vec!["n3", "n10"]; @@ -5252,22 +5258,22 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_sum_f64s() { + fn test_node_property_temporal_last_sum_f64s() { let filter = NodeFilter::property("p_f64s") .temporal() - .latest() + .last() .sum() .eq(Prop::F64(90.0)); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } - // ------ Temporal latest: AVG ------ + // ------ Temporal last: AVG ------ #[test] - fn test_node_property_temporal_latest_avg_u8s() { + fn test_node_property_temporal_last_avg_u8s() { let filter = NodeFilter::property("p_u8s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(2.5)); let expected = vec!["n1"]; @@ -5275,10 +5281,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_avg_u16s() { + fn test_node_property_temporal_last_avg_u16s() { let filter = NodeFilter::property("p_u16s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(2.0)); let expected = vec!["n3", "n10"]; @@ -5286,10 +5292,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_avg_u32s() { + fn test_node_property_temporal_last_avg_u32s() { let filter = NodeFilter::property("p_u32s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(2.5)); let expected = vec!["n1"]; @@ -5297,10 +5303,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_avg_u64s() { + fn test_node_property_temporal_last_avg_u64s() { let filter = NodeFilter::property("p_u64s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(20.0)); let expected = vec!["n4"]; @@ -5308,10 +5314,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_avg_i32s() { + fn test_node_property_temporal_last_avg_i32s() { let filter = NodeFilter::property("p_i32s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(0.6666666666666666)); let expected = vec!["n6"]; @@ -5319,10 +5325,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_avg_i64s() { + fn test_node_property_temporal_last_avg_i64s() { let filter = NodeFilter::property("p_i64s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(0.0)); let expected = vec!["n3", "n10"]; @@ -5330,10 +5336,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_avg_f32s() { + fn test_node_property_temporal_last_avg_f32s() { let filter = NodeFilter::property("p_f32s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(20.0)); let expected = vec!["n4"]; @@ -5341,22 +5347,22 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_avg_f64s() { + fn test_node_property_temporal_last_avg_f64s() { let filter = NodeFilter::property("p_f64s") .temporal() - .latest() + .last() .avg() .eq(Prop::F64(45.0)); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } - // ------ Temporal latest: MIN ------ + // ------ Temporal last: MIN ------ #[test] - fn test_node_property_temporal_latest_min_u8s() { + fn test_node_property_temporal_last_min_u8s() { let filter = NodeFilter::property("p_u8s") .temporal() - .latest() + .last() .min() .eq(Prop::U8(1)); let expected = vec!["n1", "n3", "n10"]; @@ -5364,10 +5370,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_min_u16s() { + fn test_node_property_temporal_last_min_u16s() { let filter = NodeFilter::property("p_u16s") .temporal() - .latest() + .last() .min() .eq(Prop::U16(1)); let expected = vec!["n1", "n3", "n10"]; @@ -5375,10 +5381,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_min_u32s() { + fn test_node_property_temporal_last_min_u32s() { let filter = NodeFilter::property("p_u32s") .temporal() - .latest() + .last() .min() .eq(Prop::U32(1)); let expected = vec!["n1", "n3", "n10"]; @@ -5386,10 +5392,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_min_u64s() { + fn test_node_property_temporal_last_min_u64s() { let filter = NodeFilter::property("p_u64s") .temporal() - .latest() + .last() .min() .eq(Prop::U64(10)); let expected = vec!["n4"]; @@ -5397,10 +5403,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_min_i32s() { + fn test_node_property_temporal_last_min_i32s() { let filter = NodeFilter::property("p_i32s") .temporal() - .latest() + .last() .min() .eq(Prop::I32(-2)); let expected = vec!["n6"]; @@ -5408,10 +5414,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_min_i64s() { + fn test_node_property_temporal_last_min_i64s() { let filter = NodeFilter::property("p_i64s") .temporal() - .latest() + .last() .min() .eq(Prop::I64(-3)); let expected = vec!["n3", "n10"]; @@ -5419,10 +5425,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_min_f32s() { + fn test_node_property_temporal_last_min_f32s() { let filter = NodeFilter::property("p_f32s") .temporal() - .latest() + .last() .min() .eq(Prop::F32(10.0)); let expected = vec!["n4"]; @@ -5430,22 +5436,22 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_min_f64s() { + fn test_node_property_temporal_last_min_f64s() { let filter = NodeFilter::property("p_f64s") .temporal() - .latest() + .last() .min() .eq(Prop::F64(40.0)); let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); } - // ------ Temporal latest: MAX ------ + // ------ Temporal last: MAX ------ #[test] - fn test_node_property_temporal_latest_max_u8s() { + fn test_node_property_temporal_last_max_u8s() { let filter = NodeFilter::property("p_u8s") .temporal() - .latest() + .last() .max() .eq(Prop::U8(4)); let expected = vec!["n1"]; @@ -5453,10 +5459,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_max_u16s() { + fn test_node_property_temporal_last_max_u16s() { let filter = NodeFilter::property("p_u16s") .temporal() - .latest() + .last() .max() .eq(Prop::U16(3)); let expected = vec!["n3", "n10"]; @@ -5464,10 +5470,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_max_u32s() { + fn test_node_property_temporal_last_max_u32s() { let filter = NodeFilter::property("p_u32s") .temporal() - .latest() + .last() .max() .eq(Prop::U32(4)); let expected = vec!["n1"]; @@ -5475,10 +5481,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_max_u64s() { + fn test_node_property_temporal_last_max_u64s() { let filter = NodeFilter::property("p_u64s") .temporal() - .latest() + .last() .max() .eq(Prop::U64(30)); let expected = vec!["n4"]; @@ -5486,10 +5492,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_max_i32s() { + fn test_node_property_temporal_last_max_i32s() { let filter = NodeFilter::property("p_i32s") .temporal() - .latest() + .last() .max() .eq(Prop::I32(3)); let expected = vec!["n3", "n6", "n10"]; @@ -5497,10 +5503,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_max_i64s() { + fn test_node_property_temporal_last_max_i64s() { let filter = NodeFilter::property("p_i64s") .temporal() - .latest() + .last() .max() .eq(Prop::I64(2)); let expected = vec!["n3", "n10"]; @@ -5508,10 +5514,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_max_f32s() { + fn test_node_property_temporal_last_max_f32s() { let filter = NodeFilter::property("p_f32s") .temporal() - .latest() + .last() .max() .eq(Prop::F32(3.5)); let expected = vec!["n3", "n10"]; @@ -5519,22 +5525,22 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_max_f64s() { + fn test_node_property_temporal_last_max_f64s() { let filter = NodeFilter::property("p_f64s") .temporal() - .latest() + .last() .max() .eq(Prop::F64(50.0)); let expected = vec!["n1", "n2", "n3", "n10"]; apply_assertion(filter, &expected); } - // ------ Temporal latest: LEN ------ + // ------ Temporal last: LEN ------ #[test] - fn test_node_property_temporal_latest_len_u8s() { + fn test_node_property_temporal_last_len_u8s() { let filter = NodeFilter::property("p_u8s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5542,10 +5548,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_len_u16s() { + fn test_node_property_temporal_last_len_u16s() { let filter = NodeFilter::property("p_u16s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5553,10 +5559,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_len_u32s() { + fn test_node_property_temporal_last_len_u32s() { let filter = NodeFilter::property("p_u32s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5564,10 +5570,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_len_u64s() { + fn test_node_property_temporal_last_len_u64s() { let filter = NodeFilter::property("p_u64s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n4", "n10"]; @@ -5575,10 +5581,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_len_i32s() { + fn test_node_property_temporal_last_len_i32s() { let filter = NodeFilter::property("p_i32s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n4", "n6", "n10"]; @@ -5586,10 +5592,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_len_i64s() { + fn test_node_property_temporal_last_len_i64s() { let filter = NodeFilter::property("p_i64s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n10"]; @@ -5597,10 +5603,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_len_f32s() { + fn test_node_property_temporal_last_len_f32s() { let filter = NodeFilter::property("p_f32s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(3)); let expected = vec!["n3", "n4", "n10"]; @@ -5608,10 +5614,10 @@ pub(crate) mod test_filters { } #[test] - fn test_node_property_temporal_latest_len_f64s() { + fn test_node_property_temporal_last_len_f64s() { let filter = NodeFilter::property("p_f64s") .temporal() - .latest() + .last() .len() .eq(Prop::U64(2)); let expected = vec!["n3", "n10"]; @@ -7246,7 +7252,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p_u64s") .temporal() - .latest() + .last() .min() .eq(Prop::U64(0)); let expected: Vec<&str> = vec![]; @@ -7407,24 +7413,24 @@ pub(crate) mod test_filters { apply_assertion(filter, &expected); } - // ------ Temporal Latest: any ------ + // ------ Temporal last: any ------ #[test] - fn test_node_temporal_property_latest_any() { + fn test_node_temporal_property_last_any() { let filter = NodeFilter::property("p_f32s") .temporal() - .latest() + .last() .any() .eq(Prop::F32(3.5)); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } - // ------ Temporal Latest: all ------ + // ------ Temporal last: all ------ #[test] - fn test_node_temporal_property_latest_all() { + fn test_node_temporal_property_last_all() { let filter = NodeFilter::property("p_bools_all") .temporal() - .latest() + .last() .all() .eq(true); let expected = vec!["n10", "n4"]; @@ -9280,7 +9286,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::property("p10") .temporal() - .latest() + .last() .starts_with("Paper"); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( @@ -9300,7 +9306,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::property("p10") .temporal() - .latest() + .last() .starts_with("Traffic"); let expected_results: Vec<&str> = vec![]; assert_filter_edges_results( @@ -9401,7 +9407,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::property("p10") .temporal() - .latest() + .last() .ends_with("ane"); let expected_results: Vec<&str> = vec!["1->2", "2->1"]; assert_filter_edges_results( @@ -9421,7 +9427,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::property("p10") .temporal() - .latest() + .last() .ends_with("marcus"); let expected_results: Vec<&str> = vec![]; assert_filter_edges_results( @@ -9522,7 +9528,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::property("p10") .temporal() - .latest() + .last() .contains("Paper"); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( @@ -9623,7 +9629,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::property("p10") .temporal() - .latest() + .last() .not_contains("ship"); let expected_results: Vec<&str> = vec!["1->2", "2->1"]; assert_filter_edges_results( diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index 4ce8dec912..b142c36847 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -39,42 +39,49 @@ use raphtory_storage::graph::{ use std::{collections::HashSet, fmt, fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ListAgg { +pub enum Op { + // Selectors + First, + Last, + // Aggregators Len, Sum, Avg, Min, Max, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ListElemQualifier { + // Qualifiers Any, All, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Temporal { - Any, - Latest, - First, - All, - Values, +impl Op { + #[inline] + pub fn is_selector(self) -> bool { + matches!(self, Op::First | Op::Last) + } + + #[inline] + pub fn is_aggregator(self) -> bool { + matches!(self, Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max) + } + + #[inline] + pub fn is_qualifier(self) -> bool { + matches!(self, Op::Any | Op::All) + } } #[derive(Debug, Clone, PartialEq, Eq)] pub enum PropertyRef { Property(String), Metadata(String), - TemporalProperty(String, Temporal), + TemporalProperty(String), } impl Display for PropertyRef { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - PropertyRef::TemporalProperty(name, temporal) => { - write!(f, "TemporalProperty({}, {:?})", name, temporal) - } + PropertyRef::TemporalProperty(name) => write!(f, "TemporalProperty({})", name), PropertyRef::Metadata(name) => write!(f, "Metadata({})", name), PropertyRef::Property(name) => write!(f, "Property({})", name), } @@ -86,7 +93,7 @@ impl PropertyRef { match self { PropertyRef::Property(name) | PropertyRef::Metadata(name) - | PropertyRef::TemporalProperty(name, _) => name, + | PropertyRef::TemporalProperty(name) => name, } } } @@ -103,58 +110,59 @@ pub struct PropertyFilter { pub prop_ref: PropertyRef, pub prop_value: PropertyFilterValue, pub operator: FilterOperator, - pub list_agg: Option, - pub list_elem_qualifier: Option, + pub ops: Vec, // at most 2 (validated) pub _phantom: PhantomData, } impl Display for PropertyFilter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let base = match &self.prop_ref { + let mut expr = match &self.prop_ref { PropertyRef::Property(name) => name.to_string(), PropertyRef::Metadata(name) => format!("const({})", name), - PropertyRef::TemporalProperty(name, t) => { - format!("temporal_{:?}({})", t, name) - } - }; - - let qualified = match self.list_elem_qualifier { - Some(ListElemQualifier::Any) => format!("any({})", base), - Some(ListElemQualifier::All) => format!("all({})", base), - None => base, + PropertyRef::TemporalProperty(name) => format!("temporal({})", name), }; - let decorated = match self.list_agg { - Some(ListAgg::Len) => format!("len({})", qualified), - Some(ListAgg::Sum) => format!("sum({})", qualified), - Some(ListAgg::Avg) => format!("avg({})", qualified), - Some(ListAgg::Min) => format!("min({})", qualified), - Some(ListAgg::Max) => format!("max({})", qualified), - None => qualified, - }; + for op in &self.ops { + expr = match op { + Op::First => format!("first({})", expr), + Op::Last => format!("last({})", expr), + Op::Len => format!("len({})", expr), + Op::Sum => format!("sum({})", expr), + Op::Avg => format!("avg({})", expr), + Op::Min => format!("min({})", expr), + Op::Max => format!("max({})", expr), + Op::Any => format!("any({})", expr), + Op::All => format!("all({})", expr), + }; + } match &self.prop_value { - PropertyFilterValue::None => write!(f, "{} {}", decorated, self.operator), - PropertyFilterValue::Single(value) => { - write!(f, "{} {} {}", decorated, self.operator, value) - } + PropertyFilterValue::None => write!(f, "{} {}", expr, self.operator), + PropertyFilterValue::Single(value) => write!(f, "{} {} {}", expr, self.operator, value), PropertyFilterValue::Set(values) => { let sorted = sort_comparable_props(values.iter().collect_vec()); let values_str = sorted.iter().map(|v| format!("{}", v)).join(", "); - write!(f, "{} {} [{}]", decorated, self.operator, values_str) + write!(f, "{} {} [{}]", expr, self.operator, values_str) } } } } +enum ValueType { + Seq(Vec), + Scalar(Option), +} + impl PropertyFilter { - pub fn with_list_agg(mut self, agg: Option) -> Self { - self.list_agg = agg; + #[inline] + pub fn with_op(mut self, op: Op) -> Self { + self.ops.push(op); self } - pub fn with_list_elem_qualifier(mut self, q: Option) -> Self { - self.list_elem_qualifier = q; + #[inline] + pub fn with_ops(mut self, ops: impl IntoIterator) -> Self { + self.ops.extend(ops); self } @@ -163,8 +171,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::Eq, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -174,8 +181,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::Ne, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -185,8 +191,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::Le, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -196,8 +201,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::Ge, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -207,8 +211,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::Lt, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -218,8 +221,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::Gt, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -229,8 +231,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Set(Arc::new(prop_values.into_iter().collect())), operator: FilterOperator::In, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -240,8 +241,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Set(Arc::new(prop_values.into_iter().collect())), operator: FilterOperator::NotIn, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -251,8 +251,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::None, operator: FilterOperator::IsNone, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -262,8 +261,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::None, operator: FilterOperator::IsSome, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -273,8 +271,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::StartsWith, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -284,8 +281,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::EndsWith, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -295,8 +291,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::Contains, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -306,8 +301,7 @@ impl PropertyFilter { prop_ref, prop_value: PropertyFilterValue::Single(prop_value.into()), operator: FilterOperator::NotContains, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -325,8 +319,7 @@ impl PropertyFilter { levenshtein_distance, prefix_match, }, - list_agg: None, - list_elem_qualifier: None, + ops: vec![], _phantom: PhantomData, } } @@ -358,7 +351,11 @@ impl PropertyFilter { Ok(filter_dtype) } - fn validate(&self, dtype: &PropType, expect_map: bool) -> Result<(), GraphError> { + fn validate_operator_against_dtype( + &self, + dtype: &PropType, + expect_map: bool, + ) -> Result<(), GraphError> { match self.operator { FilterOperator::Eq | FilterOperator::Ne => { self.validate_single_dtype(dtype, expect_map)?; @@ -383,205 +380,325 @@ impl PropertyFilter { | FilterOperator::EndsWith | FilterOperator::Contains | FilterOperator::NotContains - | FilterOperator::FuzzySearch { .. } => { - match &self.prop_value { - PropertyFilterValue::None => { - return Err(GraphError::InvalidFilterExpectSingleGotNone(self.operator)) - } - PropertyFilterValue::Single(v) => { - if !matches!(dtype, PropType::Str) || !matches!(v.dtype(), PropType::Str) { - return Err(GraphError::InvalidContains(self.operator)); - } - } - PropertyFilterValue::Set(_) => { - return Err(GraphError::InvalidFilterExpectSingleGotSet(self.operator)) + | FilterOperator::FuzzySearch { .. } => match &self.prop_value { + PropertyFilterValue::None => { + return Err(GraphError::InvalidFilterExpectSingleGotNone(self.operator)) + } + PropertyFilterValue::Single(v) => { + if !matches!(dtype, PropType::Str) || !matches!(v.dtype(), PropType::Str) { + return Err(GraphError::InvalidContains(self.operator)); } - }; - } - } - Ok(()) - } - - fn validate_list_agg_operator(&self) -> Result<(), GraphError> { - if self.list_agg.is_none() { - return Ok(()); - } - match self.operator { - FilterOperator::Eq - | FilterOperator::Ne - | FilterOperator::Lt - | FilterOperator::Le - | FilterOperator::Gt - | FilterOperator::Ge - | FilterOperator::In - | FilterOperator::NotIn => Ok(()), - FilterOperator::IsSome - | FilterOperator::IsNone - | FilterOperator::StartsWith - | FilterOperator::EndsWith - | FilterOperator::Contains - | FilterOperator::NotContains - | FilterOperator::FuzzySearch { .. } => Err(GraphError::InvalidFilter(format!( - "Operator {} is not supported with list aggregation {:?}; allowed: EQ, NE, LT, LE, GT, GE, IN, NOT_IN", - self.operator, self.list_agg - ))), - } - } - - /// Effective result dtype for (agg, element PropType) as per semantic: - /// Len -> U64 - /// Avg -> F64 - /// Sum -> U64 (unsigned), I64 (signed), F64 (float) - /// Min/Max -> same as element type (numeric only) - fn effective_dtype_for_list_agg_with_inner( - agg: ListAgg, - inner: &PropType, - ) -> Result { - Ok(match agg { - ListAgg::Len => PropType::U64, - ListAgg::Avg => PropType::F64, - ListAgg::Sum => match inner { - PropType::U8 | PropType::U16 | PropType::U32 | PropType::U64 => PropType::U64, - PropType::I32 | PropType::I64 => PropType::I64, - PropType::F32 | PropType::F64 => PropType::F64, - _ => { - return Err(GraphError::InvalidFilter(format!( - "SUM requires numeric list; got element type {:?}", - inner - ))) } - }, - ListAgg::Min | ListAgg::Max => match inner { - PropType::U8 - | PropType::U16 - | PropType::U32 - | PropType::U64 - | PropType::I32 - | PropType::I64 - | PropType::F32 - | PropType::F64 => inner.clone(), // same as element type - _ => { - return Err(GraphError::InvalidFilter(format!( - "{:?} requires numeric list; got element type {:?}", - agg, inner - ))) + PropertyFilterValue::Set(_) => { + return Err(GraphError::InvalidFilterExpectSingleGotSet(self.operator)) } }, - }) + } + Ok(()) } - fn validate_list_agg_and_effective_dtype( + /// Enforce the OK/NOK matrix and compute the **effective dtype** + /// after applying the op chain (used to validate the final operator). + /// + /// Op Pair Validity Matrix (first op × second op) + /// Selector Aggregator Qualifier + /// Selector NOK OK OK + /// Aggregator NOK NOK NOK + /// Qualifier NOK OK OK + /// + /// Single-op cases (standalone): + /// Selector → OK + /// Aggregator → OK + /// Qualifier → OK + fn validate_chain_and_infer_effective_dtype( &self, - prop_dtype: &PropType, + src_dtype: &PropType, + is_temporal: bool, ) -> Result { - if let PropType::List(inner) = prop_dtype { - Self::effective_dtype_for_list_agg_with_inner(self.list_agg.unwrap(), inner) - } else { - Err(GraphError::InvalidFilter(format!( - "List aggregation {:?} is only supported on list properties; got {:?}", - self.list_agg, prop_dtype - ))) + fn agg_result_dtype( + inner: &PropType, + op: Op, + ctx: &'static str, + ) -> Result { + use PropType::*; + Ok(match op { + Op::Len => U64, + + Op::Sum => match inner { + U8 | U16 | U32 | U64 => U64, + I32 | I64 => I64, + F32 | F64 => F64, + _ => { + return Err(GraphError::InvalidFilter(format!( + "sum() {} requires numeric", + ctx + ))) + } + }, + + Op::Avg => match inner { + U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 => F64, + _ => { + return Err(GraphError::InvalidFilter(format!( + "avg() {} requires numeric", + ctx + ))) + } + }, + + Op::Min | Op::Max => match inner { + U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 => inner.clone(), + _ => { + return Err(GraphError::InvalidFilter(format!( + "{:?} {} requires numeric", + op, ctx + ))) + } + }, + + _ => unreachable!(), + }) } - } - fn validate_agg_single_filter_clause_prop(&self, eff: &PropType) -> Result<(), GraphError> { - let _ = self.validate_single_dtype(eff, false)?; - // If strictly numeric op, ensure the eff type can compare numerically. - if self.operator.is_strictly_numeric_operation() && !eff.has_cmp() { - return Err(GraphError::InvalidFilterCmp(eff.clone())); + if self.ops.len() > 2 { + return Err(GraphError::InvalidFilter( + "At most two list/temporal operations are allowed.".into(), + )); } - Ok(()) - } - fn validate_agg_set_filter_clause_prop(&self) -> Result<(), GraphError> { - match &self.prop_value { - PropertyFilterValue::Set(_) => Ok(()), // filter clause prop list doesn't need to be homogenous - PropertyFilterValue::Single(_) | PropertyFilterValue::None => { - Err(GraphError::InvalidFilterExpectSetGotSingle(self.operator)) + let used_qualifier = self.ops.iter().any(|o| o.is_qualifier()); + if used_qualifier && !is_temporal { + if matches!( + self.operator, + FilterOperator::IsSome | FilterOperator::IsNone + ) { + return Err(GraphError::InvalidFilter( + "Operator IS_SOME/IS_NONE is not supported with element qualifiers; apply it to the list itself (without elem qualifiers).".into() + )); } } - } - fn validate_agg(&self, dtype: &PropType) -> Result<(), GraphError> { - self.validate_list_agg_operator()?; - let agg = self.list_agg.unwrap(); - let eff = match &self.prop_ref { - PropertyRef::TemporalProperty(_, Temporal::Values) => { - Self::effective_dtype_for_list_agg_with_inner(agg, dtype)? + let used_agg = self.ops.iter().any(|o| o.is_aggregator()); + let disallowed_with_agg = matches!( + self.operator, + FilterOperator::StartsWith + | FilterOperator::EndsWith + | FilterOperator::Contains + | FilterOperator::NotContains + | FilterOperator::FuzzySearch { .. } + | FilterOperator::IsNone + | FilterOperator::IsSome + ); + if used_agg && disallowed_with_agg { + return Err(GraphError::InvalidFilter(format!( + "Operator {} is not supported with list aggregation", + self.operator + ))); + } + + let require_iterable = + |op: Op, shape_is_seq: bool, shape_is_list: bool| -> Result<(), GraphError> { + if op.is_selector() { + if !shape_is_seq { + return Err(GraphError::InvalidFilter(format!( + "{:?} requires list or temporal source", + op + ))); + } + } else if op.is_aggregator() { + if !(shape_is_seq || shape_is_list) { + return Err(GraphError::InvalidFilter(format!( + "{:?} requires list or temporal source", + op + ))); + } + } else if op.is_qualifier() { + if !(shape_is_seq || shape_is_list) { + return Err(GraphError::InvalidFilter(format!( + "{:?} requires list or temporal source", + op + ))); + } + } + Ok(()) + }; + + let (shape_is_list, elem_ty) = (matches!(src_dtype, PropType::List(_)), src_dtype.clone()); + + // Pair rules (OK/NOK) + let pair_ok = |a: Op, b: Op| -> bool { + // NOK + if a.is_selector() && b.is_selector() { + return false; + } // [Sel, Sel] + if a.is_aggregator() && (b.is_selector() || b.is_qualifier() || b.is_aggregator()) { + return false; // [Agg, Sel] / [Agg, Qual] / [Agg, Agg] } - _ => self.validate_list_agg_and_effective_dtype(dtype)?, + if a.is_qualifier() && b.is_selector() { + return false; + } // [Qual, Sel] + // OK + if a.is_selector() && (b.is_aggregator() || b.is_qualifier()) { + return true; + } // [Sel, Agg] / [Sel, Qual] + if a.is_qualifier() && (b.is_aggregator() || b.is_qualifier()) { + return true; + } // [Qual, Agg] / [Qual, Qual] + true }; - match self.operator { - FilterOperator::Eq - | FilterOperator::Ne - | FilterOperator::Lt - | FilterOperator::Le - | FilterOperator::Gt - | FilterOperator::Ge => self.validate_agg_single_filter_clause_prop(&eff), - FilterOperator::In | FilterOperator::NotIn => { - self.validate_agg_set_filter_clause_prop() - } - _ => unreachable!("blocked by validate_list_agg_operator"), - } - } + match (is_temporal, self.ops.as_slice()) { + // Catch-all: if 3 or more ops are present, reject + (false, &[_, _, _, ..]) | (true, &[_, _, _, ..]) => Err(GraphError::InvalidFilter( + "At most two list/temporal operations are allowed.".into(), + )), - fn validate_list_elem_and_operator(&self, dtype: &PropType) -> Result<(), GraphError> { - // Get the inner element type as &PropType - let inner_ty: &PropType = match dtype { - PropType::List(inner) => inner.as_ref(), // <- &PropType - _ => { - return Err(GraphError::InvalidFilter(format!( - "Element qualifier {:?} is only supported on list properties; got {:?}", - self.list_elem_qualifier, dtype - ))) + // No ops + (true, []) => { + // effective dtype: List (comparing to the full time-series) + Ok(PropType::List(Box::new(elem_ty.clone()))) } - }; + (false, []) => Ok(elem_ty), - match self.operator { - FilterOperator::IsSome | FilterOperator::IsNone => { - return Err(GraphError::InvalidFilter( - "Operator IS_SOME/IS_NONE is not supported with element qualifiers; \ - apply it to the list itself (without elem qualifiers)." - .into(), - )); + // Non-temporal: exactly one op (must be Agg or Qual) on List + (false, [op]) => { + if !op.is_aggregator() && !op.is_qualifier() { + return Err(GraphError::InvalidFilter(format!( + "Non-temporal properties support only aggregators/qualifiers; got {:?}", + op + ))); + } + if !shape_is_list { + return Err(GraphError::InvalidFilter(format!( + "{:?} requires list; property is {:?}", + op, elem_ty + ))); + } + let inner = elem_ty.inner().ok_or_else(|| { + GraphError::InvalidFilter(format!("Expected list type, got {:?}", elem_ty)) + })?; + if op.is_aggregator() { + agg_result_dtype(inner, *op, "requires numeric list") + } else { + Ok(inner.clone()) + } } - FilterOperator::In | FilterOperator::NotIn => { - if let PropertyFilterValue::Set(ref s) = self.prop_value { - for v in s.iter() { - // inner_ty is &PropType, v.dtype() is PropType - unify_types(inner_ty, &v.dtype(), &mut false) - .map_err(|e| e.with_name(self.prop_ref.name().to_owned()))?; + // Non-temporal: two ops -> not allowed + (false, [_a, _b]) => Err(GraphError::InvalidFilter( + "Non-temporal properties support at most one op.".into(), + )), + + // Temporal: one op + (true, [op]) => { + require_iterable(*op, true, shape_is_list)?; + if op.is_selector() { + // Selecting a single instant from the temporal sequence. + // If the temporal element type is T, `first/last` yields T. + // If the temporal element type is List, it yields List. + let eff = elem_ty.clone(); + return Ok(eff); + } + + let eff = if op.is_aggregator() { + if shape_is_list { + return Err(GraphError::InvalidFilter(format!( + "{:?} over temporal requires scalar elements; got List", + op + ))); } + return agg_result_dtype(&elem_ty, *op, "over time"); } else { - return Err(GraphError::InvalidFilterExpectSetGotSingle(self.operator)); - } + if let PropType::List(inner) = &elem_ty { + inner.as_ref().clone() + } else { + elem_ty.clone() + } + }; + Ok(eff) } - _ => { - // Validate single value against the element type - let dt = self.validate_single_dtype(inner_ty, false)?; - if self.operator.is_strictly_numeric_operation() && !dt.has_cmp() { - return Err(GraphError::InvalidFilterCmp(dt)); + // Temporal: two ops + (true, [a, b]) => { + if !pair_ok(*a, *b) { + return Err(GraphError::InvalidFilter(format!( + "Invalid op pair: {:?} then {:?}", + a, b + ))); } - // For string-only ops, require element type Str - if self.operator.is_string_operation() && !matches!(inner_ty, PropType::Str) { - return Err(GraphError::InvalidContains(self.operator)); + match (*a, *b) { + (sa, sb) if sa.is_selector() && sb.is_aggregator() => { + if !shape_is_list { + return Err(GraphError::InvalidFilter( + "Selector then aggregator requires Seq[List[T]] (temporal of lists)".into(), + )); + } + let inner = elem_ty.inner().ok_or_else(|| { + GraphError::InvalidFilter(format!( + "Expected list type, got {:?}", + elem_ty + )) + })?; + agg_result_dtype(inner, sb, "requires numeric") + } + (sa, sb) if sa.is_selector() && sb.is_qualifier() => { + if !shape_is_list { + return Err(GraphError::InvalidFilter( + "Selector then qualifier requires Seq[List[T]]".into(), + )); + } + let inner = elem_ty.inner().ok_or_else(|| { + GraphError::InvalidFilter(format!( + "Expected list type, got {:?}", + elem_ty + )) + })?; + Ok(inner.clone()) + } + (qa, qb) if qa.is_qualifier() && qb.is_aggregator() => { + if !shape_is_list { + return Err(GraphError::InvalidFilter( + "Qualifier then aggregator requires Seq[List[T]]".into(), + )); + } + let inner = elem_ty.inner().ok_or_else(|| { + GraphError::InvalidFilter(format!( + "Expected list type, got {:?}", + elem_ty + )) + })?; + agg_result_dtype(inner, qb, "requires numeric") + } + (qa, qb) if qa.is_qualifier() && qb.is_qualifier() => { + // Two qualifiers on a temporal property: operator applies to INNER ELEMENTS. + // We must validate the operator against the *inner element type*, not Bool. + if !shape_is_list { + return Err(GraphError::InvalidFilter( + "Two qualifiers on a temporal property require Seq[List[T]]".into(), + )); + } + let inner = elem_ty.inner().ok_or_else(|| { + GraphError::InvalidFilter(format!( + "Expected list type, got {:?}", + elem_ty + )) + })?; + Ok(inner.clone()) + } + _ => unreachable!(), } } } - - Ok(()) } pub fn resolve_prop_id(&self, meta: &Meta, expect_map: bool) -> Result { - let (name, is_static) = match &self.prop_ref { - PropertyRef::Metadata(n) => (n.as_str(), true), - PropertyRef::Property(n) | PropertyRef::TemporalProperty(n, _) => (n.as_str(), false), + let (name, is_static, is_temporal) = match &self.prop_ref { + PropertyRef::Metadata(n) => (n.as_str(), true, false), + PropertyRef::Property(n) => (n.as_str(), false, false), + PropertyRef::TemporalProperty(n) => (n.as_str(), false, true), }; - let (id, dtype) = match meta.get_prop_id_and_type(name, is_static) { + let (id, original_dtype) = match meta.get_prop_id_and_type(name, is_static) { None => { return if is_static { Err(GraphError::MetadataMissingError(name.to_string())) @@ -592,319 +709,437 @@ impl PropertyFilter { Some((id, dtype)) => (id, dtype), }; - if let (Some(agg), Some(_q)) = (self.list_agg, self.list_elem_qualifier) { - return Err(GraphError::InvalidFilter(format!( - "List aggregation {:?} cannot be used after an element qualifier (any/all).", - agg - ))); - } - - if let PropertyRef::TemporalProperty(_, Temporal::Values) = &self.prop_ref { - if let Some(_) = self.list_agg { - self.validate_agg(&dtype)?; - return Ok(id); - } - - if self.list_elem_qualifier.is_some() { - return Err(GraphError::InvalidFilter( - "Element qualifiers (any/all) are not supported with temporal aggregation." - .into(), - )); - } - - return match self.operator { - FilterOperator::Eq | FilterOperator::Ne => { - match &self.prop_value { - PropertyFilterValue::Single(Prop::List(list)) => { - // Empty list is allowed; otherwise check homogeneity & dtype compatibility - if let Some(first) = list.first() { - let first_ty = first.dtype(); - unify_types(&dtype, &first_ty, &mut false) - .map_err(|e| e.with_name(self.prop_ref.name().to_owned()))?; - for v in list.iter().skip(1) { - unify_types(&first_ty, &v.dtype(), &mut false).map_err( - |e| e.with_name(self.prop_ref.name().to_owned()), - )?; - } - } - Ok(id) - } - PropertyFilterValue::Single(_) => { - Err(GraphError::InvalidFilter( - "temporal() == / != expects a list value (e.g. [1,2,3])".into(), - )) - } - PropertyFilterValue::Set(_) | PropertyFilterValue::None => { - Err(GraphError::InvalidFilter( - "temporal() == / != expects a single list value".into(), - )) - } - } - } - _ => { - Err(GraphError::InvalidFilter( - "temporal() without aggregation supports only EQ/NE against a list (e.g. [..])." - .into(), - )) - } - } - } - - if let Some(_) = self.list_agg { - self.validate_agg(&dtype)?; - } else if let Some(_) = self.list_elem_qualifier { - self.validate_list_elem_and_operator(&dtype)?; + // Decide map-semantics for metadata + let is_original_map = matches!(original_dtype, PropType::Map(..)); + let rhs_is_map = matches!(self.prop_value, PropertyFilterValue::Single(Prop::Map(_))); + let expect_map_now = if is_static && rhs_is_map { + true } else { - self.validate(&dtype, is_static && expect_map)?; - } + is_static && expect_map && is_original_map + }; + + // Validate chain and final operator with correct semantics + let eff = self.validate_chain_and_infer_effective_dtype(&original_dtype, is_temporal)?; + // (Redundant safety) final check + self.validate_operator_against_dtype(&eff, expect_map_now)?; Ok(id) } - fn scan_u64_sum(vals: &[Prop]) -> Option<(bool, u64, u128)> { - let mut sum64: u64 = 0; - let mut sum128: u128 = 0; - let mut promoted = false; - - for p in vals { - let x = p.as_u64_lossless()?; - if !promoted { - if let Some(s) = sum64.checked_add(x) { - sum64 = s; + fn aggregate_values(vals: &[Prop], op: Op) -> Option { + fn scan_u64_sum(vals: &[Prop]) -> Option<(bool, u64, u128)> { + let mut sum64: u64 = 0; + let mut sum128: u128 = 0; + let mut promoted = false; + + for p in vals { + let x = p.as_u64_lossless()?; + if !promoted { + if let Some(s) = sum64.checked_add(x) { + sum64 = s; + } else { + promoted = true; + sum128 = (sum64 as u128) + (x as u128); + } } else { - promoted = true; - sum128 = (sum64 as u128) + (x as u128); + sum128 += x as u128; } - } else { - sum128 += x as u128; } + Some((promoted, sum64, sum128)) } - Some((promoted, sum64, sum128)) - } - fn scan_i64_sum(vals: &[Prop]) -> Option<(bool, i64, i128)> { - let mut sum64: i64 = 0; - let mut sum128: i128 = 0; - let mut promoted = false; - - for p in vals { - let x = p.as_i64_lossless()?; - if !promoted { - if let Some(s) = sum64.checked_add(x) { - sum64 = s; + fn scan_i64_sum(vals: &[Prop]) -> Option<(bool, i64, i128)> { + let mut sum64: i64 = 0; + let mut sum128: i128 = 0; + let mut promoted = false; + + for p in vals { + let x = p.as_i64_lossless()?; + if !promoted { + if let Some(s) = sum64.checked_add(x) { + sum64 = s; + } else { + promoted = true; + sum128 = (sum64 as i128) + (x as i128); + } } else { - promoted = true; - sum128 = (sum64 as i128) + (x as i128); + sum128 += x as i128; } - } else { - sum128 += x as i128; } + Some((promoted, sum64, sum128)) } - Some((promoted, sum64, sum128)) - } - - fn scan_u64_min_max(vals: &[Prop]) -> Option<(u64, u64)> { - let mut it = vals.iter(); - let first = it.next()?.as_u64_lossless()?; - let mut min_v = first; - let mut max_v = first; - for p in it { - let x = p.as_u64_lossless()?; - if x < min_v { - min_v = x; - } - if x > max_v { - max_v = x; + + fn scan_u64_min_max(vals: &[Prop]) -> Option<(u64, u64)> { + let mut it = vals.iter(); + let first = it.next()?.as_u64_lossless()?; + let mut min_v = first; + let mut max_v = first; + for p in it { + let x = p.as_u64_lossless()?; + if x < min_v { + min_v = x; + } + if x > max_v { + max_v = x; + } } + Some((min_v, max_v)) } - Some((min_v, max_v)) - } - - fn scan_i64_min_max(vals: &[Prop]) -> Option<(i64, i64)> { - let mut it = vals.iter(); - let first = it.next()?.as_i64_lossless()?; - let mut min_v = first; - let mut max_v = first; - for p in it { - let x = p.as_i64_lossless()?; - if x < min_v { - min_v = x; - } - if x > max_v { - max_v = x; + + fn scan_i64_min_max(vals: &[Prop]) -> Option<(i64, i64)> { + let mut it = vals.iter(); + let first = it.next()?.as_i64_lossless()?; + let mut min_v = first; + let mut max_v = first; + for p in it { + let x = p.as_i64_lossless()?; + if x < min_v { + min_v = x; + } + if x > max_v { + max_v = x; + } } + Some((min_v, max_v)) } - Some((min_v, max_v)) - } - fn scan_f64_sum_count(vals: &[Prop]) -> Option<(f64, u64)> { - let mut sum = 0.0f64; - let mut count = 0u64; - for p in vals { - let x = p.as_f64_lossless()?; - if !x.is_finite() { - return None; + fn scan_f64_sum_count(vals: &[Prop]) -> Option<(f64, u64)> { + let mut sum = 0.0f64; + let mut count = 0u64; + for p in vals { + let x = p.as_f64_lossless()?; + if !x.is_finite() { + return None; + } + sum += x; + count += 1; } - sum += x; - count += 1; + Some((sum, count)) } - Some((sum, count)) - } - fn scan_f64_min_max(vals: &[Prop]) -> Option<(f64, f64)> { - let mut it = vals.iter(); - let first = it.next()?.as_f64_lossless()?; - if !first.is_finite() { - return None; - } - let mut min_v = first; - let mut max_v = first; - for p in it { - let x = p.as_f64_lossless()?; - if !x.is_finite() { + fn scan_f64_min_max(vals: &[Prop]) -> Option<(f64, f64)> { + let mut it = vals.iter(); + let first = it.next()?.as_f64_lossless()?; + if !first.is_finite() { return None; } - if x < min_v { - min_v = x; - } - if x > max_v { - max_v = x; + let mut min_v = first; + let mut max_v = first; + for p in it { + let x = p.as_f64_lossless()?; + if !x.is_finite() { + return None; + } + if x < min_v { + min_v = x; + } + if x > max_v { + max_v = x; + } } + Some((min_v, max_v)) } - Some((min_v, max_v)) - } - fn reduce_unsigned(vals: &[Prop], ret_minmax: fn(u64) -> Prop, agg: ListAgg) -> Option { - match agg { - ListAgg::Sum => { - let (promoted, s64, s128) = Self::scan_u64_sum(vals)?; - Some(if promoted { - Prop::U64(u64::try_from(s128).ok()?) - } else { - Prop::U64(s64) - }) - } - ListAgg::Avg => { - let (promoted, s64, s128) = Self::scan_u64_sum(vals)?; - let count = vals.len() as u64; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) + fn reduce_unsigned(vals: &[Prop], ret_minmax: fn(u64) -> Prop, op: Op) -> Option { + match op { + Op::Sum => { + let (promoted, s64, s128) = scan_u64_sum(vals)?; + Some(if promoted { + Prop::U64(u64::try_from(s128).ok()?) + } else { + Prop::U64(s64) + }) + } + Op::Avg => { + let (promoted, s64, s128) = scan_u64_sum(vals)?; + let count = vals.len() as u64; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + Op::Min => scan_u64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), + Op::Max => scan_u64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), + Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } - ListAgg::Min => Self::scan_u64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), - ListAgg::Max => Self::scan_u64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), - ListAgg::Len => unreachable!(), } - } - fn reduce_signed(vals: &[Prop], ret_minmax: fn(i64) -> Prop, agg: ListAgg) -> Option { - match agg { - ListAgg::Sum => { - let (promoted, s64, s128) = Self::scan_i64_sum(vals)?; - Some(if promoted { - Prop::I64(i64::try_from(s128).ok()?) - } else { - Prop::I64(s64) - }) + fn reduce_signed(vals: &[Prop], ret_minmax: fn(i64) -> Prop, op: Op) -> Option { + match op { + Op::Sum => { + let (promoted, s64, s128) = scan_i64_sum(vals)?; + Some(if promoted { + Prop::I64(i64::try_from(s128).ok()?) + } else { + Prop::I64(s64) + }) + } + Op::Avg => { + let (promoted, s64, s128) = scan_i64_sum(vals)?; + let count = vals.len() as u64; + let s = if promoted { s128 as f64 } else { s64 as f64 }; + Some(Prop::F64(s / (count as f64))) + } + Op::Min => scan_i64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), + Op::Max => scan_i64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), + Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } - ListAgg::Avg => { - let (promoted, s64, s128) = Self::scan_i64_sum(vals)?; - let count = vals.len() as u64; - let s = if promoted { s128 as f64 } else { s64 as f64 }; - Some(Prop::F64(s / (count as f64))) + } + + fn reduce_float(vals: &[Prop], ret_minmax: fn(f64) -> Prop, op: Op) -> Option { + match op { + Op::Sum => scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)), + Op::Avg => { + let (sum, count) = scan_f64_sum_count(vals)?; + Some(Prop::F64(sum / (count as f64))) + } + Op::Min => scan_f64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), + Op::Max => scan_f64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), + Op::Len | Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } - ListAgg::Min => Self::scan_i64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), - ListAgg::Max => Self::scan_i64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), - ListAgg::Len => unreachable!(), } - } - fn reduce_float(vals: &[Prop], ret_minmax: fn(f64) -> Prop, agg: ListAgg) -> Option { - match agg { - ListAgg::Sum => Self::scan_f64_sum_count(vals).map(|(sum, _)| Prop::F64(sum)), - ListAgg::Avg => { - let (sum, count) = Self::scan_f64_sum_count(vals)?; - Some(Prop::F64(sum / (count as f64))) + match op { + Op::Len => Some(Prop::U64(vals.len() as u64)), + Op::Sum | Op::Avg | Op::Min | Op::Max => { + if vals.is_empty() { + return None; + } + let inner = vals[0].dtype(); + match inner { + PropType::U8 => reduce_unsigned(vals, |x| Prop::U8(x as u8), op), + PropType::U16 => reduce_unsigned(vals, |x| Prop::U16(x as u16), op), + PropType::U32 => reduce_unsigned(vals, |x| Prop::U32(x as u32), op), + PropType::U64 => reduce_unsigned(vals, |x| Prop::U64(x), op), + + PropType::I32 => reduce_signed(vals, |x| Prop::I32(x as i32), op), + PropType::I64 => reduce_signed(vals, |x| Prop::I64(x), op), + + PropType::F32 => reduce_float(vals, |x| Prop::F32(x as f32), op), + PropType::F64 => reduce_float(vals, |x| Prop::F64(x), op), + _ => None, + } } - ListAgg::Min => Self::scan_f64_min_max(vals).map(|(mn, _)| ret_minmax(mn)), - ListAgg::Max => Self::scan_f64_min_max(vals).map(|(_, mx)| ret_minmax(mx)), - ListAgg::Len => unreachable!(), + Op::First | Op::Last | Op::Any | Op::All => unreachable!(), } } - fn aggregate_values(vals: &[Prop], agg: ListAgg) -> Option { - if vals.is_empty() { - return match agg { - ListAgg::Len => Some(Prop::U64(0)), - _ => None, + fn apply_two_qualifiers_temporal(&self, prop: &[Prop], outer: Op, inner: Op) -> bool { + debug_assert!(outer.is_qualifier() && inner.is_qualifier()); + + let mut per_time: Vec = Vec::with_capacity(prop.len()); + for v in prop { + // Only lists participate. Non-lists => "no elements" at that time. + let elems: &[Prop] = match v { + Prop::List(inner_vals) => inner_vals.as_slice(), + _ => &[], // <-- do NOT coerce a scalar into a 1-element list }; + + let inner_ok = match inner { + Op::Any => elems + .iter() + .any(|e| self.operator.apply_to_property(&self.prop_value, Some(e))), + Op::All => { + // All requires at least one element. + !elems.is_empty() + && elems + .iter() + .all(|e| self.operator.apply_to_property(&self.prop_value, Some(e))) + } + _ => unreachable!(), + }; + + per_time.push(inner_ok); } - if let ListAgg::Len = agg { - return Some(Prop::U64(vals.len() as u64)); + + match outer { + Op::Any => per_time.into_iter().any(|b| b), + Op::All => !per_time.is_empty() && per_time.into_iter().all(|b| b), + _ => unreachable!(), } + } - // Assume homogeneity (validated elsewhere). Use the first value's dtype. - let inner = vals[0].dtype(); - match inner { - PropType::U8 => Self::reduce_unsigned(vals, |x| Prop::U8(x as u8), agg), - PropType::U16 => Self::reduce_unsigned(vals, |x| Prop::U16(x as u16), agg), - PropType::U32 => Self::reduce_unsigned(vals, |x| Prop::U32(x as u32), agg), - PropType::U64 => Self::reduce_unsigned(vals, |x| Prop::U64(x), agg), + fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Option) { + let mut qualifier: Option = None; - PropType::I32 => Self::reduce_signed(vals, |x| Prop::I32(x as i32), agg), - PropType::I64 => Self::reduce_signed(vals, |x| Prop::I64(x), agg), + let has_later_reduce = |ops: &[Op], i: usize| -> bool { + ops.iter() + .enumerate() + .skip(i + 1) + .any(|(_, op)| op.is_selector() || op.is_aggregator() || op.is_qualifier()) + }; - PropType::F32 => Self::reduce_float(vals, |x| Prop::F32(x as f32), agg), - PropType::F64 => Self::reduce_float(vals, |x| Prop::F64(x), agg), + let flatten_one = |vals: Vec| -> Vec { + let mut out = Vec::new(); + for p in vals { + if let Prop::List(inner) = p { + out.extend(inner.as_slice().iter().cloned()); + } else { + out.push(p); + } + } + out + }; - // Non-numeric: only Len is supported (already handled). - _ => None, - } - } + let per_elem_map = |vals: Vec, op: Op| -> Vec { + vals.into_iter() + .filter_map(|p| match (op, p) { + (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), + (Op::Sum, Prop::List(inner)) => { + Self::aggregate_values(inner.as_slice(), Op::Sum) + } + (Op::Avg, Prop::List(inner)) => { + Self::aggregate_values(inner.as_slice(), Op::Avg) + } + (Op::Min, Prop::List(inner)) => { + Self::aggregate_values(inner.as_slice(), Op::Min) + } + (Op::Max, Prop::List(inner)) => { + Self::aggregate_values(inner.as_slice(), Op::Max) + } + _ => None, + }) + .collect() + }; - fn aggregate_list_value(&self, list_prop: &Prop, agg: ListAgg) -> Option { - match list_prop { - Prop::List(v) => Self::aggregate_values(v.as_slice(), agg), - _ => None, + for (i, op) in self.ops.iter().enumerate() { + match *op { + Op::First => { + state = match state { + ValueType::Seq(vs) => ValueType::Scalar(vs.first().cloned()), + ValueType::Scalar(s) => ValueType::Scalar(s), + }; + } + Op::Last => { + state = match state { + ValueType::Seq(vs) => ValueType::Scalar(vs.last().cloned()), + ValueType::Scalar(s) => ValueType::Scalar(s), + }; + } + Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { + state = match state { + ValueType::Seq(vs) => { + if matches!(vs.first(), Some(Prop::List(_))) { + ValueType::Seq(per_elem_map(vs, *op)) + } else { + ValueType::Scalar(Self::aggregate_values(&vs, *op)) + } + } + ValueType::Scalar(Some(Prop::List(inner))) => { + ValueType::Scalar(Self::aggregate_values(inner.as_slice(), *op)) + } + ValueType::Scalar(Some(_)) => ValueType::Scalar(None), + ValueType::Scalar(None) => ValueType::Scalar(None), + }; + } + Op::Any | Op::All => { + qualifier = Some(*op); + let later = has_later_reduce(&self.ops, i); + state = match state { + ValueType::Seq(vs) => { + if later { + ValueType::Seq(vs) + } else { + ValueType::Seq(flatten_one(vs)) + } + } + ValueType::Scalar(Some(Prop::List(inner))) => { + if later { + ValueType::Seq(vec![Prop::List(inner)]) + } else { + ValueType::Seq(inner.as_slice().to_vec()) + } + } + ValueType::Scalar(Some(p)) => ValueType::Seq(vec![p]), + ValueType::Scalar(None) => ValueType::Seq(vec![]), + }; + } + } } - } - fn aggregate_temporal_values<'a>( - &self, - iter: impl Iterator, - agg: ListAgg, - ) -> Option { - if matches!(agg, ListAgg::Len) { - return Some(Prop::U64(iter.into_iter().count() as u64)); + if let Some(q) = qualifier { + let elems = match state { + ValueType::Seq(vs) => vs, + ValueType::Scalar(Some(Prop::List(inner))) => inner.as_slice().to_vec(), + ValueType::Scalar(Some(p)) => vec![p], + ValueType::Scalar(None) => vec![], + }; + return (None, Some(elems), Some(q)); } - let values: Vec = iter.into_iter().collect(); - Self::aggregate_values(&values, agg) + match state { + ValueType::Scalar(v) => (v, None, None), + ValueType::Seq(vs) => (None, Some(vs), None), + } } - pub fn matches(&self, other: Option<&Prop>) -> bool { - if let Some(agg) = self.list_agg { - let agg_other = other.and_then(|x| self.aggregate_list_value(x, agg)); + fn apply_eval( + &self, + reduced: Option, + maybe_seq: Option>, + qualifier: Option, + ) -> bool { + // 1) Reduced scalar -> compare directly + // For example: + // 1. NodeFilter::property("temp").temporal().avg().ge(Prop::F64(10.0)) + // 2. NodeFilter::property("readings").temporal().first().len().eq(Prop::U64(3)) + // 3. NodeFilter::property("scores").avg().gt(Prop::F64(0.5)) + if let Some(value) = reduced { return self .operator - .apply_to_property(&self.prop_value, agg_other.as_ref()); + .apply_to_property(&self.prop_value, Some(&value)); } - if let Some(q) = self.list_elem_qualifier { - let vals = match other { - Some(Prop::List(v)) => v.as_slice(), - _ => return false, // not a list - }; + // 2) Qualifier over a sequence (ANY/ALL) + // For example: + // 1. NodeFilter::property("tags").any().eq(Prop::Str("gold".into())) + // 2. NodeFilter::property("price").temporal().any().gt(Prop::F64(100.0)) + if let Some(q) = qualifier { + let vals = maybe_seq.unwrap_or_default(); if vals.is_empty() { return false; } - let chk = |val: &Prop| self.operator.apply_to_property(&self.prop_value, Some(val)); + let chk = |v: &Prop| self.operator.apply_to_property(&self.prop_value, Some(v)); return match q { - ListElemQualifier::Any => vals.iter().any(chk), - ListElemQualifier::All => vals.iter().all(chk), + Op::Any => vals.iter().any(chk), + Op::All => vals.iter().all(chk), + _ => unreachable!(), }; } - self.operator.apply_to_property(&self.prop_value, other) + // 3) Compare whole sequence as a List, or missing value + // For example: + // 1. NodeFilter::property("temperature").temporal().eq(Prop::List(vec![...])) + // 2. NodeFilter::property("tags").eq(Prop::List(vec!["gold", "silver"])) + if let Some(seq) = maybe_seq { + let full = Prop::List(Arc::new(seq)); + self.operator + .apply_to_property(&self.prop_value, Some(&full)) + } else { + self.operator.apply_to_property(&self.prop_value, None) + } + } + + fn eval_scalar_and_apply(&self, prop: Option) -> bool { + let (reduced, maybe_seq, qualifier) = self.eval_ops(ValueType::Scalar(prop)); + self.apply_eval(reduced, maybe_seq, qualifier) + } + + fn eval_temporal_and_apply(&self, props: Vec) -> bool { + // Special-case: two qualifiers on temporal -> directly compute final bool. + // For example: + // 1. NodeFilter::property("p_flags").temporal().all().all().eq(Prop::u64(1)) + // 2. NodeFilter::property("p_flags").temporal().any().all().eq(Prop::bool(true)) + if self.ops.len() == 2 && self.ops[0].is_qualifier() && self.ops[1].is_qualifier() { + return self.apply_two_qualifiers_temporal(&props, self.ops[0], self.ops[1]); + } + let (reduced, maybe_seq, qualifier) = self.eval_ops(ValueType::Seq(props)); + self.apply_eval(reduced, maybe_seq, qualifier) + } + + pub fn matches(&self, other: Option<&Prop>) -> bool { + if self.ops.is_empty() { + return self.operator.apply_to_property(&self.prop_value, other); + } + self.eval_scalar_and_apply(other.cloned()) } fn is_property_matched( @@ -914,55 +1149,16 @@ impl PropertyFilter { ) -> bool { match self.prop_ref { PropertyRef::Property(_) => { - let prop_value = props.get_by_id(t_prop_id); - self.matches(prop_value.as_ref()) + let prop = props.get_by_id(t_prop_id); + self.matches(prop.as_ref()) } PropertyRef::Metadata(_) => false, - PropertyRef::TemporalProperty(_, Temporal::Any) => props - .temporal() - .get_by_id(t_prop_id) - .filter(|prop_view| prop_view.values().any(|v| self.matches(Some(&v)))) - .is_some(), - PropertyRef::TemporalProperty(_, Temporal::Latest) => { - let prop_value = props - .temporal() - .get_by_id(t_prop_id) - .and_then(|prop_view| prop_view.latest()); - self.matches(prop_value.as_ref()) - } - PropertyRef::TemporalProperty(_, Temporal::First) => { - let prop_value = props - .temporal() - .get_by_id(t_prop_id) - .and_then(|prop_view| prop_view.first()); - self.matches(prop_value.as_ref()) - } - PropertyRef::TemporalProperty(_, Temporal::All) => props - .temporal() - .get_by_id(t_prop_id) - .filter(|prop_view| { - let has_any = prop_view.values().next().is_some(); - let all_ok = prop_view.values().all(|v| self.matches(Some(&v))); - has_any && all_ok - }) - .is_some(), - PropertyRef::TemporalProperty(_, Temporal::Values) => { - let tview = match props.temporal().get_by_id(t_prop_id) { - Some(v) => v, - None => return false, + PropertyRef::TemporalProperty(_) => { + let Some(tview) = props.temporal().get_by_id(t_prop_id) else { + return false; }; - - if let Some(agg) = self.list_agg { - let agg_prop = self.aggregate_temporal_values(tview.values(), agg); - self.operator - .apply_to_property(&self.prop_value, agg_prop.as_ref()) - } else { - // No aggregation: compare the full temporal history as a list - let values: Vec = tview.values().collect(); - let full = Prop::List(Arc::new(values)); - self.operator - .apply_to_property(&self.prop_value, Some(&full)) - } + let props: Vec = tview.values().collect(); + self.eval_temporal_and_apply(props) } } } @@ -993,7 +1189,7 @@ impl PropertyFilter { let props = node.metadata(); self.is_metadata_matched(prop_id, props) } - PropertyRef::TemporalProperty(_, _) | PropertyRef::Property(_) => { + PropertyRef::TemporalProperty(_) | PropertyRef::Property(_) => { let props = node.properties(); self.is_property_matched(prop_id, props) } @@ -1012,7 +1208,7 @@ impl PropertyFilter { let props = edge.metadata(); self.is_metadata_matched(prop_id, props) } - PropertyRef::TemporalProperty(_, _) | PropertyRef::Property(_) => { + PropertyRef::TemporalProperty(_) | PropertyRef::Property(_) => { let props = edge.properties(); self.is_property_matched(prop_id, props) } @@ -1033,7 +1229,7 @@ impl PropertyFilter { let props = edge.metadata(); self.is_metadata_matched(prop_id, props) } - PropertyRef::TemporalProperty(_, _) | PropertyRef::Property(_) => { + PropertyRef::TemporalProperty(_) | PropertyRef::Property(_) => { let props = edge.properties(); self.is_property_matched(prop_id, props) } @@ -1094,28 +1290,18 @@ pub trait InternalPropertyFilterOps: Send + Sync { fn property_ref(&self) -> PropertyRef; - fn list_agg(&self) -> Option { - None - } - - fn list_elem_qualifier(&self) -> Option { - None + fn ops(&self) -> &[Op] { + &[] } } impl InternalPropertyFilterOps for Arc { type Marker = T::Marker; - fn property_ref(&self) -> PropertyRef { self.deref().property_ref() } - - fn list_agg(&self) -> Option { - self.deref().list_agg() - } - - fn list_elem_qualifier(&self) -> Option { - self.deref().list_elem_qualifier() + fn ops(&self) -> &[Op] { + self.deref().ops() } } @@ -1158,87 +1344,63 @@ pub trait PropertyFilterOps: InternalPropertyFilterOps { impl PropertyFilterOps for T { fn eq(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::eq(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::eq(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } fn ne(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::ne(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::ne(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } fn le(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::le(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::le(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } fn ge(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::ge(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::ge(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } fn lt(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::lt(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::lt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } fn gt(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::gt(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::gt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } fn is_in(&self, values: impl IntoIterator) -> PropertyFilter { - PropertyFilter::is_in(self.property_ref(), values) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::is_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) } fn is_not_in(&self, values: impl IntoIterator) -> PropertyFilter { - PropertyFilter::is_not_in(self.property_ref(), values) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::is_not_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) } fn is_none(&self) -> PropertyFilter { - PropertyFilter::is_none(self.property_ref()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::is_none(self.property_ref()).with_ops(self.ops().iter().copied()) } fn is_some(&self) -> PropertyFilter { - PropertyFilter::is_some(self.property_ref()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + PropertyFilter::is_some(self.property_ref()).with_ops(self.ops().iter().copied()) } fn starts_with(&self, value: impl Into) -> PropertyFilter { PropertyFilter::starts_with(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + .with_ops(self.ops().iter().copied()) } fn ends_with(&self, value: impl Into) -> PropertyFilter { PropertyFilter::ends_with(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + .with_ops(self.ops().iter().copied()) } fn contains(&self, value: impl Into) -> PropertyFilter { PropertyFilter::contains(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + .with_ops(self.ops().iter().copied()) } fn not_contains(&self, value: impl Into) -> PropertyFilter { PropertyFilter::not_contains(self.property_ref(), value.into()) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + .with_ops(self.ops().iter().copied()) } fn fuzzy_search( @@ -1253,8 +1415,7 @@ impl PropertyFilterOps for T { levenshtein_distance, prefix_match, ) - .with_list_agg(self.list_agg()) - .with_list_elem_qualifier(self.list_elem_qualifier()) + .with_ops(self.ops().iter().copied()) } } @@ -1269,66 +1430,11 @@ impl PropertyFilterBuilder { impl InternalPropertyFilterOps for PropertyFilterBuilder { type Marker = M; - fn property_ref(&self) -> PropertyRef { PropertyRef::Property(self.0.clone()) } } -#[derive(Clone)] -pub struct AnyElemFilterBuilder(pub PropertyRef, PhantomData); - -#[derive(Clone)] -pub struct AllElemFilterBuilder(pub PropertyRef, PhantomData); - -pub trait ElemQualifierOps: InternalPropertyFilterOps { - fn any(self) -> AnyElemFilterBuilder - where - Self: Sized, - { - AnyElemFilterBuilder(self.property_ref(), PhantomData) - } - - fn all(self) -> AllElemFilterBuilder - where - Self: Sized, - { - AllElemFilterBuilder(self.property_ref(), PhantomData) - } -} - -impl ElemQualifierOps for T {} - -impl InternalPropertyFilterOps for AnyElemFilterBuilder { - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - self.0.clone() - } - - fn list_elem_qualifier(&self) -> Option { - Some(ListElemQualifier::Any) - } -} - -impl InternalPropertyFilterOps for AllElemFilterBuilder { - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - self.0.clone() - } - - fn list_elem_qualifier(&self) -> Option { - Some(ListElemQualifier::All) - } -} - -impl PropertyFilterBuilder { - pub fn temporal(self) -> TemporalPropertyFilterBuilder { - TemporalPropertyFilterBuilder(self.0, PhantomData) - } -} - #[derive(Clone)] pub struct MetadataFilterBuilder(pub String, PhantomData); @@ -1340,240 +1446,152 @@ impl MetadataFilterBuilder { impl InternalPropertyFilterOps for MetadataFilterBuilder { type Marker = M; - fn property_ref(&self) -> PropertyRef { PropertyRef::Metadata(self.0.clone()) } } #[derive(Clone)] -pub struct TemporalPropertyFilterBuilder(pub String, PhantomData); - -impl TemporalPropertyFilterBuilder { - pub fn any(self) -> AnyTemporalPropertyFilterBuilder { - AnyTemporalPropertyFilterBuilder(self.0, PhantomData) - } - - pub fn latest(self) -> LatestTemporalPropertyFilterBuilder { - LatestTemporalPropertyFilterBuilder(self.0, PhantomData) - } - - pub fn first(self) -> FirstTemporalPropertyFilterBuilder { - FirstTemporalPropertyFilterBuilder(self.0, PhantomData) - } - - pub fn all(self) -> AllTemporalPropertyFilterBuilder { - AllTemporalPropertyFilterBuilder(self.0, PhantomData) - } -} - -impl InternalPropertyFilterOps - for TemporalPropertyFilterBuilder -{ - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::Values) - } - - fn list_agg(&self) -> Option { - None - } - - fn list_elem_qualifier(&self) -> Option { - None - } -} - -impl ListAggOps for TemporalPropertyFilterBuilder { - fn property_ref_for_self(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::Values) - } -} - -#[derive(Clone)] -pub struct AnyTemporalPropertyFilterBuilder(pub String, PhantomData); - -impl InternalPropertyFilterOps - for AnyTemporalPropertyFilterBuilder -{ - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::Any) - } -} - -#[derive(Clone)] -pub struct LatestTemporalPropertyFilterBuilder(pub String, PhantomData); - -impl InternalPropertyFilterOps - for LatestTemporalPropertyFilterBuilder -{ - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::Latest) - } -} - -#[derive(Clone)] -pub struct FirstTemporalPropertyFilterBuilder(pub String, PhantomData); - -impl InternalPropertyFilterOps - for FirstTemporalPropertyFilterBuilder -{ - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::First) - } -} - -#[derive(Clone)] -pub struct AllTemporalPropertyFilterBuilder(pub String, PhantomData); - -impl InternalPropertyFilterOps - for AllTemporalPropertyFilterBuilder -{ - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::All) - } +pub struct OpChainBuilder { + pub prop_ref: PropertyRef, + pub ops: Vec, + pub _phantom: PhantomData, } -#[derive(Clone)] -pub struct LenFilterBuilder(pub PropertyRef, PhantomData); - -#[derive(Clone)] -pub struct SumFilterBuilder(pub PropertyRef, PhantomData); - -#[derive(Clone)] -pub struct AvgFilterBuilder(pub PropertyRef, PhantomData); - -#[derive(Clone)] -pub struct MinFilterBuilder(pub PropertyRef, PhantomData); - -#[derive(Clone)] -pub struct MaxFilterBuilder(pub PropertyRef, PhantomData); - -pub trait ListAggOps: Sized { - fn property_ref_for_self(&self) -> PropertyRef; - - fn len(self) -> LenFilterBuilder { - LenFilterBuilder(self.property_ref_for_self(), PhantomData) +impl OpChainBuilder { + pub fn with_op(mut self, op: Op) -> Self { + self.ops.push(op); + self } - fn sum(self) -> SumFilterBuilder { - SumFilterBuilder(self.property_ref_for_self(), PhantomData) + pub fn with_ops(mut self, ops: impl IntoIterator) -> Self { + self.ops.extend(ops); + self } - fn avg(self) -> AvgFilterBuilder { - AvgFilterBuilder(self.property_ref_for_self(), PhantomData) + pub fn first(self) -> Self { + self.with_op(Op::First) } - fn min(self) -> MinFilterBuilder { - MinFilterBuilder(self.property_ref_for_self(), PhantomData) + pub fn last(self) -> Self { + self.with_op(Op::Last) } - fn max(self) -> MaxFilterBuilder { - MaxFilterBuilder(self.property_ref_for_self(), PhantomData) + pub fn any(self) -> Self { + self.with_op(Op::Any) } -} -impl ListAggOps for PropertyFilterBuilder { - fn property_ref_for_self(&self) -> PropertyRef { - PropertyRef::Property(self.0.clone()) + pub fn all(self) -> Self { + self.with_op(Op::All) } -} -impl ListAggOps for MetadataFilterBuilder { - fn property_ref_for_self(&self) -> PropertyRef { - PropertyRef::Metadata(self.0.clone()) + pub fn len(self) -> Self { + self.with_op(Op::Len) } -} -impl ListAggOps for AnyTemporalPropertyFilterBuilder { - fn property_ref_for_self(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::Any) + pub fn sum(self) -> Self { + self.with_op(Op::Sum) } -} -impl ListAggOps for LatestTemporalPropertyFilterBuilder { - fn property_ref_for_self(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::Latest) + pub fn avg(self) -> Self { + self.with_op(Op::Avg) } -} -impl ListAggOps for FirstTemporalPropertyFilterBuilder { - fn property_ref_for_self(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::First) + pub fn min(self) -> Self { + self.with_op(Op::Min) } -} -impl ListAggOps for AllTemporalPropertyFilterBuilder { - fn property_ref_for_self(&self) -> PropertyRef { - PropertyRef::TemporalProperty(self.0.clone(), Temporal::All) + pub fn max(self) -> Self { + self.with_op(Op::Max) } } -impl InternalPropertyFilterOps for LenFilterBuilder { +impl InternalPropertyFilterOps for OpChainBuilder { type Marker = M; fn property_ref(&self) -> PropertyRef { - self.0.clone() + self.prop_ref.clone() } - fn list_agg(&self) -> Option { - Some(ListAgg::Len) + fn ops(&self) -> &[Op] { + &self.ops } } -impl InternalPropertyFilterOps for SumFilterBuilder { - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - self.0.clone() +pub trait ElemQualifierOps: InternalPropertyFilterOps { + fn any(self) -> OpChainBuilder + where + Self: Sized, + { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::Any]).collect(), + _phantom: PhantomData, + } } - fn list_agg(&self) -> Option { - Some(ListAgg::Sum) + fn all(self) -> OpChainBuilder + where + Self: Sized, + { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::All]).collect(), + _phantom: PhantomData, + } } } +impl ElemQualifierOps for T {} -impl InternalPropertyFilterOps for AvgFilterBuilder { - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - self.0.clone() - } - - fn list_agg(&self) -> Option { - Some(ListAgg::Avg) +impl PropertyFilterBuilder { + pub fn temporal(self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: PropertyRef::TemporalProperty(self.0), + ops: vec![], + _phantom: PhantomData, + } } } -impl InternalPropertyFilterOps for MinFilterBuilder { - type Marker = M; - - fn property_ref(&self) -> PropertyRef { - self.0.clone() +pub trait ListAggOps: InternalPropertyFilterOps + Sized { + fn len(self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::Len]).collect(), + _phantom: PhantomData, + } } - fn list_agg(&self) -> Option { - Some(ListAgg::Min) + fn sum(self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::Sum]).collect(), + _phantom: PhantomData, + } } -} -impl InternalPropertyFilterOps for MaxFilterBuilder { - type Marker = M; + fn avg(self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::Avg]).collect(), + _phantom: PhantomData, + } + } - fn property_ref(&self) -> PropertyRef { - self.0.clone() + fn min(self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::Min]).collect(), + _phantom: PhantomData, + } } - fn list_agg(&self) -> Option { - Some(ListAgg::Max) + fn max(self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::Max]).collect(), + _phantom: PhantomData, + } } } +impl ListAggOps for T {} diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 0d13948f17..2a896ec88c 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -4,7 +4,6 @@ use crate::python::filter::{ node_filter_builders::PyNodeFilter, property_filter_builders::{ PyMetadataFilterBuilder, PyPropertyFilterBuilder, PyPropertyFilterOps, - PyTemporalPropertyFilterBuilder, }, }; use pyo3::{ @@ -29,7 +28,6 @@ pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> { filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; - filter_module.add_class::()?; Ok(filter_module) } diff --git a/raphtory/src/python/filter/property_filter_builders.rs b/raphtory/src/python/filter/property_filter_builders.rs index db86d612ae..7405f02820 100644 --- a/raphtory/src/python/filter/property_filter_builders.rs +++ b/raphtory/src/python/filter/property_filter_builders.rs @@ -4,7 +4,7 @@ use crate::{ model::{ property_filter::{ ElemQualifierOps, InternalPropertyFilterOps, ListAggOps, MetadataFilterBuilder, - PropertyFilterBuilder, PropertyFilterOps, TemporalPropertyFilterBuilder, + OpChainBuilder, PropertyFilterBuilder, PropertyFilterOps, }, TryAsCompositeFilter, }, @@ -140,35 +140,40 @@ pub struct PyPropertyFilterOps { ops: Arc, agg: Arc, qual: Arc, + sel: Arc, } impl PyPropertyFilterOps { - fn from_parts( + fn from_parts( ops_provider: Arc, agg_provider: Arc, qual_provider: Arc, + sel_provider: Arc, ) -> Self where O: DynPropertyFilterOps + 'static, A: DynListAggOps + 'static, Q: DynElemQualifierOps + 'static, + S: DynSelectorOps + 'static, { PyPropertyFilterOps { ops: ops_provider, agg: agg_provider, qual: qual_provider, + sel: sel_provider, } } fn from_builder(builder: B) -> Self where - B: DynPropertyFilterOps + DynListAggOps + DynElemQualifierOps + 'static, + B: DynPropertyFilterOps + DynListAggOps + DynElemQualifierOps + DynSelectorOps + 'static, { let shared: Arc = Arc::new(builder); PyPropertyFilterOps { ops: shared.clone(), agg: shared.clone(), - qual: shared, + qual: shared.clone(), + sel: shared, } } } @@ -241,6 +246,14 @@ impl PyPropertyFilterOps { .fuzzy_search(prop_value, levenshtein_distance, prefix_match) } + pub fn first(&self) -> PyResult { + self.sel.first() + } + + pub fn last(&self) -> PyResult { + self.sel.last() + } + pub fn any(&self) -> PyResult { self.qual.any() } @@ -310,21 +323,25 @@ impl DynListAggOps for NoListAggOps { "List aggregation len cannot be used after an element qualifier (any/all).", )) } + fn sum(&self) -> PyResult { Err(PyTypeError::new_err( "List aggregation sum cannot be used after an element qualifier (any/all).", )) } + fn avg(&self) -> PyResult { Err(PyTypeError::new_err( "List aggregation avg cannot be used after an element qualifier (any/all).", )) } + fn min(&self) -> PyResult { Err(PyTypeError::new_err( "List aggregation min cannot be used after an element qualifier (any/all).", )) } + fn max(&self) -> PyResult { Err(PyTypeError::new_err( "List aggregation max cannot be used after an element qualifier (any/all).", @@ -332,186 +349,190 @@ impl DynListAggOps for NoListAggOps { } } +trait DynSelectorOps: Send + Sync { + fn first(&self) -> PyResult; + + fn last(&self) -> PyResult; +} + +#[derive(Clone)] +struct NoSelector; + +impl DynSelectorOps for NoSelector { + fn first(&self) -> PyResult { + Err(PyTypeError::new_err( + "first() is only valid on temporal properties.", + )) + } + + fn last(&self) -> PyResult { + Err(PyTypeError::new_err( + "last() is only valid on temporal properties.", + )) + } +} + impl DynListAggOps for T where - T: ListAggOps<::Marker> - + InternalPropertyFilterOps - + Clone - + Send - + Sync - + 'static, + T: ListAggOps + InternalPropertyFilterOps + Clone + Send + Sync + 'static, PropertyFilter<::Marker>: CreateFilter + TryAsCompositeFilter, { fn len(&self) -> PyResult { - let ops = Arc::new(>::len(self.clone())); + let ops = Arc::new(::len(self.clone())); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual)) + let sel = Arc::new(NoSelector); + Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) } + fn sum(&self) -> PyResult { - let ops = Arc::new(>::sum(self.clone())); + let ops = Arc::new(::sum(self.clone())); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual)) + let sel = Arc::new(NoSelector); + Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) } + fn avg(&self) -> PyResult { - let ops = Arc::new(>::avg(self.clone())); + let ops = Arc::new(::avg(self.clone())); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual)) + let sel = Arc::new(NoSelector); + Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) } + fn min(&self) -> PyResult { - let ops = Arc::new(>::min(self.clone())); + let ops = Arc::new(::min(self.clone())); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual)) + let sel = Arc::new(NoSelector); + Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) } + fn max(&self) -> PyResult { - let ops = Arc::new(>::max(self.clone())); + let ops = Arc::new(::max(self.clone())); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual)) + let sel = Arc::new(NoSelector); + Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) } } -impl DynElemQualifierOps for T +trait QualifierBehavior: + InternalPropertyFilterOps + ElemQualifierOps + Clone + Send + Sync + 'static where - T: ElemQualifierOps<::Marker> - + InternalPropertyFilterOps - + Clone - + Send - + Sync - + 'static, - PropertyFilter<::Marker>: CreateFilter + TryAsCompositeFilter, + PropertyFilter: CreateFilter + TryAsCompositeFilter, { - fn any(&self) -> PyResult { - let ops = Arc::new(>::any(self.clone())); + fn build_any(&self) -> PyPropertyFilterOps; + + fn build_all(&self) -> PyPropertyFilterOps; +} + +impl QualifierBehavior for PropertyFilterBuilder +where + M: Clone + Send + Sync + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, +{ + fn build_any(&self) -> PyPropertyFilterOps { + let ops = Arc::new(ElemQualifierOps::any(self.clone())); let agg = Arc::new(NoListAggOps); let qual = Arc::new(NoElemQualifiers); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual)) + let sel = Arc::new(NoSelector); + PyPropertyFilterOps::from_parts(ops, agg, qual, sel) } - fn all(&self) -> PyResult { - let ops = Arc::new(>::all(self.clone())); + + fn build_all(&self) -> PyPropertyFilterOps { + let ops = Arc::new(ElemQualifierOps::all(self.clone())); let agg = Arc::new(NoListAggOps); let qual = Arc::new(NoElemQualifiers); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual)) + let sel = Arc::new(NoSelector); + PyPropertyFilterOps::from_parts(ops, agg, qual, sel) } } -trait DynTemporalPropertyFilterBuilderOps: Send + Sync { - fn __eq__(&self, value: Prop) -> PyFilterExpr; - - fn __ne__(&self, value: Prop) -> PyFilterExpr; - - fn any(&self) -> PyPropertyFilterOps; - - fn latest(&self) -> PyPropertyFilterOps; - - fn first(&self) -> PyPropertyFilterOps; - - fn all(&self) -> PyPropertyFilterOps; -} - -impl DynTemporalPropertyFilterBuilderOps - for TemporalPropertyFilterBuilder +impl QualifierBehavior for MetadataFilterBuilder where + M: Clone + Send + Sync + 'static, PropertyFilter: CreateFilter + TryAsCompositeFilter, { - fn __eq__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.clone().eq(value))) - } - - fn __ne__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.clone().ne(value))) - } - - fn any(&self) -> PyPropertyFilterOps { - PyPropertyFilterOps::from_builder(self.clone().any()) - } - - fn latest(&self) -> PyPropertyFilterOps { - PyPropertyFilterOps::from_builder(self.clone().latest()) - } - - fn first(&self) -> PyPropertyFilterOps { - PyPropertyFilterOps::from_builder(self.clone().first()) + fn build_any(&self) -> PyPropertyFilterOps { + let ops = Arc::new(ElemQualifierOps::any(self.clone())); + let agg = Arc::new(NoListAggOps); + let qual = Arc::new(NoElemQualifiers); + let sel = Arc::new(NoSelector); + PyPropertyFilterOps::from_parts(ops, agg, qual, sel) } - - fn all(&self) -> PyPropertyFilterOps { - PyPropertyFilterOps::from_builder(self.clone().all()) + fn build_all(&self) -> PyPropertyFilterOps { + let ops = Arc::new(ElemQualifierOps::all(self.clone())); + let agg = Arc::new(NoListAggOps); + let qual = Arc::new(NoElemQualifiers); + let sel = Arc::new(NoSelector); + PyPropertyFilterOps::from_parts(ops, agg, qual, sel) } } -#[pyclass( - frozen, - name = "TemporalPropertyFilterBuilder", - module = "raphtory.filter" -)] -#[derive(Clone)] -pub struct PyTemporalPropertyFilterBuilder { - t: Arc, - agg: Arc, -} - -#[pymethods] -impl PyTemporalPropertyFilterBuilder { - pub fn any(&self) -> PyPropertyFilterOps { - self.t.any() - } - - pub fn latest(&self) -> PyPropertyFilterOps { - self.t.latest() - } - - pub fn first(&self) -> PyPropertyFilterOps { - self.t.first() - } - - pub fn all(&self) -> PyPropertyFilterOps { - self.t.all() - } - - pub fn len(&self) -> PyResult { - self.agg.len() - } - - pub fn sum(&self) -> PyResult { - self.agg.sum() - } - - pub fn avg(&self) -> PyResult { - self.agg.avg() +impl DynSelectorOps for OpChainBuilder +where + M: Send + Sync + Clone + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, +{ + fn first(&self) -> PyResult { + Ok(PyPropertyFilterOps::from_builder(self.clone().first())) } - pub fn min(&self) -> PyResult { - self.agg.min() + fn last(&self) -> PyResult { + Ok(PyPropertyFilterOps::from_builder(self.clone().last())) } +} - pub fn max(&self) -> PyResult { - self.agg.max() +impl QualifierBehavior for OpChainBuilder +where + M: Send + Sync + Clone + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, +{ + fn build_any(&self) -> PyPropertyFilterOps { + let chain = self.clone().any(); + let ops = Arc::new(chain.clone()); + let agg = Arc::new(chain.clone()); + let qual = Arc::new(chain.clone()); + let sel = Arc::new(NoSelector); + PyPropertyFilterOps::from_parts(ops, agg, qual, sel) + } + + fn build_all(&self) -> PyPropertyFilterOps { + let chain = self.clone().all(); + let ops = Arc::new(chain.clone()); + let agg = Arc::new(chain.clone()); + let qual = Arc::new(chain.clone()); + let sel = Arc::new(NoSelector); + PyPropertyFilterOps::from_parts(ops, agg, qual, sel) } +} - fn __eq__(&self, value: Prop) -> PyFilterExpr { - self.t.__eq__(value) +impl DynElemQualifierOps for T +where + T: QualifierBehavior, + PropertyFilter: CreateFilter + TryAsCompositeFilter, +{ + fn any(&self) -> PyResult { + Ok(self.build_any()) } - fn __ne__(&self, value: Prop) -> PyFilterExpr { - self.t.__ne__(value) + fn all(&self) -> PyResult { + Ok(self.build_all()) } } trait DynPropertyFilterBuilderOps: Send + Sync { - fn temporal(&self) -> PyTemporalPropertyFilterBuilder; + fn temporal(&self) -> PyPropertyFilterOps; } impl DynPropertyFilterBuilderOps for PropertyFilterBuilder where PropertyFilter: CreateFilter + TryAsCompositeFilter, { - fn temporal(&self) -> PyTemporalPropertyFilterBuilder { - let t = Arc::new(self.clone().temporal()) as Arc; - let agg = Arc::new(self.clone().temporal()) as Arc; - PyTemporalPropertyFilterBuilder { t, agg } + fn temporal(&self) -> PyPropertyFilterOps { + PyPropertyFilterOps::from_builder(self.clone().temporal()) } } @@ -538,6 +559,7 @@ where ops: inner.clone(), agg: inner.clone(), qual: inner.clone(), + sel: Arc::new(NoSelector), }; let child = PyPropertyFilterBuilder(inner as Arc); Bound::new(py, (child, parent)) @@ -546,7 +568,7 @@ where #[pymethods] impl PyPropertyFilterBuilder { - fn temporal(&self) -> PyTemporalPropertyFilterBuilder { + fn temporal(&self) -> PyPropertyFilterOps { self.0.temporal() } } @@ -573,6 +595,7 @@ where ops: Arc::new(self.clone()), agg: Arc::new(self.clone()), qual: Arc::new(self), + sel: Arc::new(NoSelector), }; let child = PyMetadataFilterBuilder; Bound::new(py, (child, parent)) diff --git a/raphtory/src/search/edge_filter_executor.rs b/raphtory/src/search/edge_filter_executor.rs index b07c7c5bfc..12a53216e5 100644 --- a/raphtory/src/search/edge_filter_executor.rs +++ b/raphtory/src/search/edge_filter_executor.rs @@ -190,7 +190,7 @@ impl<'a> EdgeFilterExecutor<'a> { limit: usize, offset: usize, ) -> Result>, GraphError> { - if filter.list_agg.is_some() || filter.list_elem_qualifier.is_some() { + if filter.ops.is_empty() { return fallback_filter_edges(graph, filter, limit, offset); } @@ -198,7 +198,7 @@ impl<'a> EdgeFilterExecutor<'a> { PropertyRef::Metadata(prop_name) => { self.apply_metadata_filter(graph, prop_name, filter, limit, offset) } - PropertyRef::TemporalProperty(prop_name, _) | PropertyRef::Property(prop_name) => self + PropertyRef::TemporalProperty(prop_name) | PropertyRef::Property(prop_name) => self .apply_temporal_property_filter( graph, prop_name, diff --git a/raphtory/src/search/exploded_edge_filter_executor.rs b/raphtory/src/search/exploded_edge_filter_executor.rs index e4589bd265..fb4664a271 100644 --- a/raphtory/src/search/exploded_edge_filter_executor.rs +++ b/raphtory/src/search/exploded_edge_filter_executor.rs @@ -191,7 +191,7 @@ impl<'a> ExplodedEdgeFilterExecutor<'a> { limit: usize, offset: usize, ) -> Result>, GraphError> { - if filter.list_agg.is_some() || filter.list_elem_qualifier.is_some() { + if filter.ops.is_empty() { return fallback_filter_exploded_edges(graph, filter, limit, offset); } @@ -199,7 +199,7 @@ impl<'a> ExplodedEdgeFilterExecutor<'a> { PropertyRef::Metadata(prop_name) => { self.apply_metadata_filter(graph, prop_name, filter, limit, offset) } - PropertyRef::TemporalProperty(prop_name, _) | PropertyRef::Property(prop_name) => self + PropertyRef::TemporalProperty(prop_name) | PropertyRef::Property(prop_name) => self .apply_temporal_property_filter( graph, prop_name, diff --git a/raphtory/src/search/mod.rs b/raphtory/src/search/mod.rs index 7467aca878..47c083b595 100644 --- a/raphtory/src/search/mod.rs +++ b/raphtory/src/search/mod.rs @@ -1019,7 +1019,7 @@ mod test_index { .build(); create_index_fn(&graph, index_spec.clone()).unwrap(); - let filter = NodeFilter::property("p2").temporal().latest().eq(50u64); + let filter = NodeFilter::property("p2").temporal().last().eq(50u64); assert_eq!(search_nodes(&graph, filter.clone()), vec!["pometry"]); let node = graph @@ -1027,7 +1027,7 @@ mod test_index { .unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = NodeFilter::property("p1").temporal().latest().eq(100u64); + let filter = NodeFilter::property("p1").temporal().last().eq(100u64); assert_eq!(search_nodes(&graph, filter.clone()), vec!["shivam"]); node.add_metadata([("z", true)]).unwrap(); @@ -1059,7 +1059,7 @@ mod test_index { .add_edge(1, "shivam", "kapoor", [("p1", 100u64)], None) .unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = EdgeFilter::property("p1").temporal().latest().eq(100u64); + let filter = EdgeFilter::property("p1").temporal().last().eq(100u64); assert_eq!(search_edges(&graph, filter.clone()), vec!["shivam->kapoor"]); edge.add_metadata([("z", true)], None).unwrap(); diff --git a/raphtory/src/search/node_filter_executor.rs b/raphtory/src/search/node_filter_executor.rs index da8744549e..53a45022c4 100644 --- a/raphtory/src/search/node_filter_executor.rs +++ b/raphtory/src/search/node_filter_executor.rs @@ -189,7 +189,7 @@ impl<'a> NodeFilterExecutor<'a> { limit: usize, offset: usize, ) -> Result>, GraphError> { - if filter.list_agg.is_some() || filter.list_elem_qualifier.is_some() { + if !filter.ops.is_empty() { return fallback_filter_nodes(graph, filter, limit, offset); } @@ -197,7 +197,7 @@ impl<'a> NodeFilterExecutor<'a> { PropertyRef::Metadata(prop_name) => { self.apply_metadata_filter(graph, prop_name, filter, limit, offset) } - PropertyRef::TemporalProperty(prop_name, _) | PropertyRef::Property(prop_name) => self + PropertyRef::TemporalProperty(prop_name) | PropertyRef::Property(prop_name) => self .apply_temporal_property_filter( graph, prop_name, From 61145c9af444e4505ce51d8d215face4058933be Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Mon, 6 Oct 2025 14:55:53 +0100 Subject: [PATCH 2/8] takes a reference --- python/python/raphtory/__init__.pyi | 56 +++++++++++++++++++ .../python/raphtory/algorithms/__init__.pyi | 3 + raphtory-graphql/schema.graphql | 2 +- .../views/filter/model/property_filter.rs | 14 ++--- .../python/filter/property_filter_builders.rs | 18 +++--- 5 files changed, 76 insertions(+), 17 deletions(-) diff --git a/python/python/raphtory/__init__.pyi b/python/python/raphtory/__init__.pyi index e7d3659c82..0a17666de6 100644 --- a/python/python/raphtory/__init__.pyi +++ b/python/python/raphtory/__init__.pyi @@ -49,6 +49,7 @@ __all__ = [ "IndexSpec", "Prop", "version", + "DiskGraphStorage", "graphql", "algorithms", "graph_loader", @@ -1375,6 +1376,17 @@ class Graph(GraphView): MutableNode: The node object with the specified id, or None if the node does not exist """ + def persist_as_disk_graph(self, graph_dir: str | PathLike) -> DiskGraphStorage: + """ + save graph in disk_graph format and memory map the result + + Arguments: + graph_dir (str | PathLike): folder where the graph will be saved + + Returns: + DiskGraphStorage: the persisted graph storage + """ + def persistent_graph(self) -> PersistentGraph: """ View graph with persistent semantics @@ -1412,6 +1424,17 @@ class Graph(GraphView): bytes: """ + def to_disk_graph(self, graph_dir: str | PathLike) -> Graph: + """ + Persist graph on disk + + Arguments: + graph_dir (str | PathLike): the folder where the graph will be persisted + + Returns: + Graph: a view of the persisted graph + """ + def to_parquet(self, graph_dir: str | PathLike): """ Persist graph to parquet files @@ -2186,6 +2209,7 @@ class PersistentGraph(GraphView): bytes: """ + def to_disk_graph(self, graph_dir): ... def update_metadata(self, metadata: dict) -> None: """ Updates metadata of the graph. @@ -6118,3 +6142,35 @@ class Prop(object): def u8(value): ... def version(): ... + +class DiskGraphStorage(object): + def __repr__(self): + """Return repr(self).""" + + def append_node_temporal_properties(self, location, chunk_size=20000000): ... + def graph_dir(self): ... + @staticmethod + def load_from_dir(graph_dir): ... + @staticmethod + def load_from_pandas(graph_dir, edge_df, time_col, src_col, dst_col): ... + @staticmethod + def load_from_parquets( + graph_dir, + layer_parquet_cols, + node_properties=None, + chunk_size=10000000, + t_props_chunk_size=10000000, + num_threads=4, + node_type_col=None, + node_id_col=None, + ): ... + def load_node_metadata(self, location, col_names=None, chunk_size=None): ... + def load_node_types(self, location, col_name, chunk_size=None): ... + def merge_by_sorted_gids(self, other, graph_dir): + """ + Merge this graph with another `DiskGraph`. Note that both graphs should have nodes that are + sorted by their global ids or the resulting graph will be nonsense! + """ + + def to_events(self): ... + def to_persistent(self): ... diff --git a/python/python/raphtory/algorithms/__init__.pyi b/python/python/raphtory/algorithms/__init__.pyi index 3873e9b001..02ea5eba3e 100644 --- a/python/python/raphtory/algorithms/__init__.pyi +++ b/python/python/raphtory/algorithms/__init__.pyi @@ -70,6 +70,7 @@ __all__ = [ "max_weight_matching", "Matching", "Infected", + "connected_components", ] def dijkstra_single_source_shortest_paths( @@ -893,3 +894,5 @@ class Infected(object): Returns: int: """ + +def connected_components(graph): ... diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index f95736a85f..d02a95e9f6 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -876,8 +876,8 @@ type Graph { } type GraphAlgorithmPlugin { - pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! shortest_path(source: String!, targets: [String!]!, direction: String): [ShortestPathOutput!]! + pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! } type GraphSchema { diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index b142c36847..01ae2ad710 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -1519,7 +1519,7 @@ impl InternalPropertyFilterOps for OpChainBuil } pub trait ElemQualifierOps: InternalPropertyFilterOps { - fn any(self) -> OpChainBuilder + fn any(&self) -> OpChainBuilder where Self: Sized, { @@ -1530,7 +1530,7 @@ pub trait ElemQualifierOps: InternalPropertyFilterOps { } } - fn all(self) -> OpChainBuilder + fn all(&self) -> OpChainBuilder where Self: Sized, { @@ -1554,7 +1554,7 @@ impl PropertyFilterBuilder { } pub trait ListAggOps: InternalPropertyFilterOps + Sized { - fn len(self) -> OpChainBuilder { + fn len(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Len]).collect(), @@ -1562,7 +1562,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { } } - fn sum(self) -> OpChainBuilder { + fn sum(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Sum]).collect(), @@ -1570,7 +1570,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { } } - fn avg(self) -> OpChainBuilder { + fn avg(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Avg]).collect(), @@ -1578,7 +1578,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { } } - fn min(self) -> OpChainBuilder { + fn min(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Min]).collect(), @@ -1586,7 +1586,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { } } - fn max(self) -> OpChainBuilder { + fn max(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Max]).collect(), diff --git a/raphtory/src/python/filter/property_filter_builders.rs b/raphtory/src/python/filter/property_filter_builders.rs index 7405f02820..9c731c3f11 100644 --- a/raphtory/src/python/filter/property_filter_builders.rs +++ b/raphtory/src/python/filter/property_filter_builders.rs @@ -378,7 +378,7 @@ where PropertyFilter<::Marker>: CreateFilter + TryAsCompositeFilter, { fn len(&self) -> PyResult { - let ops = Arc::new(::len(self.clone())); + let ops = Arc::new(::len(&self)); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); @@ -386,7 +386,7 @@ where } fn sum(&self) -> PyResult { - let ops = Arc::new(::sum(self.clone())); + let ops = Arc::new(::sum(&self)); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); @@ -394,7 +394,7 @@ where } fn avg(&self) -> PyResult { - let ops = Arc::new(::avg(self.clone())); + let ops = Arc::new(::avg(&self)); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); @@ -402,7 +402,7 @@ where } fn min(&self) -> PyResult { - let ops = Arc::new(::min(self.clone())); + let ops = Arc::new(::min(&self)); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); @@ -410,7 +410,7 @@ where } fn max(&self) -> PyResult { - let ops = Arc::new(::max(self.clone())); + let ops = Arc::new(::max(&self)); let agg = Arc::new(self.clone()); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); @@ -434,7 +434,7 @@ where PropertyFilter: CreateFilter + TryAsCompositeFilter, { fn build_any(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::any(self.clone())); + let ops = Arc::new(ElemQualifierOps::any(self)); let agg = Arc::new(NoListAggOps); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); @@ -442,7 +442,7 @@ where } fn build_all(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::all(self.clone())); + let ops = Arc::new(ElemQualifierOps::all(self)); let agg = Arc::new(NoListAggOps); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); @@ -456,14 +456,14 @@ where PropertyFilter: CreateFilter + TryAsCompositeFilter, { fn build_any(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::any(self.clone())); + let ops = Arc::new(ElemQualifierOps::any(self)); let agg = Arc::new(NoListAggOps); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); PyPropertyFilterOps::from_parts(ops, agg, qual, sel) } fn build_all(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::all(self.clone())); + let ops = Arc::new(ElemQualifierOps::all(self)); let agg = Arc::new(NoListAggOps); let qual = Arc::new(NoElemQualifiers); let sel = Arc::new(NoSelector); From 261385b0128f36f5968fa7aa58ed9dd3559f22b7 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:42:04 +0100 Subject: [PATCH 3/8] rework validation --- .../views/filter/model/property_filter.rs | 1007 ++++++++++------- 1 file changed, 572 insertions(+), 435 deletions(-) diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index 01ae2ad710..dc94510613 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -49,7 +49,7 @@ pub enum Op { Avg, Min, Max, - // Qualifiers + // Qualifiers (quantifiers) Any, All, } @@ -59,18 +59,100 @@ impl Op { pub fn is_selector(self) -> bool { matches!(self, Op::First | Op::Last) } - #[inline] pub fn is_aggregator(self) -> bool { matches!(self, Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max) } - #[inline] pub fn is_qualifier(self) -> bool { matches!(self, Op::Any | Op::All) } } +#[derive(Clone, Debug, PartialEq, Eq)] +enum Shape { + Scalar(PropType), + List(Box), + Seq(Box), + /// A pending quantifier (any/all) over elements of the inner shape. + /// We keep the element dtype available so the final operator can be + /// validated against element type (predicate-after-quantifier). + Quantified(Box, Op), +} + +impl Shape { + #[inline] + fn elem_dtype(&self) -> Option { + match self { + Shape::Scalar(t) => Some(t.clone()), + Shape::List(t) => Some(*t.clone()), + Shape::Seq(inner) => inner.elem_dtype(), + Shape::Quantified(inner, _) => inner.elem_dtype(), + } + } +} + +/// Count consecutive Quantified wrappers and return (base shape, depth). +fn flatten_quantified_depth<'a>(mut s: &'a Shape) -> (&'a Shape, usize) { + let mut depth = 0; + while let Shape::Quantified(inner, _) = s { + depth += 1; + s = inner; + } + (s, depth) +} + +/// Unwrap `n` list layers from a PropType::List nesting. +fn unwrap_list_n(mut t: PropType, mut n: usize) -> Option { + while n > 0 { + if let PropType::List(inner) = t { + t = *inner; + n -= 1; + } else { + return None; + } + } + Some(t) +} + +#[inline] +fn agg_result_dtype(inner: &PropType, op: Op, ctx: &str) -> Result { + use PropType::*; + Ok(match op { + Op::Len => U64, + Op::Sum => match inner { + U8 | U16 | U32 | U64 => U64, + I32 | I64 => I64, + F32 | F64 => F64, + _ => { + return Err(GraphError::InvalidFilter(format!( + "sum() {} requires numeric", + ctx + ))) + } + }, + Op::Avg => match inner { + U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 => F64, + _ => { + return Err(GraphError::InvalidFilter(format!( + "avg() {} requires numeric", + ctx + ))) + } + }, + Op::Min | Op::Max => match inner { + U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 => inner.clone(), + _ => { + return Err(GraphError::InvalidFilter(format!( + "{:?} {} requires numeric", + op, ctx + ))) + } + }, + _ => unreachable!(), + }) +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum PropertyRef { Property(String), @@ -110,7 +192,7 @@ pub struct PropertyFilter { pub prop_ref: PropertyRef, pub prop_value: PropertyFilterValue, pub operator: FilterOperator, - pub ops: Vec, // at most 2 (validated) + pub ops: Vec, // validated by validate_chain_and_infer_effective_dtype pub _phantom: PhantomData, } @@ -356,6 +438,23 @@ impl PropertyFilter { dtype: &PropType, expect_map: bool, ) -> Result<(), GraphError> { + if self.ops.iter().copied().any(Op::is_aggregator) { + match self.operator { + FilterOperator::StartsWith + | FilterOperator::EndsWith + | FilterOperator::Contains + | FilterOperator::NotContains + | FilterOperator::IsNone + | FilterOperator::IsSome => { + return Err(GraphError::InvalidFilter(format!( + "Operator {} is not supported with list aggregation", + self.operator + ))); + } + _ => {} + } + } + match self.operator { FilterOperator::Eq | FilterOperator::Ne => { self.validate_single_dtype(dtype, expect_map)?; @@ -394,301 +493,222 @@ impl PropertyFilter { } }, } + Ok(()) } - /// Enforce the OK/NOK matrix and compute the **effective dtype** - /// after applying the op chain (used to validate the final operator). - /// - /// Op Pair Validity Matrix (first op × second op) - /// Selector Aggregator Qualifier - /// Selector NOK OK OK - /// Aggregator NOK NOK NOK - /// Qualifier NOK OK OK - /// - /// Single-op cases (standalone): - /// Selector → OK - /// Aggregator → OK - /// Qualifier → OK + /// Validate the op chain via a **right-to-left** fold over a Shape model and + /// return the effective dtype against which the final operator is checked. + /// Selectors (`first/last`) are applied *before* any other ops regardless of + /// source order to ensure chains like `.temporal().first().avg()` work as intended. fn validate_chain_and_infer_effective_dtype( &self, src_dtype: &PropType, is_temporal: bool, ) -> Result { - fn agg_result_dtype( - inner: &PropType, - op: Op, - ctx: &'static str, - ) -> Result { - use PropType::*; - Ok(match op { - Op::Len => U64, - - Op::Sum => match inner { - U8 | U16 | U32 | U64 => U64, - I32 | I64 => I64, - F32 | F64 => F64, - _ => { - return Err(GraphError::InvalidFilter(format!( - "sum() {} requires numeric", - ctx - ))) - } - }, + use Shape::*; - Op::Avg => match inner { - U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 => F64, - _ => { + // Build the base shape from the underlying property type. + let base_shape: Shape = if is_temporal { + let inner: Shape = match src_dtype { + PropType::List(inner) => List(inner.clone()), + t => Scalar(t.clone()), + }; + Seq(Box::new(inner)) + } else { + match src_dtype { + PropType::List(inner) => List(inner.clone()), + t => Scalar(t.clone()), + } + }; + + #[inline] + fn agg_out(inner: &PropType, op: Op, ctx: &str) -> Result { + agg_result_dtype(inner, op, ctx) + } + + // ---- IMPORTANT: selector precedence -------------------------------------- + // To preserve RTL validation but apply selectors first, we reorder ops so that + // selectors come *last* in the vector (closest to the source), which means they + // are processed *first* when we iterate in reverse. + let mut selectors: Vec = Vec::new(); + let mut others: Vec = Vec::new(); + for &op in &self.ops { + if op.is_selector() { + selectors.push(op); + } else { + others.push(op); + } + } + let mut ops_for_validation: Vec = Vec::with_capacity(self.ops.len()); + ops_for_validation.extend(others); + ops_for_validation.extend(selectors); + // --------------------------------------------------------------------------- + + // Right-to-left fold over ops_for_validation. + let mut shape = base_shape; + for &op in ops_for_validation.iter().rev() { + shape = match op { + // --- Selectors collapse one temporal layer --- + Op::First | Op::Last => match shape { + Seq(inner) => *inner, + other => { return Err(GraphError::InvalidFilter(format!( - "avg() {} requires numeric", - ctx + "{:?} requires temporal sequence (Seq), got {:?}", + op, other ))) } }, - Op::Min | Op::Max => match inner { - U8 | U16 | U32 | U64 | I32 | I64 | F32 | F64 => inner.clone(), + // --- Quantifiers wrap current list level (or per-time list). + // Allow singleton policy for scalars. + Op::Any | Op::All => match shape { + List(_) => Quantified(Box::new(shape), op), + + Seq(inner) => match *inner { + List(_) | Quantified(_, _) => { + Seq(Box::new(Quantified(Box::new(*inner), op))) + } + Scalar(t) => Seq(Box::new(Quantified(Box::new(Scalar(t)), op))), + other => { + return Err(GraphError::InvalidFilter(format!( + "{:?} requires list elements over time; got Seq({:?})", + op, other + ))) + } + }, + + Scalar(t) => Quantified(Box::new(Scalar(t)), op), + _ => { return Err(GraphError::InvalidFilter(format!( - "{:?} {} requires numeric", - op, ctx + "{:?} requires a list (or temporal list); got {:?}", + op, shape ))) } }, - _ => unreachable!(), - }) - } - - if self.ops.len() > 2 { - return Err(GraphError::InvalidFilter( - "At most two list/temporal operations are allowed.".into(), - )); - } - - let used_qualifier = self.ops.iter().any(|o| o.is_qualifier()); - if used_qualifier && !is_temporal { - if matches!( - self.operator, - FilterOperator::IsSome | FilterOperator::IsNone - ) { - return Err(GraphError::InvalidFilter( - "Operator IS_SOME/IS_NONE is not supported with element qualifiers; apply it to the list itself (without elem qualifiers).".into() - )); - } - } - - let used_agg = self.ops.iter().any(|o| o.is_aggregator()); - let disallowed_with_agg = matches!( - self.operator, - FilterOperator::StartsWith - | FilterOperator::EndsWith - | FilterOperator::Contains - | FilterOperator::NotContains - | FilterOperator::FuzzySearch { .. } - | FilterOperator::IsNone - | FilterOperator::IsSome - ); - if used_agg && disallowed_with_agg { - return Err(GraphError::InvalidFilter(format!( - "Operator {} is not supported with list aggregation", - self.operator - ))); - } - - let require_iterable = - |op: Op, shape_is_seq: bool, shape_is_list: bool| -> Result<(), GraphError> { - if op.is_selector() { - if !shape_is_seq { - return Err(GraphError::InvalidFilter(format!( - "{:?} requires list or temporal source", - op - ))); + // --- Aggregators map list/temporal(list|scalar) to a scalar dtype --- + Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => match shape { + // Aggregate a plain list + List(inner) => { + let t = *inner; + let out = agg_out(&t, op, "over list")?; + Scalar(out) } - } else if op.is_aggregator() { - if !(shape_is_seq || shape_is_list) { - return Err(GraphError::InvalidFilter(format!( - "{:?} requires list or temporal source", - op - ))); + + // Aggregate over time + Seq(inner) => match *inner { + Scalar(t) => { + let out = agg_out(&t, op, "over time")?; + Scalar(out) + } + List(t) => { + let elem_t = *t; + let out = agg_out(&elem_t, op, "over time of lists")?; + Scalar(out) + } + // time series with quantifier on the payload: preserve the quantifier, + // map the inner list dtype through the aggregator. + Quantified(qinner, qop) => { + let mapped_inner = match *qinner { + List(t) => { + let out = + agg_out(&*t, op, "under temporal quantifier over list")?; + Scalar(out) + } + Scalar(t) => { + return Err(GraphError::InvalidFilter(format!( + "{:?} under temporal quantifier requires list elements; got Scalar({:?})", + op, t + ))); + } + Seq(_) | Quantified(_, _) => unreachable!(), + }; + Seq(Box::new(Quantified(Box::new(mapped_inner), qop))) + } + Seq(_) => unreachable!(), + }, + + // Aggregator under a non-temporal quantifier: preserve the quantifier. + Quantified(qinner, qop) => { + let mapped_inner = match *qinner { + List(t) => { + let out = agg_out(&*t, op, "under quantifier over list")?; + Scalar(out) + } + Scalar(t) => { + return Err(GraphError::InvalidFilter(format!( + "{:?} under quantifier requires list elements; got Scalar({:?})", + op, t + ))); + } + Seq(_) | Quantified(_, _) => unreachable!(), + }; + Quantified(Box::new(mapped_inner), qop) } - } else if op.is_qualifier() { - if !(shape_is_seq || shape_is_list) { + + // Aggregators require a collection or temporal sequence before them. + Scalar(t) => { return Err(GraphError::InvalidFilter(format!( - "{:?} requires list or temporal source", - op + "{:?} requires list or temporal sequence, got Scalar({:?})", + op, t ))); } - } - Ok(()) + }, }; + } - let (shape_is_list, elem_ty) = (matches!(src_dtype, PropType::List(_)), src_dtype.clone()); - - // Pair rules (OK/NOK) - let pair_ok = |a: Op, b: Op| -> bool { - // NOK - if a.is_selector() && b.is_selector() { - return false; - } // [Sel, Sel] - if a.is_aggregator() && (b.is_selector() || b.is_qualifier() || b.is_aggregator()) { - return false; // [Agg, Sel] / [Agg, Qual] / [Agg, Agg] - } - if a.is_qualifier() && b.is_selector() { - return false; - } // [Qual, Sel] - // OK - if a.is_selector() && (b.is_aggregator() || b.is_qualifier()) { - return true; - } // [Sel, Agg] / [Sel, Qual] - if a.is_qualifier() && (b.is_aggregator() || b.is_qualifier()) { - return true; - } // [Qual, Agg] / [Qual, Qual] - true - }; - - match (is_temporal, self.ops.as_slice()) { - // Catch-all: if 3 or more ops are present, reject - (false, &[_, _, _, ..]) | (true, &[_, _, _, ..]) => Err(GraphError::InvalidFilter( - "At most two list/temporal operations are allowed.".into(), - )), - - // No ops - (true, []) => { - // effective dtype: List (comparing to the full time-series) - Ok(PropType::List(Box::new(elem_ty.clone()))) - } - (false, []) => Ok(elem_ty), - - // Non-temporal: exactly one op (must be Agg or Qual) on List - (false, [op]) => { - if !op.is_aggregator() && !op.is_qualifier() { - return Err(GraphError::InvalidFilter(format!( - "Non-temporal properties support only aggregators/qualifiers; got {:?}", - op - ))); - } - if !shape_is_list { - return Err(GraphError::InvalidFilter(format!( - "{:?} requires list; property is {:?}", - op, elem_ty - ))); - } - let inner = elem_ty.inner().ok_or_else(|| { - GraphError::InvalidFilter(format!("Expected list type, got {:?}", elem_ty)) - })?; - if op.is_aggregator() { - agg_result_dtype(inner, *op, "requires numeric list") - } else { - Ok(inner.clone()) - } - } - - // Non-temporal: two ops -> not allowed - (false, [_a, _b]) => Err(GraphError::InvalidFilter( - "Non-temporal properties support at most one op.".into(), - )), - - // Temporal: one op - (true, [op]) => { - require_iterable(*op, true, shape_is_list)?; - if op.is_selector() { - // Selecting a single instant from the temporal sequence. - // If the temporal element type is T, `first/last` yields T. - // If the temporal element type is List, it yields List. - let eff = elem_ty.clone(); - return Ok(eff); - } - - let eff = if op.is_aggregator() { - if shape_is_list { - return Err(GraphError::InvalidFilter(format!( - "{:?} over temporal requires scalar elements; got List", - op - ))); - } - return agg_result_dtype(&elem_ty, *op, "over time"); - } else { - if let PropType::List(inner) = &elem_ty { - inner.as_ref().clone() - } else { - elem_ty.clone() + // Effective dtype seen by the final operator. + let effective_dtype: PropType = match shape { + Scalar(t) => t, + List(t) => PropType::List(t), + + // Non-temporal quantified input: + // Any depth of quantifiers over a list exposes the element dtype. + // Any depth over a scalar (singleton policy) exposes the scalar dtype. + Quantified(_, _) => { + let (base, _qdepth) = flatten_quantified_depth(&shape); + match base { + List(t) => (**t).clone(), + Scalar(t) => t.clone(), + _ => { + return Err(GraphError::InvalidFilter( + "Quantifier requires list or scalar input".into(), + )) } - }; - Ok(eff) + } } - // Temporal: two ops - (true, [a, b]) => { - if !pair_ok(*a, *b) { - return Err(GraphError::InvalidFilter(format!( - "Invalid op pair: {:?} then {:?}", - a, b - ))); - } - match (*a, *b) { - (sa, sb) if sa.is_selector() && sb.is_aggregator() => { - if !shape_is_list { - return Err(GraphError::InvalidFilter( - "Selector then aggregator requires Seq[List[T]] (temporal of lists)".into(), - )); - } - let inner = elem_ty.inner().ok_or_else(|| { - GraphError::InvalidFilter(format!( - "Expected list type, got {:?}", - elem_ty - )) - })?; - agg_result_dtype(inner, sb, "requires numeric") - } - (sa, sb) if sa.is_selector() && sb.is_qualifier() => { - if !shape_is_list { - return Err(GraphError::InvalidFilter( - "Selector then qualifier requires Seq[List[T]]".into(), - )); - } - let inner = elem_ty.inner().ok_or_else(|| { - GraphError::InvalidFilter(format!( - "Expected list type, got {:?}", - elem_ty - )) - })?; - Ok(inner.clone()) - } - (qa, qb) if qa.is_qualifier() && qb.is_aggregator() => { - if !shape_is_list { + // Temporal payload + Seq(inner) => match *inner { + // time series of scalars -> List effective dtype + Scalar(t) => PropType::List(Box::new(t)), + // time series of lists -> List> effective dtype + List(t) => PropType::List(Box::new(PropType::List(t))), + + // Quantified per-time payload: same expose-as-element/singleton semantics. + Quantified(_, _) => { + let (base, _qdepth) = flatten_quantified_depth(&*inner); + match base { + List(t) => (**t).clone(), + Scalar(t) => t.clone(), + _ => { return Err(GraphError::InvalidFilter( - "Qualifier then aggregator requires Seq[List[T]]".into(), - )); - } - let inner = elem_ty.inner().ok_or_else(|| { - GraphError::InvalidFilter(format!( - "Expected list type, got {:?}", - elem_ty + "Temporal quantifier requires list or scalar elements per time" + .into(), )) - })?; - agg_result_dtype(inner, qb, "requires numeric") - } - (qa, qb) if qa.is_qualifier() && qb.is_qualifier() => { - // Two qualifiers on a temporal property: operator applies to INNER ELEMENTS. - // We must validate the operator against the *inner element type*, not Bool. - if !shape_is_list { - return Err(GraphError::InvalidFilter( - "Two qualifiers on a temporal property require Seq[List[T]]".into(), - )); } - let inner = elem_ty.inner().ok_or_else(|| { - GraphError::InvalidFilter(format!( - "Expected list type, got {:?}", - elem_ty - )) - })?; - Ok(inner.clone()) } - _ => unreachable!(), } - } - } + Seq(_) => unreachable!(), + }, + }; + + // NOTE: Do **not** validate the final operator here. + // The caller (resolve_prop_id) will validate using the correct `expect_map_now` + // to support metadata map semantics. + Ok(effective_dtype) } pub fn resolve_prop_id(&self, meta: &Meta, expect_map: bool) -> Result { @@ -922,152 +942,270 @@ impl PropertyFilter { } } - fn apply_two_qualifiers_temporal(&self, prop: &[Prop], outer: Op, inner: Op) -> bool { - debug_assert!(outer.is_qualifier() && inner.is_qualifier()); - - let mut per_time: Vec = Vec::with_capacity(prop.len()); - for v in prop { - // Only lists participate. Non-lists => "no elements" at that time. - let elems: &[Prop] = match v { - Prop::List(inner_vals) => inner_vals.as_slice(), - _ => &[], // <-- do NOT coerce a scalar into a 1-element list - }; - - let inner_ok = match inner { - Op::Any => elems - .iter() - .any(|e| self.operator.apply_to_property(&self.prop_value, Some(e))), - Op::All => { - // All requires at least one element. - !elems.is_empty() - && elems - .iter() - .all(|e| self.operator.apply_to_property(&self.prop_value, Some(e))) + /// Recursive reducer for nested quantifiers: applies the final predicate + /// at the innermost level and bubbles results back using each quantifier. + fn reduce_qualifiers_rec( + &self, + quals: &[Op], + v: &Prop, + predicate: &dyn Fn(&Prop) -> bool, + ) -> bool { + if quals.is_empty() { + return predicate(v); + } + let (q, rest) = (quals[0], &quals[1..]); + + // If we have a list, descend normally. + if let Prop::List(inner) = v { + let elems = inner.as_slice(); + let check = |e: &Prop| { + if rest.is_empty() { + predicate(e) + } else { + self.reduce_qualifiers_rec(rest, e, predicate) } + }; + return match q { + Op::Any => elems.iter().any(check), + Op::All => !elems.is_empty() && elems.iter().all(check), _ => unreachable!(), }; - - per_time.push(inner_ok); } - match outer { - Op::Any => per_time.into_iter().any(|b| b), - Op::All => !per_time.is_empty() && per_time.into_iter().all(|b| b), - _ => unreachable!(), + // No list at this level: apply singleton semantics. + // Consuming ANY/ALL over a single value is equivalent to just + // continuing with the remaining quantifiers on the same value. + if rest.is_empty() { + // Last quantifier => apply to the single value. + return match q { + Op::Any | Op::All => predicate(v), + _ => unreachable!(), + }; } - } + // Still have deeper quantifiers: keep consuming them on the same value. + self.reduce_qualifiers_rec(rest, v, predicate) + } + + /// Apply a list-style aggregator to a single Prop, with singleton semantics for scalars. + fn apply_agg_to_prop(p: &Prop, op: Op) -> Option { + match (op, p) { + // ----- Lists ----- + (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), + (Op::Sum, Prop::List(inner)) + | (Op::Avg, Prop::List(inner)) + | (Op::Min, Prop::List(inner)) + | (Op::Max, Prop::List(inner)) => Self::aggregate_values(inner.as_slice(), op), + + // ----- Scalars (singleton semantics) ----- + (Op::Len, _) => Some(Prop::U64(1)), + + (Op::Sum, Prop::U8(x)) => Some(Prop::U8(*x)), + (Op::Sum, Prop::U16(x)) => Some(Prop::U16(*x)), + (Op::Sum, Prop::U32(x)) => Some(Prop::U32(*x)), + (Op::Sum, Prop::U64(x)) => Some(Prop::U64(*x)), + (Op::Sum, Prop::I32(x)) => Some(Prop::I32(*x)), + (Op::Sum, Prop::I64(x)) => Some(Prop::I64(*x)), + (Op::Sum, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) + } else { + None + } + } + (Op::Sum, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None + } + } - fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Option) { - let mut qualifier: Option = None; + (Op::Avg, Prop::U8(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::U16(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::U32(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::U64(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::I32(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::I64(x)) => Some(Prop::F64(*x as f64)), + (Op::Avg, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) + } else { + None + } + } + (Op::Avg, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None + } + } - let has_later_reduce = |ops: &[Op], i: usize| -> bool { - ops.iter() - .enumerate() - .skip(i + 1) - .any(|(_, op)| op.is_selector() || op.is_aggregator() || op.is_qualifier()) - }; + (Op::Min, Prop::U8(x)) => Some(Prop::U8(*x)), + (Op::Min, Prop::U16(x)) => Some(Prop::U16(*x)), + (Op::Min, Prop::U32(x)) => Some(Prop::U32(*x)), + (Op::Min, Prop::U64(x)) => Some(Prop::U64(*x)), + (Op::Min, Prop::I32(x)) => Some(Prop::I32(*x)), + (Op::Min, Prop::I64(x)) => Some(Prop::I64(*x)), + (Op::Min, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) + } else { + None + } + } + (Op::Min, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None + } + } - let flatten_one = |vals: Vec| -> Vec { - let mut out = Vec::new(); - for p in vals { - if let Prop::List(inner) = p { - out.extend(inner.as_slice().iter().cloned()); + (Op::Max, Prop::U8(x)) => Some(Prop::U8(*x)), + (Op::Max, Prop::U16(x)) => Some(Prop::U16(*x)), + (Op::Max, Prop::U32(x)) => Some(Prop::U32(*x)), + (Op::Max, Prop::U64(x)) => Some(Prop::U64(*x)), + (Op::Max, Prop::I32(x)) => Some(Prop::I32(*x)), + (Op::Max, Prop::I64(x)) => Some(Prop::I64(*x)), + (Op::Max, Prop::F32(x)) => { + if x.is_finite() { + Some(Prop::F32(*x)) } else { - out.push(p); + None + } + } + (Op::Max, Prop::F64(x)) => { + if x.is_finite() { + Some(Prop::F64(*x)) + } else { + None } } - out - }; - let per_elem_map = |vals: Vec, op: Op| -> Vec { + // Non-numeric scalars for numeric aggs => None + (Op::Sum, _) | (Op::Avg, _) | (Op::Min, _) | (Op::Max, _) => None, + + // Selectors/qualifiers are handled elsewhere. + _ => None, + } + } + + /// Evaluate ops left-to-right to produce either a reduced scalar, + /// or a sequence plus quantifiers to apply. We also track whether + /// the produced sequence is truly *temporal* (came from a temporal + /// property) or just a normalized wrapper for element-quantifiers. + fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Vec, bool) { + // Collect quantifiers in source order. + let mut qualifiers: Vec = Vec::new(); + // Whether current sequence represents *time*. + let mut seq_is_temporal = matches!(state, ValueType::Seq(..)); + // Track whether we've seen ANY/ALL before the first aggregator. + let mut seen_qual_before_agg = false; + + // Apply list-style aggregator to a single Prop (list or scalar). + let per_step_map = |vals: Vec, op: Op| -> Vec { vals.into_iter() - .filter_map(|p| match (op, p) { - (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), - (Op::Sum, Prop::List(inner)) => { - Self::aggregate_values(inner.as_slice(), Op::Sum) - } - (Op::Avg, Prop::List(inner)) => { - Self::aggregate_values(inner.as_slice(), Op::Avg) - } - (Op::Min, Prop::List(inner)) => { - Self::aggregate_values(inner.as_slice(), Op::Min) + .filter_map(|p| Self::apply_agg_to_prop(&p, op)) + .collect() + }; + + // Collapse the temporal sequence itself (aggregate OVER TIME). + let reduce_over_seq = |vs: Vec, op: Op| -> Option { + match op { + Op::Len => Some(Prop::U64(vs.len() as u64)), // number of time steps + Op::Sum | Op::Avg | Op::Min | Op::Max => { + if vs.is_empty() { + return None; } - (Op::Max, Prop::List(inner)) => { - Self::aggregate_values(inner.as_slice(), Op::Max) + // Only defined for sequences of scalars; if steps hold lists, return None. + if matches!(vs.first(), Some(Prop::List(_))) { + return None; } - _ => None, - }) - .collect() + Self::aggregate_values(&vs, op) + } + _ => None, + } }; - for (i, op) in self.ops.iter().enumerate() { + for op in &self.ops { match *op { Op::First => { state = match state { - ValueType::Seq(vs) => ValueType::Scalar(vs.first().cloned()), + ValueType::Seq(vs) => { + // Collapsing time: after this, no temporal sequence remains. + seq_is_temporal = false; + ValueType::Scalar(vs.first().cloned()) + } ValueType::Scalar(s) => ValueType::Scalar(s), }; } Op::Last => { state = match state { - ValueType::Seq(vs) => ValueType::Scalar(vs.last().cloned()), + ValueType::Seq(vs) => { + seq_is_temporal = false; + ValueType::Scalar(vs.last().cloned()) + } ValueType::Scalar(s) => ValueType::Scalar(s), }; } + Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { state = match state { + // If a quantifier already occurred, aggregate per time step (do NOT collapse time). + ValueType::Seq(vs) if seen_qual_before_agg => { + ValueType::Seq(per_step_map(vs, *op)) + } + // Otherwise, aggregate ACROSS time to one scalar. ValueType::Seq(vs) => { - if matches!(vs.first(), Some(Prop::List(_))) { - ValueType::Seq(per_elem_map(vs, *op)) - } else { - ValueType::Scalar(Self::aggregate_values(&vs, *op)) - } + seq_is_temporal = false; + ValueType::Scalar(reduce_over_seq(vs, *op)) } + // Non-temporal list -> reduce to scalar. ValueType::Scalar(Some(Prop::List(inner))) => { ValueType::Scalar(Self::aggregate_values(inner.as_slice(), *op)) } - ValueType::Scalar(Some(_)) => ValueType::Scalar(None), + // Non-temporal scalar -> singleton semantics. + ValueType::Scalar(Some(p)) => { + ValueType::Scalar(Self::apply_agg_to_prop(&p, *op)) + } ValueType::Scalar(None) => ValueType::Scalar(None), }; } + Op::Any | Op::All => { - qualifier = Some(*op); - let later = has_later_reduce(&self.ops, i); + qualifiers.push(*op); + seen_qual_before_agg = true; + + // If we *already* have a temporal sequence, keep it (temporal stays true). + // Otherwise, normalize the current (non-temporal) value into a 1-step sequence, + // but mark that sequence as NON-TEMPORAL so we don't treat the first quantifier + // as temporal in apply_eval. state = match state { ValueType::Seq(vs) => { - if later { - ValueType::Seq(vs) - } else { - ValueType::Seq(flatten_one(vs)) - } + // remains temporal + ValueType::Seq(vs) } ValueType::Scalar(Some(Prop::List(inner))) => { - if later { - ValueType::Seq(vec![Prop::List(inner)]) - } else { - ValueType::Seq(inner.as_slice().to_vec()) - } + seq_is_temporal = false; + ValueType::Seq(vec![Prop::List(inner)]) + } + ValueType::Scalar(Some(p)) => { + seq_is_temporal = false; + ValueType::Seq(vec![p]) + } + ValueType::Scalar(None) => { + seq_is_temporal = false; + ValueType::Seq(vec![]) } - ValueType::Scalar(Some(p)) => ValueType::Seq(vec![p]), - ValueType::Scalar(None) => ValueType::Seq(vec![]), }; } } } - if let Some(q) = qualifier { - let elems = match state { - ValueType::Seq(vs) => vs, - ValueType::Scalar(Some(Prop::List(inner))) => inner.as_slice().to_vec(), - ValueType::Scalar(Some(p)) => vec![p], - ValueType::Scalar(None) => vec![], - }; - return (None, Some(elems), Some(q)); - } - match state { - ValueType::Scalar(v) => (v, None, None), - ValueType::Seq(vs) => (None, Some(vs), None), + ValueType::Scalar(v) => (v, None, qualifiers, seq_is_temporal), + ValueType::Seq(vs) => (None, Some(vs), qualifiers, seq_is_temporal), } } @@ -1075,40 +1213,86 @@ impl PropertyFilter { &self, reduced: Option, maybe_seq: Option>, - qualifier: Option, + qualifiers: Vec, + seq_is_temporal: bool, ) -> bool { - // 1) Reduced scalar -> compare directly - // For example: - // 1. NodeFilter::property("temp").temporal().avg().ge(Prop::F64(10.0)) - // 2. NodeFilter::property("readings").temporal().first().len().eq(Prop::U64(3)) - // 3. NodeFilter::property("scores").avg().gt(Prop::F64(0.5)) + // 1) Reduced to a single scalar -> compare directly. if let Some(value) = reduced { return self .operator .apply_to_property(&self.prop_value, Some(&value)); } - // 2) Qualifier over a sequence (ANY/ALL) - // For example: - // 1. NodeFilter::property("tags").any().eq(Prop::Str("gold".into())) - // 2. NodeFilter::property("price").temporal().any().gt(Prop::F64(100.0)) - if let Some(q) = qualifier { - let vals = maybe_seq.unwrap_or_default(); - if vals.is_empty() { + // 2) Quantifiers present + if !qualifiers.is_empty() { + // If the sequence is truly temporal, treat the **first** qualifier as temporal; + // otherwise, treat *all* qualifiers as element-level. + let (temporal_q_opt, elem_quals): (Option, &[Op]) = if seq_is_temporal { + (Some(qualifiers[0]), &qualifiers[1..]) + } else { + (None, &qualifiers[..]) + }; + + let pred = |p: &Prop| self.operator.apply_to_property(&self.prop_value, Some(p)); + + if let Some(seq) = maybe_seq { + // If there's a temporal quantifier, combine across time with it. + if let Some(temporal_q) = temporal_q_opt { + let mut saw_step = false; + match temporal_q { + Op::All => { + for p in &seq { + saw_step = true; + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if !ok { + return false; + } + } + return saw_step; + } + Op::Any => { + for p in &seq { + saw_step = true; + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if ok { + return true; + } + } + return false; + } + _ => unreachable!(), + } + } else { + // No temporal combinator: just apply *element* quantifiers to the single value + // each seq item represents (this seq is non-temporal normalization). + // Since it's a non-temporal normalization, there is exactly one item (by construction), + // but handling multiple is harmless. + for p in &seq { + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if ok { + return true; + } + } + return false; + } + } else { return false; } - let chk = |v: &Prop| self.operator.apply_to_property(&self.prop_value, Some(v)); - return match q { - Op::Any => vals.iter().any(chk), - Op::All => vals.iter().all(chk), - _ => unreachable!(), - }; } - // 3) Compare whole sequence as a List, or missing value - // For example: - // 1. NodeFilter::property("temperature").temporal().eq(Prop::List(vec![...])) - // 2. NodeFilter::property("tags").eq(Prop::List(vec!["gold", "silver"])) + // 3) No quantifiers and not reduced: compare the whole sequence as a list. if let Some(seq) = maybe_seq { let full = Prop::List(Arc::new(seq)); self.operator @@ -1119,20 +1303,15 @@ impl PropertyFilter { } fn eval_scalar_and_apply(&self, prop: Option) -> bool { - let (reduced, maybe_seq, qualifier) = self.eval_ops(ValueType::Scalar(prop)); - self.apply_eval(reduced, maybe_seq, qualifier) + let (reduced, maybe_seq, qualifiers, seq_is_temporal) = + self.eval_ops(ValueType::Scalar(prop)); + self.apply_eval(reduced, maybe_seq, qualifiers, seq_is_temporal) } fn eval_temporal_and_apply(&self, props: Vec) -> bool { - // Special-case: two qualifiers on temporal -> directly compute final bool. - // For example: - // 1. NodeFilter::property("p_flags").temporal().all().all().eq(Prop::u64(1)) - // 2. NodeFilter::property("p_flags").temporal().any().all().eq(Prop::bool(true)) - if self.ops.len() == 2 && self.ops[0].is_qualifier() && self.ops[1].is_qualifier() { - return self.apply_two_qualifiers_temporal(&props, self.ops[0], self.ops[1]); - } - let (reduced, maybe_seq, qualifier) = self.eval_ops(ValueType::Seq(props)); - self.apply_eval(reduced, maybe_seq, qualifier) + let (reduced, maybe_seq, qualifiers, seq_is_temporal) = + self.eval_ops(ValueType::Seq(props)); + self.apply_eval(reduced, maybe_seq, qualifiers, seq_is_temporal) } pub fn matches(&self, other: Option<&Prop>) -> bool { @@ -1307,33 +1486,19 @@ impl InternalPropertyFilterOps for Arc { pub trait PropertyFilterOps: InternalPropertyFilterOps { fn eq(&self, value: impl Into) -> PropertyFilter; - fn ne(&self, value: impl Into) -> PropertyFilter; - fn le(&self, value: impl Into) -> PropertyFilter; - fn ge(&self, value: impl Into) -> PropertyFilter; - fn lt(&self, value: impl Into) -> PropertyFilter; - fn gt(&self, value: impl Into) -> PropertyFilter; - fn is_in(&self, values: impl IntoIterator) -> PropertyFilter; - fn is_not_in(&self, values: impl IntoIterator) -> PropertyFilter; - fn is_none(&self) -> PropertyFilter; - fn is_some(&self) -> PropertyFilter; - fn starts_with(&self, value: impl Into) -> PropertyFilter; - fn ends_with(&self, value: impl Into) -> PropertyFilter; - fn contains(&self, value: impl Into) -> PropertyFilter; - fn not_contains(&self, value: impl Into) -> PropertyFilter; - fn fuzzy_search( &self, prop_value: impl Into, @@ -1346,63 +1511,49 @@ impl PropertyFilterOps for T { fn eq(&self, value: impl Into) -> PropertyFilter { PropertyFilter::eq(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } - fn ne(&self, value: impl Into) -> PropertyFilter { PropertyFilter::ne(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } - fn le(&self, value: impl Into) -> PropertyFilter { PropertyFilter::le(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } - fn ge(&self, value: impl Into) -> PropertyFilter { PropertyFilter::ge(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } - fn lt(&self, value: impl Into) -> PropertyFilter { PropertyFilter::lt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } - fn gt(&self, value: impl Into) -> PropertyFilter { PropertyFilter::gt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } - fn is_in(&self, values: impl IntoIterator) -> PropertyFilter { PropertyFilter::is_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) } - fn is_not_in(&self, values: impl IntoIterator) -> PropertyFilter { PropertyFilter::is_not_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) } - fn is_none(&self) -> PropertyFilter { PropertyFilter::is_none(self.property_ref()).with_ops(self.ops().iter().copied()) } - fn is_some(&self) -> PropertyFilter { PropertyFilter::is_some(self.property_ref()).with_ops(self.ops().iter().copied()) } - fn starts_with(&self, value: impl Into) -> PropertyFilter { PropertyFilter::starts_with(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } - fn ends_with(&self, value: impl Into) -> PropertyFilter { PropertyFilter::ends_with(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } - fn contains(&self, value: impl Into) -> PropertyFilter { PropertyFilter::contains(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } - fn not_contains(&self, value: impl Into) -> PropertyFilter { PropertyFilter::not_contains(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } - fn fuzzy_search( &self, prop_value: impl Into, @@ -1472,35 +1623,27 @@ impl OpChainBuilder { pub fn first(self) -> Self { self.with_op(Op::First) } - pub fn last(self) -> Self { self.with_op(Op::Last) } - pub fn any(self) -> Self { self.with_op(Op::Any) } - pub fn all(self) -> Self { self.with_op(Op::All) } - pub fn len(self) -> Self { self.with_op(Op::Len) } - pub fn sum(self) -> Self { self.with_op(Op::Sum) } - pub fn avg(self) -> Self { self.with_op(Op::Avg) } - pub fn min(self) -> Self { self.with_op(Op::Min) } - pub fn max(self) -> Self { self.with_op(Op::Max) } @@ -1508,11 +1651,9 @@ impl OpChainBuilder { impl InternalPropertyFilterOps for OpChainBuilder { type Marker = M; - fn property_ref(&self) -> PropertyRef { self.prop_ref.clone() } - fn ops(&self) -> &[Op] { &self.ops } @@ -1561,7 +1702,6 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } - fn sum(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), @@ -1569,7 +1709,6 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } - fn avg(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), @@ -1577,7 +1716,6 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } - fn min(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), @@ -1585,7 +1723,6 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } - fn max(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), From 4281d4ed9bd63fb132c6bcf11712c0e7e24a4fc3 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 8 Oct 2025 14:56:30 +0100 Subject: [PATCH 4/8] fix arbitrary list, fix tests --- python/python/raphtory/__init__.pyi | 56 --- .../python/raphtory/algorithms/__init__.pyi | 3 - .../test_filters/test_node_property_filter.py | 4 +- raphtory/src/db/graph/views/filter/mod.rs | 88 +++- .../views/filter/model/filter_operator.rs | 127 ++++-- .../views/filter/model/property_filter.rs | 424 ++++++++---------- 6 files changed, 361 insertions(+), 341 deletions(-) diff --git a/python/python/raphtory/__init__.pyi b/python/python/raphtory/__init__.pyi index 0a17666de6..e7d3659c82 100644 --- a/python/python/raphtory/__init__.pyi +++ b/python/python/raphtory/__init__.pyi @@ -49,7 +49,6 @@ __all__ = [ "IndexSpec", "Prop", "version", - "DiskGraphStorage", "graphql", "algorithms", "graph_loader", @@ -1376,17 +1375,6 @@ class Graph(GraphView): MutableNode: The node object with the specified id, or None if the node does not exist """ - def persist_as_disk_graph(self, graph_dir: str | PathLike) -> DiskGraphStorage: - """ - save graph in disk_graph format and memory map the result - - Arguments: - graph_dir (str | PathLike): folder where the graph will be saved - - Returns: - DiskGraphStorage: the persisted graph storage - """ - def persistent_graph(self) -> PersistentGraph: """ View graph with persistent semantics @@ -1424,17 +1412,6 @@ class Graph(GraphView): bytes: """ - def to_disk_graph(self, graph_dir: str | PathLike) -> Graph: - """ - Persist graph on disk - - Arguments: - graph_dir (str | PathLike): the folder where the graph will be persisted - - Returns: - Graph: a view of the persisted graph - """ - def to_parquet(self, graph_dir: str | PathLike): """ Persist graph to parquet files @@ -2209,7 +2186,6 @@ class PersistentGraph(GraphView): bytes: """ - def to_disk_graph(self, graph_dir): ... def update_metadata(self, metadata: dict) -> None: """ Updates metadata of the graph. @@ -6142,35 +6118,3 @@ class Prop(object): def u8(value): ... def version(): ... - -class DiskGraphStorage(object): - def __repr__(self): - """Return repr(self).""" - - def append_node_temporal_properties(self, location, chunk_size=20000000): ... - def graph_dir(self): ... - @staticmethod - def load_from_dir(graph_dir): ... - @staticmethod - def load_from_pandas(graph_dir, edge_df, time_col, src_col, dst_col): ... - @staticmethod - def load_from_parquets( - graph_dir, - layer_parquet_cols, - node_properties=None, - chunk_size=10000000, - t_props_chunk_size=10000000, - num_threads=4, - node_type_col=None, - node_id_col=None, - ): ... - def load_node_metadata(self, location, col_names=None, chunk_size=None): ... - def load_node_types(self, location, col_name, chunk_size=None): ... - def merge_by_sorted_gids(self, other, graph_dir): - """ - Merge this graph with another `DiskGraph`. Note that both graphs should have nodes that are - sorted by their global ids or the resulting graph will be nonsense! - """ - - def to_events(self): ... - def to_persistent(self): ... diff --git a/python/python/raphtory/algorithms/__init__.pyi b/python/python/raphtory/algorithms/__init__.pyi index 02ea5eba3e..3873e9b001 100644 --- a/python/python/raphtory/algorithms/__init__.pyi +++ b/python/python/raphtory/algorithms/__init__.pyi @@ -70,7 +70,6 @@ __all__ = [ "max_weight_matching", "Matching", "Infected", - "connected_components", ] def dijkstra_single_source_shortest_paths( @@ -894,5 +893,3 @@ class Infected(object): Returns: int: """ - -def connected_components(graph): ... 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 8164e4a309..0275ffc7af 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 @@ -174,7 +174,7 @@ def check(graph): filter_expr = filter.Node.property("p20").temporal().all().starts_with("Gold") result_ids = sorted(graph.filter(filter_expr).nodes.id) - expected_ids = ["1", "3", "4"] + expected_ids = ["3", "4"] assert result_ids == expected_ids return check @@ -210,7 +210,7 @@ def check(graph): filter_expr = filter.Node.property("p20").temporal().all().ends_with("ship") result_ids = sorted(graph.filter(filter_expr).nodes.id) - expected_ids = ["1", "2"] + expected_ids = ["2"] assert result_ids == expected_ids filter_expr = filter.Node.metadata("p10").ends_with("ane") diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 67df94252e..8c89360c91 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -1379,10 +1379,7 @@ pub(crate) mod test_filters { } } - use crate::db::graph::{ - assertions::GraphTransformer, - views::filter::{internal::CreateFilter, model::TryAsCompositeFilter}, - }; + use crate::db::graph::assertions::GraphTransformer; fn init_nodes_graph< G: StaticGraphViewOps @@ -2786,7 +2783,7 @@ pub(crate) mod test_filters { ); let filter = NodeFilter::property("p1").temporal().all().ne("Gold_ship"); - let expected_results = vec!["1", "2"]; + let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -2894,7 +2891,7 @@ pub(crate) mod test_filters { ); let filter = NodeFilter::property("p2").temporal().all().le(10u64); - let expected_results = vec!["2", "3"]; + let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -3077,7 +3074,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::property("p2") .temporal() - .all() + .any() .is_in(vec![Prop::U64(2)]); let expected_results = vec!["2"]; assert_filter_nodes_results( @@ -4275,7 +4272,10 @@ pub(crate) mod test_filters { }, prelude::{AdditionOps, GraphViewOps, PropertyAdditionOps}, }; - use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; + use raphtory_api::core::{ + entities::properties::prop::{IntoProp, Prop}, + storage::arc_str::ArcStr, + }; use raphtory_storage::mutation::{ addition_ops::InternalAdditionOps, property_addition_ops::InternalPropertyAdditionOps, }; @@ -4314,6 +4314,11 @@ pub(crate) mod test_filters { Prop::List(Arc::new(xs.iter().copied().map(Prop::Bool).collect())) } + #[inline] + fn list(v: Vec) -> Prop { + Prop::List(Arc::new(v)) + } + pub fn init_nodes_graph< G: StaticGraphViewOps + AdditionOps @@ -4342,6 +4347,19 @@ pub(crate) mod test_filters { ("p_i64s", list_i64(&[1, 2, 3])), // min: 1, max: 3, sum: 6, avg: 2.0, len: 3 ("p_f32s", list_f32(&[1.0, 2.0, 3.5])), // min: 1.0, max: 3.5, sum: 6.5, avg: 2.1666666666666665, len: 3 ("p_f64s", list_f64(&[50.0, 40.0])), // min: 40.0, max: 50.0, sum: 90.0, avg: 45.0, len: 2 + ( + "nested_list", + list(vec![ + list(vec![ + list(vec![ + list(vec![50.0.into_prop(), 40.0.into_prop()]), + list(vec![60.0.into_prop()]), + ]), + list(vec![list(vec![46.0.into_prop()])]), + ]), + list(vec![list(vec![list(vec![90.0.into_prop()])])]), + ]), + ), ], ), ( @@ -4393,6 +4411,19 @@ pub(crate) mod test_filters { ("p_i64s", list_i64(&[0, 3, -3])), // min: -3, max: 3, sum: 0, avg: 0.0, len: 3 ("p_f32s", list_f32(&[1.0, 2.5, 3.0])), // min: 1.0, max: 3.0, sum: 6.5, avg: 2.1666666666666665, len: 3 ("p_f64s", list_f64(&[30.0, 60.0])), // min: 30.0, max: 60.0, sum: 90.0, avg: 45.0, len: 2 + ( + "nested_list", + list(vec![ + list(vec![ + list(vec![ + list(vec![50.0.into_prop(), 40.0.into_prop()]), + list(vec![60.0.into_prop()]), + ]), + list(vec![list(vec![46.0.into_prop()])]), + ]), + list(vec![list(vec![list(vec![90.0.into_prop()])])]), + ]), + ), ], ), ( @@ -4409,6 +4440,19 @@ pub(crate) mod test_filters { ("p_i64s", list_i64(&[1, 2, -3])), // min: -3, max: 2, sum: 0, avg: 0.0, len: 3 ("p_f32s", list_f32(&[1.0, 2.0, 3.5])), // min: 1.0, max: 3.5, sum: 6.5, avg: 2.1666666666666665, len: 3 ("p_f64s", list_f64(&[50.0, 40.0])), // min: 40.0, max: 50.0, sum: 90.0, avg: 45.0, len: 2 + ( + "nested_list", + list(vec![ + list(vec![ + list(vec![ + list(vec![50.0.into_prop(), 40.0.into_prop()]), + list(vec![60.0.into_prop()]), + ]), + list(vec![list(vec![46.0.into_prop()])]), + ]), + list(vec![list(vec![list(vec![90.0.into_prop()])])]), + ]), + ), ], ), ( @@ -7469,6 +7513,34 @@ pub(crate) mod test_filters { apply_assertion(filter, &expected); } + #[test] + fn test_node_nested_list_property_all_all_all_any() { + let filter = NodeFilter::property("nested_list") + .all() + .all() + .all() + .any() + .gt(45.0); + + let expected = vec!["n1", "n3"]; + apply_assertion(filter, &expected); + } + + #[test] + fn test_node_nested_list_temporal_property_all_all_all_all_any() { + let filter = NodeFilter::property("nested_list") + .temporal() + .all() + .all() + .all() + .all() + .any() + .gt(45.0); + + let expected = vec!["n3"]; + apply_assertion(filter, &expected); + } + // ------ Temporal All: any ------ #[test] fn test_node_temporal_property_all_any() { diff --git a/raphtory/src/db/graph/views/filter/model/filter_operator.rs b/raphtory/src/db/graph/views/filter/model/filter_operator.rs index 504d3e7392..eb77a11999 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -113,55 +113,98 @@ impl FilterOperator { } pub fn apply_to_property(&self, left: &PropertyFilterValue, right: Option<&Prop>) -> bool { + use std::cmp::Ordering::*; + use FilterOperator::*; + use PropertyFilterValue::*; + + let cmp = |op: &FilterOperator, r: &Prop, l: &Prop| -> bool { + match op { + Eq => r == l, + Ne => r != l, + Lt => r.partial_cmp(l).map(|o| o == Less).unwrap_or(false), + Le => r.partial_cmp(l).map(|o| o != Greater).unwrap_or(false), + Gt => r.partial_cmp(l).map(|o| o == Greater).unwrap_or(false), + Ge => r.partial_cmp(l).map(|o| o != Less).unwrap_or(false), + _ => false, + } + }; + match left { - PropertyFilterValue::None => match self { - FilterOperator::IsSome => right.is_some(), - FilterOperator::IsNone => right.is_none(), - _ => unreachable!(), + None => match self { + IsSome => right.is_some(), + IsNone => right.is_none(), + _ => false, // Missing RHS never matches for other ops }, - PropertyFilterValue::Single(l) => match self { - FilterOperator::Eq - | FilterOperator::Ne - | FilterOperator::Lt - | FilterOperator::Le - | FilterOperator::Gt - | FilterOperator::Ge => right.is_some_and(|r| { - // println!("right: {:?}, left: {:?}", r, l); - self.operation()(r, l) - }), - FilterOperator::StartsWith => right.is_some_and(|r| match (l, r) { - (Prop::Str(l), Prop::Str(r)) => r.deref().starts_with(l.deref()), - _ => unreachable!(), - }), - FilterOperator::EndsWith => right.is_some_and(|r| match (l, r) { - (Prop::Str(l), Prop::Str(r)) => r.deref().ends_with(l.deref()), - _ => unreachable!(), - }), - FilterOperator::Contains => right.is_some_and(|r| match (l, r) { - (Prop::Str(l), Prop::Str(r)) => r.deref().contains(l.deref()), - _ => unreachable!(), - }), - FilterOperator::NotContains => right.is_some_and(|r| match (l, r) { - (Prop::Str(l), Prop::Str(r)) => !r.deref().contains(l.deref()), - _ => unreachable!(), - }), - FilterOperator::FuzzySearch { + + Single(lv) => match self { + Eq | Ne | Lt | Le | Gt | Ge => { + if let Some(r) = right { + cmp(self, r, lv) + } else { + false + } + } + + StartsWith => { + if let (Some(Prop::Str(rs)), Prop::Str(ls)) = (right, lv) { + rs.deref().starts_with(ls.deref()) + } else { + false + } + } + EndsWith => { + if let (Some(Prop::Str(rs)), Prop::Str(ls)) = (right, lv) { + rs.deref().ends_with(ls.deref()) + } else { + false + } + } + Contains => { + if let (Some(Prop::Str(rs)), Prop::Str(ls)) = (right, lv) { + rs.deref().contains(ls.deref()) + } else { + false + } + } + NotContains => { + if let (Some(Prop::Str(rs)), Prop::Str(ls)) = (right, lv) { + !rs.deref().contains(ls.deref()) + } else { + false + } + } + + FuzzySearch { levenshtein_distance, prefix_match, - } => right.is_some_and(|r| match (l, r) { - (Prop::Str(l), Prop::Str(r)) => { - let fuzzy_fn = self.fuzzy_search(*levenshtein_distance, *prefix_match); - fuzzy_fn(l, r) + } => { + if let (Some(Prop::Str(rs)), Prop::Str(ls)) = (right, lv) { + let f = self.fuzzy_search(*levenshtein_distance, *prefix_match); + f(ls, rs) + } else { + false } - _ => unreachable!(), - }), - _ => unreachable!(), + } + + In | NotIn | IsSome | IsNone => false, }, - PropertyFilterValue::Set(l) => match self { - FilterOperator::In | FilterOperator::NotIn => { - right.is_some_and(|r| self.collection_operation()(l, r)) + + Set(set) => match self { + In => { + if let Some(r) = right { + set.contains(r) + } else { + false + } } - _ => unreachable!(), + NotIn => { + if let Some(r) = right { + !set.contains(r) + } else { + false + } + } + _ => false, }, } } diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index dc94510613..277ca7cc15 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -2,7 +2,11 @@ use crate::{ db::{ api::{ properties::{internal::InternalPropertiesOps, Metadata, Properties}, - view::{internal::GraphView, node::NodeViewOps, EdgeViewOps}, + view::{ + internal::{GraphView, NodeTimeSemanticsOps}, + node::NodeViewOps, + EdgeViewOps, + }, }, graph::{ edge::EdgeView, @@ -19,7 +23,7 @@ use crate::{ }, }, errors::GraphError, - prelude::{GraphViewOps, PropertiesOps}, + prelude::{GraphViewOps, PropertiesOps, TimeOps}, }; use itertools::Itertools; use raphtory_api::core::{ @@ -74,26 +78,10 @@ enum Shape { Scalar(PropType), List(Box), Seq(Box), - /// A pending quantifier (any/all) over elements of the inner shape. - /// We keep the element dtype available so the final operator can be - /// validated against element type (predicate-after-quantifier). Quantified(Box, Op), } -impl Shape { - #[inline] - fn elem_dtype(&self) -> Option { - match self { - Shape::Scalar(t) => Some(t.clone()), - Shape::List(t) => Some(*t.clone()), - Shape::Seq(inner) => inner.elem_dtype(), - Shape::Quantified(inner, _) => inner.elem_dtype(), - } - } -} - -/// Count consecutive Quantified wrappers and return (base shape, depth). -fn flatten_quantified_depth<'a>(mut s: &'a Shape) -> (&'a Shape, usize) { +fn flatten_quantified_depth(mut s: &Shape) -> (&Shape, usize) { let mut depth = 0; while let Shape::Quantified(inner, _) = s { depth += 1; @@ -102,19 +90,6 @@ fn flatten_quantified_depth<'a>(mut s: &'a Shape) -> (&'a Shape, usize) { (s, depth) } -/// Unwrap `n` list layers from a PropType::List nesting. -fn unwrap_list_n(mut t: PropType, mut n: usize) -> Option { - while n > 0 { - if let PropType::List(inner) = t { - t = *inner; - n -= 1; - } else { - return None; - } - } - Some(t) -} - #[inline] fn agg_result_dtype(inner: &PropType, op: Op, ctx: &str) -> Result { use PropType::*; @@ -406,6 +381,11 @@ impl PropertyFilter { } } + #[inline] + fn has_aggregator(&self) -> bool { + self.ops.iter().copied().any(Op::is_aggregator) + } + fn validate_single_dtype( &self, expected: &PropType, @@ -433,12 +413,38 @@ impl PropertyFilter { Ok(filter_dtype) } + #[inline] + fn has_elem_qualifier(&self) -> bool { + self.ops.iter().any(|op| matches!(op, Op::Any | Op::All)) + } + + #[inline] + fn is_temporal_ref(&self) -> bool { + matches!(self.prop_ref, PropertyRef::TemporalProperty(_)) + } + + #[inline] + fn has_temporal_first_qualifier(&self) -> bool { + self.is_temporal_ref() && matches!(self.ops.first(), Some(Op::Any | Op::All)) + } + fn validate_operator_against_dtype( &self, dtype: &PropType, expect_map: bool, ) -> Result<(), GraphError> { - if self.ops.iter().copied().any(Op::is_aggregator) { + if matches!( + self.operator, + FilterOperator::IsSome | FilterOperator::IsNone + ) { + if self.has_elem_qualifier() && !self.has_temporal_first_qualifier() { + return Err(GraphError::InvalidFilter( + "Invalid filter: Operator IS_SOME/IS_NONE is not supported with element qualifiers; apply it to the list itself (without elem qualifiers).".into() + )); + } + } + + if self.has_aggregator() { match self.operator { FilterOperator::StartsWith | FilterOperator::EndsWith @@ -457,22 +463,22 @@ impl PropertyFilter { match self.operator { FilterOperator::Eq | FilterOperator::Ne => { - self.validate_single_dtype(dtype, expect_map)?; + let _ = self.validate_single_dtype(dtype, expect_map)?; } FilterOperator::Lt | FilterOperator::Le | FilterOperator::Gt | FilterOperator::Ge => { - let filter_dtype = self.validate_single_dtype(dtype, expect_map)?; - if !filter_dtype.has_cmp() { - return Err(GraphError::InvalidFilterCmp(filter_dtype)); + let fd = self.validate_single_dtype(dtype, expect_map)?; + if !fd.has_cmp() { + return Err(GraphError::InvalidFilterCmp(fd)); } } FilterOperator::In | FilterOperator::NotIn => match &self.prop_value { + PropertyFilterValue::Set(_) => {} PropertyFilterValue::None => { return Err(GraphError::InvalidFilterExpectSetGotNone(self.operator)) } PropertyFilterValue::Single(_) => { return Err(GraphError::InvalidFilterExpectSetGotSingle(self.operator)) } - PropertyFilterValue::Set(_) => {} }, FilterOperator::IsSome | FilterOperator::IsNone => {} FilterOperator::StartsWith @@ -480,27 +486,41 @@ impl PropertyFilter { | FilterOperator::Contains | FilterOperator::NotContains | FilterOperator::FuzzySearch { .. } => match &self.prop_value { + PropertyFilterValue::Single(v) + if matches!(dtype, PropType::Str) && matches!(v.dtype(), PropType::Str) => {} PropertyFilterValue::None => { return Err(GraphError::InvalidFilterExpectSingleGotNone(self.operator)) } - PropertyFilterValue::Single(v) => { - if !matches!(dtype, PropType::Str) || !matches!(v.dtype(), PropType::Str) { - return Err(GraphError::InvalidContains(self.operator)); - } - } PropertyFilterValue::Set(_) => { return Err(GraphError::InvalidFilterExpectSingleGotSet(self.operator)) } + _ => return Err(GraphError::InvalidContains(self.operator)), }, } Ok(()) } - /// Validate the op chain via a **right-to-left** fold over a Shape model and - /// return the effective dtype against which the final operator is checked. - /// Selectors (`first/last`) are applied *before* any other ops regardless of - /// source order to ensure chains like `.temporal().first().avg()` work as intended. + // helper used at the bottom; include if you don't already have it + #[inline] + fn peel_list_n(mut t: PropType, mut n: usize) -> Result { + while n > 0 { + match t { + PropType::List(inner) => { + t = *inner; + n -= 1; + } + other => { + return Err(GraphError::InvalidFilter(format!( + "Too many any/all quantifiers for list depth (stopped at {:?})", + other + ))) + } + } + } + Ok(t) + } + fn validate_chain_and_infer_effective_dtype( &self, src_dtype: &PropType, @@ -508,7 +528,6 @@ impl PropertyFilter { ) -> Result { use Shape::*; - // Build the base shape from the underlying property type. let base_shape: Shape = if is_temporal { let inner: Shape = match src_dtype { PropType::List(inner) => List(inner.clone()), @@ -522,15 +541,6 @@ impl PropertyFilter { } }; - #[inline] - fn agg_out(inner: &PropType, op: Op, ctx: &str) -> Result { - agg_result_dtype(inner, op, ctx) - } - - // ---- IMPORTANT: selector precedence -------------------------------------- - // To preserve RTL validation but apply selectors first, we reorder ops so that - // selectors come *last* in the vector (closest to the source), which means they - // are processed *first* when we iterate in reverse. let mut selectors: Vec = Vec::new(); let mut others: Vec = Vec::new(); for &op in &self.ops { @@ -543,13 +553,10 @@ impl PropertyFilter { let mut ops_for_validation: Vec = Vec::with_capacity(self.ops.len()); ops_for_validation.extend(others); ops_for_validation.extend(selectors); - // --------------------------------------------------------------------------- - // Right-to-left fold over ops_for_validation. let mut shape = base_shape; for &op in ops_for_validation.iter().rev() { shape = match op { - // --- Selectors collapse one temporal layer --- Op::First | Op::Last => match shape { Seq(inner) => *inner, other => { @@ -560,11 +567,14 @@ impl PropertyFilter { } }, - // --- Quantifiers wrap current list level (or per-time list). - // Allow singleton policy for scalars. Op::Any | Op::All => match shape { + // nesting quantifiers is allowed + Quantified(_, _) => Quantified(Box::new(shape), op), + + // element-level quantifier over list List(_) => Quantified(Box::new(shape), op), + // **temporal** quantifier: do NOT consume a list level here Seq(inner) => match *inner { List(_) | Quantified(_, _) => { Seq(Box::new(Quantified(Box::new(*inner), op))) @@ -579,42 +589,30 @@ impl PropertyFilter { }, Scalar(t) => Quantified(Box::new(Scalar(t)), op), - - _ => { - return Err(GraphError::InvalidFilter(format!( - "{:?} requires a list (or temporal list); got {:?}", - op, shape - ))) - } }, - // --- Aggregators map list/temporal(list|scalar) to a scalar dtype --- Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => match shape { - // Aggregate a plain list List(inner) => { - let t = *inner; - let out = agg_out(&t, op, "over list")?; + let out = agg_result_dtype(&*inner, op, "over list")?; Scalar(out) } - - // Aggregate over time Seq(inner) => match *inner { Scalar(t) => { - let out = agg_out(&t, op, "over time")?; + let out = agg_result_dtype(&t, op, "over time")?; Scalar(out) } List(t) => { - let elem_t = *t; - let out = agg_out(&elem_t, op, "over time of lists")?; + let out = agg_result_dtype(&*t, op, "over time of lists")?; Scalar(out) } - // time series with quantifier on the payload: preserve the quantifier, - // map the inner list dtype through the aggregator. - Quantified(qinner, qop) => { - let mapped_inner = match *qinner { + Quantified(q_inner, qop) => { + let mapped_inner = match *q_inner { List(t) => { - let out = - agg_out(&*t, op, "under temporal quantifier over list")?; + let out = agg_result_dtype( + &*t, + op, + "under temporal quantifier over list", + )?; Scalar(out) } Scalar(t) => { @@ -629,12 +627,10 @@ impl PropertyFilter { } Seq(_) => unreachable!(), }, - - // Aggregator under a non-temporal quantifier: preserve the quantifier. - Quantified(qinner, qop) => { - let mapped_inner = match *qinner { + Quantified(q_inner, qop) => { + let mapped_inner = match *q_inner { List(t) => { - let out = agg_out(&*t, op, "under quantifier over list")?; + let out = agg_result_dtype(&*t, op, "under quantifier over list")?; Scalar(out) } Scalar(t) => { @@ -647,8 +643,6 @@ impl PropertyFilter { }; Quantified(Box::new(mapped_inner), qop) } - - // Aggregators require a collection or temporal sequence before them. Scalar(t) => { return Err(GraphError::InvalidFilter(format!( "{:?} requires list or temporal sequence, got Scalar({:?})", @@ -659,18 +653,15 @@ impl PropertyFilter { }; } - // Effective dtype seen by the final operator. - let effective_dtype: PropType = match shape { + let eff = match shape { Scalar(t) => t, List(t) => PropType::List(t), - // Non-temporal quantified input: - // Any depth of quantifiers over a list exposes the element dtype. - // Any depth over a scalar (singleton policy) exposes the scalar dtype. + // pure (non-temporal) quantifier(s): consume list depth Quantified(_, _) => { - let (base, _qdepth) = flatten_quantified_depth(&shape); + let (base, qdepth) = flatten_quantified_depth(&shape); match base { - List(t) => (**t).clone(), + List(t) => Self::peel_list_n(PropType::List(t.clone()), qdepth)?, Scalar(t) => t.clone(), _ => { return Err(GraphError::InvalidFilter( @@ -680,18 +671,15 @@ impl PropertyFilter { } } - // Temporal payload + // temporal case: the first quantifier is temporal → ignore one quantifier for depth Seq(inner) => match *inner { - // time series of scalars -> List effective dtype Scalar(t) => PropType::List(Box::new(t)), - // time series of lists -> List> effective dtype List(t) => PropType::List(Box::new(PropType::List(t))), - - // Quantified per-time payload: same expose-as-element/singleton semantics. Quantified(_, _) => { - let (base, _qdepth) = flatten_quantified_depth(&*inner); + let (base, qdepth_total) = flatten_quantified_depth(&*inner); + let qdepth_lists = qdepth_total.saturating_sub(1); // skip the temporal one match base { - List(t) => (**t).clone(), + List(t) => Self::peel_list_n(PropType::List(t.clone()), qdepth_lists)?, Scalar(t) => t.clone(), _ => { return Err(GraphError::InvalidFilter( @@ -705,10 +693,7 @@ impl PropertyFilter { }, }; - // NOTE: Do **not** validate the final operator here. - // The caller (resolve_prop_id) will validate using the correct `expect_map_now` - // to support metadata map semantics. - Ok(effective_dtype) + Ok(eff) } pub fn resolve_prop_id(&self, meta: &Meta, expect_map: bool) -> Result { @@ -729,7 +714,6 @@ impl PropertyFilter { Some((id, dtype)) => (id, dtype), }; - // Decide map-semantics for metadata let is_original_map = matches!(original_dtype, PropType::Map(..)); let rhs_is_map = matches!(self.prop_value, PropertyFilterValue::Single(Prop::Map(_))); let expect_map_now = if is_static && rhs_is_map { @@ -738,11 +722,10 @@ impl PropertyFilter { is_static && expect_map && is_original_map }; - // Validate chain and final operator with correct semantics let eff = self.validate_chain_and_infer_effective_dtype(&original_dtype, is_temporal)?; - // (Redundant safety) final check self.validate_operator_against_dtype(&eff, expect_map_now)?; + Ok(id) } @@ -942,8 +925,6 @@ impl PropertyFilter { } } - /// Recursive reducer for nested quantifiers: applies the final predicate - /// at the innermost level and bubbles results back using each quantifier. fn reduce_qualifiers_rec( &self, quals: &[Op], @@ -955,7 +936,6 @@ impl PropertyFilter { } let (q, rest) = (quals[0], &quals[1..]); - // If we have a list, descend normally. if let Prop::List(inner) = v { let elems = inner.as_slice(); let check = |e: &Prop| { @@ -972,31 +952,23 @@ impl PropertyFilter { }; } - // No list at this level: apply singleton semantics. - // Consuming ANY/ALL over a single value is equivalent to just - // continuing with the remaining quantifiers on the same value. if rest.is_empty() { - // Last quantifier => apply to the single value. return match q { Op::Any | Op::All => predicate(v), _ => unreachable!(), }; } - // Still have deeper quantifiers: keep consuming them on the same value. self.reduce_qualifiers_rec(rest, v, predicate) } - /// Apply a list-style aggregator to a single Prop, with singleton semantics for scalars. fn apply_agg_to_prop(p: &Prop, op: Op) -> Option { match (op, p) { - // ----- Lists ----- (Op::Len, Prop::List(inner)) => Some(Prop::U64(inner.len() as u64)), (Op::Sum, Prop::List(inner)) | (Op::Avg, Prop::List(inner)) | (Op::Min, Prop::List(inner)) | (Op::Max, Prop::List(inner)) => Self::aggregate_values(inner.as_slice(), op), - // ----- Scalars (singleton semantics) ----- (Op::Len, _) => Some(Prop::U64(1)), (Op::Sum, Prop::U8(x)) => Some(Prop::U8(*x)), @@ -1083,43 +1055,29 @@ impl PropertyFilter { } } - // Non-numeric scalars for numeric aggs => None (Op::Sum, _) | (Op::Avg, _) | (Op::Min, _) | (Op::Max, _) => None, - // Selectors/qualifiers are handled elsewhere. _ => None, } } - /// Evaluate ops left-to-right to produce either a reduced scalar, - /// or a sequence plus quantifiers to apply. We also track whether - /// the produced sequence is truly *temporal* (came from a temporal - /// property) or just a normalized wrapper for element-quantifiers. fn eval_ops(&self, mut state: ValueType) -> (Option, Option>, Vec, bool) { - // Collect quantifiers in source order. let mut qualifiers: Vec = Vec::new(); - // Whether current sequence represents *time*. let mut seq_is_temporal = matches!(state, ValueType::Seq(..)); - // Track whether we've seen ANY/ALL before the first aggregator. let mut seen_qual_before_agg = false; - // Apply list-style aggregator to a single Prop (list or scalar). let per_step_map = |vals: Vec, op: Op| -> Vec { vals.into_iter() .filter_map(|p| Self::apply_agg_to_prop(&p, op)) .collect() }; - // Collapse the temporal sequence itself (aggregate OVER TIME). + // Aggregate OVER TIME (when no prior qualifier). let reduce_over_seq = |vs: Vec, op: Op| -> Option { match op { - Op::Len => Some(Prop::U64(vs.len() as u64)), // number of time steps + Op::Len => Some(Prop::U64(vs.len() as u64)), Op::Sum | Op::Avg | Op::Min | Op::Max => { - if vs.is_empty() { - return None; - } - // Only defined for sequences of scalars; if steps hold lists, return None. - if matches!(vs.first(), Some(Prop::List(_))) { + if vs.is_empty() || matches!(vs.first(), Some(Prop::List(_))) { return None; } Self::aggregate_values(&vs, op) @@ -1130,42 +1088,33 @@ impl PropertyFilter { for op in &self.ops { match *op { - Op::First => { - state = match state { - ValueType::Seq(vs) => { - // Collapsing time: after this, no temporal sequence remains. - seq_is_temporal = false; - ValueType::Scalar(vs.first().cloned()) - } - ValueType::Scalar(s) => ValueType::Scalar(s), - }; - } - Op::Last => { + Op::First | Op::Last => { state = match state { ValueType::Seq(vs) => { seq_is_temporal = false; - ValueType::Scalar(vs.last().cloned()) + let v = if matches!(*op, Op::First) { + vs.first() + } else { + vs.last() + }; + ValueType::Scalar(v.cloned()) } - ValueType::Scalar(s) => ValueType::Scalar(s), + s @ ValueType::Scalar(_) => s, }; } Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { state = match state { - // If a quantifier already occurred, aggregate per time step (do NOT collapse time). ValueType::Seq(vs) if seen_qual_before_agg => { ValueType::Seq(per_step_map(vs, *op)) } - // Otherwise, aggregate ACROSS time to one scalar. ValueType::Seq(vs) => { seq_is_temporal = false; ValueType::Scalar(reduce_over_seq(vs, *op)) } - // Non-temporal list -> reduce to scalar. ValueType::Scalar(Some(Prop::List(inner))) => { ValueType::Scalar(Self::aggregate_values(inner.as_slice(), *op)) } - // Non-temporal scalar -> singleton semantics. ValueType::Scalar(Some(p)) => { ValueType::Scalar(Self::apply_agg_to_prop(&p, *op)) } @@ -1176,16 +1125,8 @@ impl PropertyFilter { Op::Any | Op::All => { qualifiers.push(*op); seen_qual_before_agg = true; - - // If we *already* have a temporal sequence, keep it (temporal stays true). - // Otherwise, normalize the current (non-temporal) value into a 1-step sequence, - // but mark that sequence as NON-TEMPORAL so we don't treat the first quantifier - // as temporal in apply_eval. state = match state { - ValueType::Seq(vs) => { - // remains temporal - ValueType::Seq(vs) - } + ValueType::Seq(vs) => ValueType::Seq(vs), // still temporal ValueType::Scalar(Some(Prop::List(inner))) => { seq_is_temporal = false; ValueType::Seq(vec![Prop::List(inner)]) @@ -1216,83 +1157,69 @@ impl PropertyFilter { qualifiers: Vec, seq_is_temporal: bool, ) -> bool { - // 1) Reduced to a single scalar -> compare directly. if let Some(value) = reduced { return self .operator .apply_to_property(&self.prop_value, Some(&value)); } - // 2) Quantifiers present if !qualifiers.is_empty() { - // If the sequence is truly temporal, treat the **first** qualifier as temporal; - // otherwise, treat *all* qualifiers as element-level. - let (temporal_q_opt, elem_quals): (Option, &[Op]) = if seq_is_temporal { + let (temporal_q_opt, elem_quals) = if seq_is_temporal { (Some(qualifiers[0]), &qualifiers[1..]) } else { (None, &qualifiers[..]) }; - let pred = |p: &Prop| self.operator.apply_to_property(&self.prop_value, Some(p)); - - if let Some(seq) = maybe_seq { - // If there's a temporal quantifier, combine across time with it. - if let Some(temporal_q) = temporal_q_opt { - let mut saw_step = false; - match temporal_q { - Op::All => { - for p in &seq { - saw_step = true; - let ok = if elem_quals.is_empty() { - pred(p) - } else { - self.reduce_qualifiers_rec(elem_quals, p, &pred) - }; - if !ok { - return false; - } - } - return saw_step; - } - Op::Any => { - for p in &seq { - saw_step = true; - let ok = if elem_quals.is_empty() { - pred(p) - } else { - self.reduce_qualifiers_rec(elem_quals, p, &pred) - }; - if ok { - return true; - } + let Some(seq) = maybe_seq else { return false }; + + if let Some(tq) = temporal_q_opt { + let mut saw = false; + match tq { + Op::All => { + for p in &seq { + saw = true; + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if !ok { + return false; } - return false; } - _ => unreachable!(), + return saw; } - } else { - // No temporal combinator: just apply *element* quantifiers to the single value - // each seq item represents (this seq is non-temporal normalization). - // Since it's a non-temporal normalization, there is exactly one item (by construction), - // but handling multiple is harmless. - for p in &seq { - let ok = if elem_quals.is_empty() { - pred(p) - } else { - self.reduce_qualifiers_rec(elem_quals, p, &pred) - }; - if ok { - return true; + Op::Any => { + for p in &seq { + saw = true; + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if ok { + return true; + } } + return false; } - return false; + _ => unreachable!(), } } else { + for p in &seq { + let ok = if elem_quals.is_empty() { + pred(p) + } else { + self.reduce_qualifiers_rec(elem_quals, p, &pred) + }; + if ok { + return true; + } + } return false; } } - // 3) No quantifiers and not reduced: compare the whole sequence as a list. if let Some(seq) = maybe_seq { let full = Prop::List(Arc::new(seq)); self.operator @@ -1303,15 +1230,13 @@ impl PropertyFilter { } fn eval_scalar_and_apply(&self, prop: Option) -> bool { - let (reduced, maybe_seq, qualifiers, seq_is_temporal) = - self.eval_ops(ValueType::Scalar(prop)); - self.apply_eval(reduced, maybe_seq, qualifiers, seq_is_temporal) + let (r, s, q, is_t) = self.eval_ops(ValueType::Scalar(prop)); + self.apply_eval(r, s, q, is_t) } fn eval_temporal_and_apply(&self, props: Vec) -> bool { - let (reduced, maybe_seq, qualifiers, seq_is_temporal) = - self.eval_ops(ValueType::Seq(props)); - self.apply_eval(reduced, maybe_seq, qualifiers, seq_is_temporal) + let (r, s, q, is_t) = self.eval_ops(ValueType::Seq(props)); + self.apply_eval(r, s, q, is_t) } pub fn matches(&self, other: Option<&Prop>) -> bool { @@ -1362,16 +1287,55 @@ impl PropertyFilter { prop_id: usize, node: NodeStorageRef, ) -> bool { - let node = NodeView::new_internal(graph, node.vid()); + let node_view = NodeView::new_internal(graph, node.vid()); + match self.prop_ref { PropertyRef::Metadata(_) => { - let props = node.metadata(); + let props = node_view.metadata(); self.is_metadata_matched(prop_id, props) } - PropertyRef::TemporalProperty(_) | PropertyRef::Property(_) => { - let props = node.properties(); + + PropertyRef::Property(_) => { + let props = node_view.properties(); self.is_property_matched(prop_id, props) } + + PropertyRef::TemporalProperty(_) => { + let props = node_view.properties(); + let Some(tview) = props.temporal().get_by_id(prop_id) else { + return false; // no temporal values at all for this node/property + }; + + let seq: Vec = tview.values().collect(); + + if self.ops.is_empty() { + return self.eval_temporal_and_apply(seq); + } + + // If the FIRST quantifier is a temporal quantifier and it's ALL, + // require the temporal property to be present on EVERY node update time. + if self + .ops + .iter() + .copied() + .find(|op| matches!(op, Op::Any | Op::All)) + == Some(Op::All) + { + let semantics = graph.node_time_semantics(); + let core_node = graph.core_node(node_view.node); + + let node_update_count = + semantics.node_updates(core_node.as_ref(), graph).count(); + let prop_time_count = seq.len(); + + // Missing at any timepoint? Leading temporal ALL must fail. + if prop_time_count < node_update_count { + return false; + } + } + + self.eval_temporal_and_apply(seq) + } } } From fdce92682021a0e9b0b54909d9955abb0b3d38d4 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Wed, 8 Oct 2025 19:31:57 +0100 Subject: [PATCH 5/8] merge python traits, fix tests, add more validations --- python/python/raphtory/filter/__init__.pyi | 15 +- .../test_filters/test_node_property_filter.py | 28 +- .../views/filter/model/property_filter.rs | 73 +- raphtory/src/python/filter/filter_expr.rs | 27 +- .../python/filter/property_filter_builders.rs | 766 +++++++++++------- raphtory/src/python/graph/node.rs | 2 +- raphtory/src/python/graph/views/graph_view.rs | 2 +- .../types/macros/trait_impl/filter_ops.rs | 29 +- 8 files changed, 605 insertions(+), 337 deletions(-) diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 94427efba7..009c7c5670 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -169,19 +169,6 @@ class ExplodedEdge(object): def property(name): ... class Property(PropertyFilterOps): - """ - Construct a property filter - - Arguments: - name (str): the name of the property to filter - """ - def temporal(self): ... -class Metadata(PropertyFilterOps): - """ - Construct a metadata filter - - Arguments: - name (str): the name of the property to filter - """ +class Metadata(PropertyFilterOps): ... 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 0275ffc7af..37527ad0d7 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 @@ -810,18 +810,24 @@ def check(graph): return check +@with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) def test_filter_nodes_with_with_qualifier_alongside_illegal_agg_operators(): - with pytest.raises( - Exception, - match=r"List aggregation len cannot be used after an element qualifier \(any/all\)", - ): - filter.Node.property("prop8").all().len() - - with pytest.raises( - Exception, - match=r"Element qualifiers \(any/all\) cannot be used after a list aggregation \(len/sum/avg/min/max\).", - ): - filter.Node.property("prop8").sum().any() + def check(graph): + filter_expr = filter.Node.property("prop8").all().len() + with pytest.raises( + Exception, + match=r"List aggregation len cannot be used after an element qualifier \(any/all\)", + ): + graph.filter(filter_expr).nodes.id + + filter_expr = filter.Node.property("prop8").sum().any() + with pytest.raises( + Exception, + match=r"Element qualifiers \(any/all\) cannot be used after a list aggregation \(len/sum/avg/min/max\).", + ): + graph.filter(filter_expr).nodes.id + + return check @with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index 277ca7cc15..bab1ed6aaa 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -528,6 +528,70 @@ impl PropertyFilter { ) -> Result { use Shape::*; + // Ordering guard for qualifiers/aggregators/selectors === + // Disallow: + // 1) aggregator after qualifier, unless the *first* qualifier is temporal + // 2) qualifier after aggregator (always illegal) + // + // Ensures: + // - property(...).all().len() -> error + // - property(...).sum().any() -> error + // - property(...).temporal().all().len() -> OK + // - property(...).temporal().first().all().len() -> error + let mut saw_agg = false; + let mut saw_qual = false; + let mut saw_selector_before_first_qual = false; + let mut first_qual_is_temporal = false; + + #[inline] + fn agg_name(op: Op) -> &'static str { + match op { + Op::Len => "len", + Op::Sum => "sum", + Op::Avg => "avg", + Op::Min => "min", + Op::Max => "max", + _ => unreachable!(), + } + } + + for &op in &self.ops { + match op { + Op::First | Op::Last => { + if !saw_qual { + // A selector before the first qualifier collapses the temporal sequence, + // so the upcoming first qualifier cannot be considered temporal. + saw_selector_before_first_qual = true; + } + } + Op::Any | Op::All => { + // Qualifier after any aggregation is always illegal. + if saw_agg { + return Err(GraphError::InvalidFilter( + "Element qualifiers (any/all) cannot be used after a list aggregation (len/sum/avg/min/max).".into() + )); + } + // Record once whether the very first qualifier is "temporal-first" + // (chain is temporal and no selector has run yet). + if !saw_qual && is_temporal && !saw_selector_before_first_qual { + first_qual_is_temporal = true; + } + saw_qual = true; + } + Op::Len | Op::Sum | Op::Avg | Op::Min | Op::Max => { + // Aggregator after a qualifier is illegal unless that first qualifier is temporal. + if saw_qual && !first_qual_is_temporal { + return Err(GraphError::InvalidFilter(format!( + "List aggregation {} cannot be used after an element qualifier (any/all)", + agg_name(op) + ))); + } + saw_agg = true; + } + } + } + + // Build base shape let base_shape: Shape = if is_temporal { let inner: Shape = match src_dtype { PropType::List(inner) => List(inner.clone()), @@ -541,6 +605,7 @@ impl PropertyFilter { } }; + // Defer selectors to the end of validation let mut selectors: Vec = Vec::new(); let mut others: Vec = Vec::new(); for &op in &self.ops { @@ -554,6 +619,7 @@ impl PropertyFilter { ops_for_validation.extend(others); ops_for_validation.extend(selectors); + // Walk the shape backwards through the ops let mut shape = base_shape; for &op in ops_for_validation.iter().rev() { shape = match op { @@ -653,6 +719,7 @@ impl PropertyFilter { }; } + // Compute effective dtype let eff = match shape { Scalar(t) => t, List(t) => PropType::List(t), @@ -676,10 +743,10 @@ impl PropertyFilter { Scalar(t) => PropType::List(Box::new(t)), List(t) => PropType::List(Box::new(PropType::List(t))), Quantified(_, _) => { - let (base, qdepth_total) = flatten_quantified_depth(&*inner); - let qdepth_lists = qdepth_total.saturating_sub(1); // skip the temporal one + let (base, q_depth_total) = flatten_quantified_depth(&*inner); + let q_depth_lists = q_depth_total.saturating_sub(1); // skip the temporal one match base { - List(t) => Self::peel_list_n(PropType::List(t.clone()), qdepth_lists)?, + List(t) => Self::peel_list_n(PropType::List(t.clone()), q_depth_lists)?, Scalar(t) => t.clone(), _ => { return Err(GraphError::InvalidFilter( diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 75b3a32469..8a7dd3525c 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -11,9 +11,11 @@ use crate::{ }, errors::GraphError, prelude::GraphViewOps, - python::filter::create_filter::DynInternalFilterOps, + python::filter::{ + create_filter::DynInternalFilterOps, property_filter_builders::PyPropertyFilterOps, + }, }; -use pyo3::prelude::*; +use pyo3::{exceptions::PyTypeError, prelude::*}; use std::sync::Arc; #[pyclass(frozen, name = "FilterExpr", module = "raphtory.filter")] @@ -62,3 +64,24 @@ impl CreateFilter for PyFilterExpr { self.0.create_filter(graph) } } + +pub enum AcceptFilter { + Expr(PyFilterExpr), + Chain(Py), +} + +impl<'py> FromPyObject<'py> for AcceptFilter { + fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { + if let Ok(expr) = ob.extract::() { + return Ok(AcceptFilter::Expr(expr)); + } + + if let Ok(chain) = ob.extract::>() { + return Ok(AcceptFilter::Chain(chain)); + } + + Err(PyTypeError::new_err( + "Expected a FilterExpr or a PropertyFilterOps chain", + )) + } +} diff --git a/raphtory/src/python/filter/property_filter_builders.rs b/raphtory/src/python/filter/property_filter_builders.rs index 9c731c3f11..f44729b590 100644 --- a/raphtory/src/python/filter/property_filter_builders.rs +++ b/raphtory/src/python/filter/property_filter_builders.rs @@ -3,8 +3,8 @@ use crate::{ internal::CreateFilter, model::{ property_filter::{ - ElemQualifierOps, InternalPropertyFilterOps, ListAggOps, MetadataFilterBuilder, - OpChainBuilder, PropertyFilterBuilder, PropertyFilterOps, + ElemQualifierOps, ListAggOps, MetadataFilterBuilder, OpChainBuilder, + PropertyFilterBuilder, PropertyFilterOps, }, TryAsCompositeFilter, }, @@ -18,7 +18,7 @@ use pyo3::{ use raphtory_api::core::entities::properties::prop::Prop; use std::sync::Arc; -pub trait DynPropertyFilterOps: Send + Sync { +pub trait DynFilterOps: Send + Sync { fn __eq__(&self, value: Prop) -> PyFilterExpr; fn __ne__(&self, value: Prop) -> PyFilterExpr; @@ -53,67 +53,219 @@ pub trait DynPropertyFilterOps: Send + Sync { levenshtein_distance: usize, prefix_match: bool, ) -> PyFilterExpr; + + fn any(&self) -> PyResult; + + fn all(&self) -> PyResult; + + fn len(&self) -> PyResult; + + fn sum(&self) -> PyResult; + + fn avg(&self) -> PyResult; + + fn min(&self) -> PyResult; + + fn max(&self) -> PyResult; + + fn first(&self) -> PyResult; + + fn last(&self) -> PyResult; + + fn temporal(&self) -> PyResult; +} + +impl DynFilterOps for Arc { + #[inline] + fn __eq__(&self, v: Prop) -> PyFilterExpr { + (**self).__eq__(v) + } + + #[inline] + fn __ne__(&self, v: Prop) -> PyFilterExpr { + (**self).__ne__(v) + } + + #[inline] + fn __lt__(&self, v: Prop) -> PyFilterExpr { + (**self).__lt__(v) + } + + #[inline] + fn __le__(&self, v: Prop) -> PyFilterExpr { + (**self).__le__(v) + } + + #[inline] + fn __gt__(&self, v: Prop) -> PyFilterExpr { + (**self).__gt__(v) + } + + #[inline] + fn __ge__(&self, v: Prop) -> PyFilterExpr { + (**self).__ge__(v) + } + + #[inline] + fn is_in(&self, vs: FromIterable) -> PyFilterExpr { + (**self).is_in(vs) + } + + #[inline] + fn is_not_in(&self, vs: FromIterable) -> PyFilterExpr { + (**self).is_not_in(vs) + } + + #[inline] + fn is_none(&self) -> PyFilterExpr { + (**self).is_none() + } + + #[inline] + fn is_some(&self) -> PyFilterExpr { + (**self).is_some() + } + + #[inline] + fn starts_with(&self, v: Prop) -> PyFilterExpr { + (**self).starts_with(v) + } + + #[inline] + fn ends_with(&self, v: Prop) -> PyFilterExpr { + (**self).ends_with(v) + } + + #[inline] + fn contains(&self, v: Prop) -> PyFilterExpr { + (**self).contains(v) + } + + #[inline] + fn not_contains(&self, v: Prop) -> PyFilterExpr { + (**self).not_contains(v) + } + + #[inline] + fn fuzzy_search( + &self, + prop_value: String, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyFilterExpr { + (**self).fuzzy_search(prop_value, levenshtein_distance, prefix_match) + } + + #[inline] + fn any(&self) -> PyResult { + (**self).any() + } + + #[inline] + fn all(&self) -> PyResult { + (**self).all() + } + + #[inline] + fn len(&self) -> PyResult { + (**self).len() + } + + #[inline] + fn sum(&self) -> PyResult { + (**self).sum() + } + + #[inline] + fn avg(&self) -> PyResult { + (**self).avg() + } + + #[inline] + fn min(&self) -> PyResult { + (**self).min() + } + + #[inline] + fn max(&self) -> PyResult { + (**self).max() + } + + #[inline] + fn first(&self) -> PyResult { + (**self).first() + } + + #[inline] + fn last(&self) -> PyResult { + (**self).last() + } + + #[inline] + fn temporal(&self) -> PyResult { + (**self).temporal() + } } -impl DynPropertyFilterOps for F +impl DynFilterOps for PropertyFilterBuilder where - F: PropertyFilterOps, - PropertyFilter: CreateFilter + TryAsCompositeFilter, + M: Clone + Send + Sync + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, { fn __eq__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.eq(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::eq(self, value))) } fn __ne__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.ne(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::ne(self, value))) } fn __lt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.lt(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::lt(self, value))) } fn __le__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.le(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::le(self, value))) } fn __gt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.gt(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::gt(self, value))) } fn __ge__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.ge(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::ge(self, value))) } fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.is_in(values))) + PyFilterExpr(Arc::new(PropertyFilterOps::is_in(self, values))) } fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.is_not_in(values))) + PyFilterExpr(Arc::new(PropertyFilterOps::is_not_in(self, values))) } fn is_none(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.is_none())) + PyFilterExpr(Arc::new(PropertyFilterOps::is_none(self))) } fn is_some(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.is_some())) + PyFilterExpr(Arc::new(PropertyFilterOps::is_some(self))) } fn starts_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.starts_with(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::starts_with(self, value))) } fn ends_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.ends_with(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::ends_with(self, value))) } fn contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.contains(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::contains(self, value))) } fn not_contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.not_contains(value))) + PyFilterExpr(Arc::new(PropertyFilterOps::not_contains(self, value))) } fn fuzzy_search( @@ -122,118 +274,118 @@ where levenshtein_distance: usize, prefix_match: bool, ) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.fuzzy_search( + PyFilterExpr(Arc::new(PropertyFilterOps::fuzzy_search( + self, prop_value, levenshtein_distance, prefix_match, ))) } -} -#[pyclass( - frozen, - name = "PropertyFilterOps", - module = "raphtory.filter", - subclass -)] -pub struct PyPropertyFilterOps { - ops: Arc, - agg: Arc, - qual: Arc, - sel: Arc, -} + fn any(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::any(self))) + } -impl PyPropertyFilterOps { - fn from_parts( - ops_provider: Arc, - agg_provider: Arc, - qual_provider: Arc, - sel_provider: Arc, - ) -> Self - where - O: DynPropertyFilterOps + 'static, - A: DynListAggOps + 'static, - Q: DynElemQualifierOps + 'static, - S: DynSelectorOps + 'static, - { - PyPropertyFilterOps { - ops: ops_provider, - agg: agg_provider, - qual: qual_provider, - sel: sel_provider, - } - } - - fn from_builder(builder: B) -> Self - where - B: DynPropertyFilterOps + DynListAggOps + DynElemQualifierOps + DynSelectorOps + 'static, - { - let shared: Arc = Arc::new(builder); - PyPropertyFilterOps { - ops: shared.clone(), - agg: shared.clone(), - qual: shared.clone(), - sel: shared, - } + fn all(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::all(self))) + } + + fn len(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ListAggOps::len(self))) + } + + fn sum(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ListAggOps::sum(self))) + } + + fn avg(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ListAggOps::avg(self))) + } + + fn min(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ListAggOps::min(self))) + } + + fn max(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ListAggOps::max(self))) + } + + fn first(&self) -> PyResult { + Err(PyTypeError::new_err( + "first() is only valid on temporal properties. Call temporal() first.", + )) + } + + fn last(&self) -> PyResult { + Err(PyTypeError::new_err( + "last() is only valid on temporal properties. Call temporal() first.", + )) + } + + fn temporal(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(self.clone().temporal())) } } -#[pymethods] -impl PyPropertyFilterOps { +impl DynFilterOps for MetadataFilterBuilder +where + M: Clone + Send + Sync + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, +{ fn __eq__(&self, value: Prop) -> PyFilterExpr { - self.ops.__eq__(value) + PyFilterExpr(Arc::new(PropertyFilterOps::eq(self, value))) } fn __ne__(&self, value: Prop) -> PyFilterExpr { - self.ops.__ne__(value) + PyFilterExpr(Arc::new(PropertyFilterOps::ne(self, value))) } fn __lt__(&self, value: Prop) -> PyFilterExpr { - self.ops.__lt__(value) + PyFilterExpr(Arc::new(PropertyFilterOps::lt(self, value))) } fn __le__(&self, value: Prop) -> PyFilterExpr { - self.ops.__le__(value) + PyFilterExpr(Arc::new(PropertyFilterOps::le(self, value))) } fn __gt__(&self, value: Prop) -> PyFilterExpr { - self.ops.__gt__(value) + PyFilterExpr(Arc::new(PropertyFilterOps::gt(self, value))) } fn __ge__(&self, value: Prop) -> PyFilterExpr { - self.ops.__ge__(value) + PyFilterExpr(Arc::new(PropertyFilterOps::ge(self, value))) } fn is_in(&self, values: FromIterable) -> PyFilterExpr { - self.ops.is_in(values) + PyFilterExpr(Arc::new(PropertyFilterOps::is_in(self, values))) } fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - self.ops.is_not_in(values) + PyFilterExpr(Arc::new(PropertyFilterOps::is_not_in(self, values))) } fn is_none(&self) -> PyFilterExpr { - self.ops.is_none() + PyFilterExpr(Arc::new(PropertyFilterOps::is_none(self))) } fn is_some(&self) -> PyFilterExpr { - self.ops.is_some() + PyFilterExpr(Arc::new(PropertyFilterOps::is_some(self))) } fn starts_with(&self, value: Prop) -> PyFilterExpr { - self.ops.starts_with(value) + PyFilterExpr(Arc::new(PropertyFilterOps::starts_with(self, value))) } fn ends_with(&self, value: Prop) -> PyFilterExpr { - self.ops.ends_with(value) + PyFilterExpr(Arc::new(PropertyFilterOps::ends_with(self, value))) } fn contains(&self, value: Prop) -> PyFilterExpr { - self.ops.contains(value) + PyFilterExpr(Arc::new(PropertyFilterOps::contains(self, value))) } fn not_contains(&self, value: Prop) -> PyFilterExpr { - self.ops.not_contains(value) + PyFilterExpr(Arc::new(PropertyFilterOps::not_contains(self, value))) } fn fuzzy_search( @@ -242,305 +394,322 @@ impl PyPropertyFilterOps { levenshtein_distance: usize, prefix_match: bool, ) -> PyFilterExpr { - self.ops - .fuzzy_search(prop_value, levenshtein_distance, prefix_match) - } - - pub fn first(&self) -> PyResult { - self.sel.first() - } - - pub fn last(&self) -> PyResult { - self.sel.last() + PyFilterExpr(Arc::new(PropertyFilterOps::fuzzy_search( + self, + prop_value, + levenshtein_distance, + prefix_match, + ))) } - pub fn any(&self) -> PyResult { - self.qual.any() + fn any(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::any(self))) } - pub fn all(&self) -> PyResult { - self.qual.all() + fn all(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::all(self))) } fn len(&self) -> PyResult { - self.agg.len() + Ok(PyPropertyFilterOps::wrap(ListAggOps::len(self))) } fn sum(&self) -> PyResult { - self.agg.sum() + Ok(PyPropertyFilterOps::wrap(ListAggOps::sum(self))) } fn avg(&self) -> PyResult { - self.agg.avg() + Ok(PyPropertyFilterOps::wrap(ListAggOps::avg(self))) } fn min(&self) -> PyResult { - self.agg.min() + Ok(PyPropertyFilterOps::wrap(ListAggOps::min(self))) } fn max(&self) -> PyResult { - self.agg.max() + Ok(PyPropertyFilterOps::wrap(ListAggOps::max(self))) + } + + fn first(&self) -> PyResult { + Err(PyTypeError::new_err( + "first() is only valid on temporal properties.", + )) + } + + fn last(&self) -> PyResult { + Err(PyTypeError::new_err( + "last() is only valid on temporal properties.", + )) + } + + fn temporal(&self) -> PyResult { + Err(PyTypeError::new_err( + "temporal() is only valid on standard properties, not metadata.", + )) } } -pub trait DynListAggOps: Send + Sync { - fn len(&self) -> PyResult; +impl DynFilterOps for OpChainBuilder +where + M: Send + Sync + Clone + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, +{ + fn __eq__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::eq(self, value))) + } - fn sum(&self) -> PyResult; + fn __ne__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::ne(self, value))) + } - fn avg(&self) -> PyResult; + fn __lt__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::lt(self, value))) + } - fn min(&self) -> PyResult; + fn __le__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::le(self, value))) + } - fn max(&self) -> PyResult; -} + fn __gt__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::gt(self, value))) + } -trait DynElemQualifierOps: Send + Sync { - fn any(&self) -> PyResult; + fn __ge__(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::ge(self, value))) + } - fn all(&self) -> PyResult; -} + fn is_in(&self, values: FromIterable) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::is_in(self, values))) + } -#[derive(Clone)] -struct NoElemQualifiers; + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::is_not_in(self, values))) + } + + fn is_none(&self) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::is_none(self))) + } + + fn is_some(&self) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::is_some(self))) + } + + fn starts_with(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::starts_with(self, value))) + } + + fn ends_with(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::ends_with(self, value))) + } + + fn contains(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::contains(self, value))) + } + + fn not_contains(&self, value: Prop) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::not_contains(self, value))) + } + + fn fuzzy_search( + &self, + prop_value: String, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyFilterExpr { + PyFilterExpr(Arc::new(PropertyFilterOps::fuzzy_search( + self, + prop_value, + levenshtein_distance, + prefix_match, + ))) + } -impl DynElemQualifierOps for NoElemQualifiers { fn any(&self) -> PyResult { - Err(PyTypeError::new_err("Element qualifiers (any/all) cannot be used after a list aggregation (len/sum/avg/min/max).")) + Ok(PyPropertyFilterOps::wrap(self.clone().any())) } fn all(&self) -> PyResult { - Err(PyTypeError::new_err("Element qualifiers (any/all) cannot be used after a list aggregation (len/sum/avg/min/max).")) + Ok(PyPropertyFilterOps::wrap(self.clone().all())) } -} - -#[derive(Clone)] -struct NoListAggOps; -impl DynListAggOps for NoListAggOps { fn len(&self) -> PyResult { - Err(PyTypeError::new_err( - "List aggregation len cannot be used after an element qualifier (any/all).", - )) + Ok(PyPropertyFilterOps::wrap(self.clone().len())) } fn sum(&self) -> PyResult { - Err(PyTypeError::new_err( - "List aggregation sum cannot be used after an element qualifier (any/all).", - )) + Ok(PyPropertyFilterOps::wrap(self.clone().sum())) } fn avg(&self) -> PyResult { - Err(PyTypeError::new_err( - "List aggregation avg cannot be used after an element qualifier (any/all).", - )) + Ok(PyPropertyFilterOps::wrap(self.clone().avg())) } fn min(&self) -> PyResult { - Err(PyTypeError::new_err( - "List aggregation min cannot be used after an element qualifier (any/all).", - )) + Ok(PyPropertyFilterOps::wrap(self.clone().min())) } fn max(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(self.clone().max())) + } + + fn first(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(self.clone().first())) + } + + fn last(&self) -> PyResult { + Ok(PyPropertyFilterOps::wrap(self.clone().last())) + } + + fn temporal(&self) -> PyResult { Err(PyTypeError::new_err( - "List aggregation max cannot be used after an element qualifier (any/all).", + "temporal() must be called on a property builder, not on an existing chain.", )) } } -trait DynSelectorOps: Send + Sync { - fn first(&self) -> PyResult; +#[pyclass( + frozen, + name = "PropertyFilterOps", + module = "raphtory.filter", + subclass +)] +pub struct PyPropertyFilterOps { + ops: Arc, +} - fn last(&self) -> PyResult; +impl PyPropertyFilterOps { + fn wrap(t: T) -> Self { + Self { ops: Arc::new(t) } + } + + fn from_arc(ops: Arc) -> Self { + Self { ops } + } } -#[derive(Clone)] -struct NoSelector; +#[pymethods] +impl PyPropertyFilterOps { + fn __eq__(&self, value: Prop) -> PyFilterExpr { + self.ops.__eq__(value) + } -impl DynSelectorOps for NoSelector { - fn first(&self) -> PyResult { - Err(PyTypeError::new_err( - "first() is only valid on temporal properties.", - )) + fn __ne__(&self, value: Prop) -> PyFilterExpr { + self.ops.__ne__(value) } - fn last(&self) -> PyResult { - Err(PyTypeError::new_err( - "last() is only valid on temporal properties.", - )) + fn __lt__(&self, value: Prop) -> PyFilterExpr { + self.ops.__lt__(value) } -} -impl DynListAggOps for T -where - T: ListAggOps + InternalPropertyFilterOps + Clone + Send + Sync + 'static, - PropertyFilter<::Marker>: CreateFilter + TryAsCompositeFilter, -{ - fn len(&self) -> PyResult { - let ops = Arc::new(::len(&self)); - let agg = Arc::new(self.clone()); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) + fn __le__(&self, value: Prop) -> PyFilterExpr { + self.ops.__le__(value) } - fn sum(&self) -> PyResult { - let ops = Arc::new(::sum(&self)); - let agg = Arc::new(self.clone()); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) + fn __gt__(&self, value: Prop) -> PyFilterExpr { + self.ops.__gt__(value) } - fn avg(&self) -> PyResult { - let ops = Arc::new(::avg(&self)); - let agg = Arc::new(self.clone()); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) + fn __ge__(&self, value: Prop) -> PyFilterExpr { + self.ops.__ge__(value) } - fn min(&self) -> PyResult { - let ops = Arc::new(::min(&self)); - let agg = Arc::new(self.clone()); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) + fn is_in(&self, values: FromIterable) -> PyFilterExpr { + self.ops.is_in(values) } - fn max(&self) -> PyResult { - let ops = Arc::new(::max(&self)); - let agg = Arc::new(self.clone()); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - Ok(PyPropertyFilterOps::from_parts(ops, agg, qual, sel)) + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + self.ops.is_not_in(values) } -} -trait QualifierBehavior: - InternalPropertyFilterOps + ElemQualifierOps + Clone + Send + Sync + 'static -where - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn build_any(&self) -> PyPropertyFilterOps; + fn is_none(&self) -> PyFilterExpr { + self.ops.is_none() + } - fn build_all(&self) -> PyPropertyFilterOps; -} + fn is_some(&self) -> PyFilterExpr { + self.ops.is_some() + } -impl QualifierBehavior for PropertyFilterBuilder -where - M: Clone + Send + Sync + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn build_any(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::any(self)); - let agg = Arc::new(NoListAggOps); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - PyPropertyFilterOps::from_parts(ops, agg, qual, sel) + fn starts_with(&self, value: Prop) -> PyFilterExpr { + self.ops.starts_with(value) } - fn build_all(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::all(self)); - let agg = Arc::new(NoListAggOps); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - PyPropertyFilterOps::from_parts(ops, agg, qual, sel) + fn ends_with(&self, value: Prop) -> PyFilterExpr { + self.ops.ends_with(value) } -} -impl QualifierBehavior for MetadataFilterBuilder -where - M: Clone + Send + Sync + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn build_any(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::any(self)); - let agg = Arc::new(NoListAggOps); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - PyPropertyFilterOps::from_parts(ops, agg, qual, sel) - } - fn build_all(&self) -> PyPropertyFilterOps { - let ops = Arc::new(ElemQualifierOps::all(self)); - let agg = Arc::new(NoListAggOps); - let qual = Arc::new(NoElemQualifiers); - let sel = Arc::new(NoSelector); - PyPropertyFilterOps::from_parts(ops, agg, qual, sel) + fn contains(&self, value: Prop) -> PyFilterExpr { + self.ops.contains(value) } -} -impl DynSelectorOps for OpChainBuilder -where - M: Send + Sync + Clone + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn first(&self) -> PyResult { - Ok(PyPropertyFilterOps::from_builder(self.clone().first())) + fn not_contains(&self, value: Prop) -> PyFilterExpr { + self.ops.not_contains(value) } - fn last(&self) -> PyResult { - Ok(PyPropertyFilterOps::from_builder(self.clone().last())) + fn fuzzy_search( + &self, + prop_value: String, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyFilterExpr { + self.ops + .fuzzy_search(prop_value, levenshtein_distance, prefix_match) } -} -impl QualifierBehavior for OpChainBuilder -where - M: Send + Sync + Clone + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn build_any(&self) -> PyPropertyFilterOps { - let chain = self.clone().any(); - let ops = Arc::new(chain.clone()); - let agg = Arc::new(chain.clone()); - let qual = Arc::new(chain.clone()); - let sel = Arc::new(NoSelector); - PyPropertyFilterOps::from_parts(ops, agg, qual, sel) - } - - fn build_all(&self) -> PyPropertyFilterOps { - let chain = self.clone().all(); - let ops = Arc::new(chain.clone()); - let agg = Arc::new(chain.clone()); - let qual = Arc::new(chain.clone()); - let sel = Arc::new(NoSelector); - PyPropertyFilterOps::from_parts(ops, agg, qual, sel) + pub fn first(&self) -> PyResult { + self.ops.first() } -} -impl DynElemQualifierOps for T -where - T: QualifierBehavior, - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn any(&self) -> PyResult { - Ok(self.build_any()) + pub fn last(&self) -> PyResult { + self.ops.last() } - fn all(&self) -> PyResult { - Ok(self.build_all()) + pub fn any(&self) -> PyResult { + self.ops.any() + } + + pub fn all(&self) -> PyResult { + self.ops.all() + } + + fn len(&self) -> PyResult { + self.ops.len() + } + + fn sum(&self) -> PyResult { + self.ops.sum() + } + + fn avg(&self) -> PyResult { + self.ops.avg() + } + + fn min(&self) -> PyResult { + self.ops.min() + } + + fn max(&self) -> PyResult { + self.ops.max() } } -trait DynPropertyFilterBuilderOps: Send + Sync { - fn temporal(&self) -> PyPropertyFilterOps; +trait DynPropertyFilterBuilderOps: DynFilterOps { + fn as_dyn(self: Arc) -> Arc; } -impl DynPropertyFilterBuilderOps for PropertyFilterBuilder +impl DynPropertyFilterBuilderOps for T where - PropertyFilter: CreateFilter + TryAsCompositeFilter, + T: DynFilterOps + 'static, { - fn temporal(&self) -> PyPropertyFilterOps { - PyPropertyFilterOps::from_builder(self.clone().temporal()) + fn as_dyn(self: Arc) -> Arc { + self } } -/// Construct a property filter -/// -/// Arguments: -/// name (str): the name of the property to filter -#[pyclass(frozen, name = "Property", module = "raphtory.filter", extends=PyPropertyFilterOps +#[pyclass( + frozen, + name = "Property", + module = "raphtory.filter", + extends = PyPropertyFilterOps )] #[derive(Clone)] pub struct PyPropertyFilterBuilder(Arc); @@ -554,30 +723,25 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let inner = Arc::new(self); - let parent = PyPropertyFilterOps { - ops: inner.clone(), - agg: inner.clone(), - qual: inner.clone(), - sel: Arc::new(NoSelector), - }; - let child = PyPropertyFilterBuilder(inner as Arc); + let inner: Arc> = Arc::new(self); + let parent = PyPropertyFilterOps::from_arc(inner.clone().as_dyn()); + let child = PyPropertyFilterBuilder(inner); Bound::new(py, (child, parent)) } } #[pymethods] impl PyPropertyFilterBuilder { - fn temporal(&self) -> PyPropertyFilterOps { + fn temporal(&self) -> PyResult { self.0.temporal() } } -/// Construct a metadata filter -/// -/// Arguments: -/// name (str): the name of the property to filter -#[pyclass(frozen, name = "Metadata", module = "raphtory.filter", extends=PyPropertyFilterOps +#[pyclass( + frozen, + name = "Metadata", + module = "raphtory.filter", + extends = PyPropertyFilterOps )] #[derive(Clone)] pub struct PyMetadataFilterBuilder; @@ -591,12 +755,8 @@ where type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let parent = PyPropertyFilterOps { - ops: Arc::new(self.clone()), - agg: Arc::new(self.clone()), - qual: Arc::new(self), - sel: Arc::new(NoSelector), - }; + let inner: Arc> = Arc::new(self); + let parent = PyPropertyFilterOps::from_arc(inner.clone().as_dyn()); let child = PyMetadataFilterBuilder; Bound::new(py, (child, parent)) } diff --git a/raphtory/src/python/graph/node.rs b/raphtory/src/python/graph/node.rs index 9703842ffd..4614c211e5 100644 --- a/raphtory/src/python/graph/node.rs +++ b/raphtory/src/python/graph/node.rs @@ -24,7 +24,7 @@ use crate::{ errors::GraphError, prelude::PropertiesOps, python::{ - filter::filter_expr::PyFilterExpr, + filter::filter_expr::{AcceptFilter, PyFilterExpr}, graph::{ node::internal::BaseFilter, properties::{MetadataView, PropertiesView, PyMetadataListList, PyNestedPropsIterable}, diff --git a/raphtory/src/python/graph/views/graph_view.rs b/raphtory/src/python/graph/views/graph_view.rs index d5d1f9f09e..8f0afd18b2 100644 --- a/raphtory/src/python/graph/views/graph_view.rs +++ b/raphtory/src/python/graph/views/graph_view.rs @@ -33,7 +33,7 @@ use crate::{ errors::GraphError, prelude::*, python::{ - filter::filter_expr::PyFilterExpr, + filter::filter_expr::{AcceptFilter, PyFilterExpr}, graph::{edge::PyEdge, node::PyNode}, types::repr::{Repr, StructReprBuilder}, utils::PyNodeRef, diff --git a/raphtory/src/python/types/macros/trait_impl/filter_ops.rs b/raphtory/src/python/types/macros/trait_impl/filter_ops.rs index bbc522c12b..ee19eaa7bf 100644 --- a/raphtory/src/python/types/macros/trait_impl/filter_ops.rs +++ b/raphtory/src/python/types/macros/trait_impl/filter_ops.rs @@ -18,10 +18,35 @@ macro_rules! impl_filter_ops { #[doc=concat!(" ", $name, ": The filtered view")] fn filter( &self, - filter: PyFilterExpr, + filter: AcceptFilter, ) -> Result<<$base_type as BaseFilter<'static>>::Filtered, GraphError> { - Ok(self.$field.clone().filter(filter)?.into_dyn_hop()) + match filter { + AcceptFilter::Expr(expr) => { + Ok(self.$field.clone().filter(expr)?.into_dyn_hop()) + } + AcceptFilter::Chain(chain) => { + // Force Rust-side validation by turning the chain into a temporary FilterExpr. + pyo3::Python::with_gil(|py| -> Result<(), GraphError> { + // Call .is_some() to produce a FilterExpr (any operator is fine). + let obj = chain + .bind(py) + .call_method0("is_some") + .map_err(|e| GraphError::InvalidFilter(e.to_string()))?; + let probe: PyFilterExpr = obj + .extract() + .map_err(|e| GraphError::InvalidFilter(e.to_string()))?; + // This triggers resolve/validate in Rust: + let _ = self.$field.clone().filter(probe)?; + Ok(()) + })?; + + // If we get here, the chain was syntactically valid but not a full FilterExpr. + Err(GraphError::InvalidFilter( + "Expected FilterExpr; got a property chain. Add a comparison (e.g., ... > 2).".into(), + )) + } + } } } }; From 6de1a5c2bedeac1bc81b9ad9eb37cbb2b90dd570 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Tue, 14 Oct 2025 14:10:30 +0100 Subject: [PATCH 6/8] rework gql filtering apis, fix tests --- .../test_graphql/test_apply_views.py | 101 +- .../test_filters/test_edge_filter_gql.py | 34 +- .../test_graph_edges_property_filter.py | 605 ++------ .../test_graph_nodes_property_filter.py | 639 ++------ .../test_filters/test_neighbours_filter.py | 51 +- .../test_filters/test_node_filter_gql.py | 63 +- .../test_nodes_property_filter.py | 711 +++------ raphtory-benchmark/benches/search_bench.rs | 40 +- raphtory-graphql/schema.graphql | 546 +------ raphtory-graphql/src/model/graph/filtering.rs | 1310 +++++++++-------- raphtory/src/db/graph/views/filter/mod.rs | 4 +- .../views/filter/model/filter_operator.rs | 34 +- .../src/db/graph/views/filter/model/mod.rs | 10 +- .../graph/views/filter/model/node_filter.rs | 11 +- .../views/filter/model/property_filter.rs | 6 +- raphtory/src/search/query_builder.rs | 12 +- 16 files changed, 1340 insertions(+), 2837 deletions(-) diff --git a/python/tests/test_base_install/test_graphql/test_apply_views.py b/python/tests/test_base_install/test_graphql/test_apply_views.py index ea6eb2ba22..68dc608c1f 100644 --- a/python/tests/test_base_install/test_graphql/test_apply_views.py +++ b/python/tests/test_base_install/test_graphql/test_apply_views.py @@ -1456,30 +1456,27 @@ def test_apply_view_node_filter(): graph = Graph() create_graph_date(graph) query = """ -{ - graph(path: "g") { - applyViews(views: [ - { - nodeFilter: { - property: { - name: "where" - operator: EQUAL - value: {str: "Berlin"} - + { + graph(path: "g") { + applyViews(views: [ + { + nodeFilter: { + property: { + name: "where" + where: { eq: { str: "Berlin" } } + } + } + } + ]) { + nodes { + list { + name + } } - } - } - - ]) { - nodes { - list { - name } } } - } - } -""" + """ correct = {"graph": {"applyViews": {"nodes": {"list": [{"name": "1"}]}}}} run_graphql_test(query, correct, graph) @@ -1488,30 +1485,27 @@ def test_apply_view_edge_filter(): graph = Graph() create_graph_date(graph) query = """ -{ - graph(path: "g") { - applyViews(views: [ - { - edgeFilter: { - property: { - name: "where" - operator: EQUAL - value: {str: "fishbowl"} - + { + graph(path: "g") { + applyViews(views: [ + { + edgeFilter: { + property: { + name: "where" + where: { eq: { str: "fishbowl" } } + } + } + } + ]) { + edges { + list { + history + } } - } - } - - ]) { - edges { - list { - history } } } - } - } -""" + """ correct = { "graph": {"applyViews": {"edges": {"list": [{"history": [1736035200000]}]}}} } @@ -1634,22 +1628,23 @@ def test_apply_view_a_lot_of_views(): graph = Graph() create_graph_date(graph) query = """ -{ - graph(path: "g") { - nodes{ - applyViews(views: [ - {window: {start: 1735689600000, end: 1735862400000}}, - {layer: "follows"}, - {nodeFilter: {property: {name: "where", operator: EQUAL, value: {str: "Berlin"}}}}, - ]) { - list { - name - history + { + graph(path: "g") { + nodes { + applyViews(views: [ + { window: { start: 1735689600000, end: 1735862400000 } }, + { layer: "follows" }, + { nodeFilter: { property: { name: "where", where: { eq: { str: "Berlin" } } } } } + ]) { + list { + name + history + } + } } } } -} -}""" + """ correct = { "graph": { "nodes": { diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py index 6d9dd5c767..e172d489cd 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py @@ -13,20 +13,15 @@ def test_filter_edges_with_str_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { edgeFilter(filter: { - src: { - field: NODE_ID - operator: EQUAL - value: { str: "3" } - } + src: { + field: NODE_ID + where: { eq: { str: "3" } } + } }) { edges { list { - src { - name - } - dst { - name - } + src { name } + dst { name } } } } @@ -58,20 +53,15 @@ def test_filter_edges_with_num_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { edgeFilter(filter: { - src: { - field: NODE_ID - operator: EQUAL - value: { u64: 1 } - } + src: { + field: NODE_ID + where: { eq: { u64: 1 } } + } }) { edges { list { - src { - name - } - dst { - name - } + src { name } + dst { name } } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index fd2fd0b263..43519c960a 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -7,7 +7,6 @@ PERSISTENT_GRAPH = create_test_graph(PersistentGraph()) -# Edge property filter is not supported yet for PersistentGraph @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_equal(graph): query = """ @@ -15,19 +14,13 @@ def test_graph_edge_property_filter_equal(graph): graph(path: "g") { edgeFilter( filter: { - property: { - name: "eprop5" - operator: EQUAL - value: { list: [{i64: 1},{i64: 2},{i64: 3}]} + property: { + name: "eprop5" + where: { eq: { list: [{i64: 1},{i64: 2},{i64: 3}] } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -43,52 +36,19 @@ def test_graph_edge_property_filter_equal(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_equal_no_value_error(graph): +def test_graph_edge_property_filter_equal_type_error(graph): query = """ query { graph(path: "g") { edgeFilter( filter: { - property: { - name: "eprop5" - operator: EQUAL + property: { + name: "eprop5" + where: { eq: { i64: 1 } } } } ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator EQUAL requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - -# Edge property filter is not supported yet for PersistentGraph -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_equal_type_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "eprop5" - operator: EQUAL - value: { i64: 1 } - } - } - ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -105,20 +65,14 @@ def test_graph_edge_property_filter_not_equal(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop4" - operator: NOT_EQUAL - value: { bool: true } - } - } - ) { - edges { - list { - src{name} - dst{name} + filter: { + property: { + name: "eprop4" + where: { ne: { bool: true } } } } + ) { + edges { list { src { name } dst { name } } } } } } @@ -133,34 +87,6 @@ def test_graph_edge_property_filter_not_equal(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_not_equal_no_value_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "eprop4" - operator: NOT_EQUAL - } - } - ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator NOT_EQUAL requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - -# Edge property filter is not supported yet for PersistentGraph @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_not_equal_type_error(graph): query = """ @@ -168,19 +94,13 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): graph(path: "g") { edgeFilter( filter: { - property: { - name: "eprop4" - operator: NOT_EQUAL - value: { i64: 1 } + property: { + name: "eprop4" + where: { ne: { i64: 1 } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -191,7 +111,6 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): run_graphql_error_test(query, expected_error_message, graph) -# Edge property filter is not supported yet for PersistentGraph @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_greater_than_or_equal(graph): query = """ @@ -199,19 +118,13 @@ def test_graph_edge_property_filter_greater_than_or_equal(graph): graph(path: "g") { edgeFilter( filter: { - property: { - name: "eprop1" - operator: GREATER_THAN_OR_EQUAL - value: { i64: 60 } + property: { + name: "eprop1" + where: { ge: { i64: 60 } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -227,55 +140,19 @@ def test_graph_edge_property_filter_greater_than_or_equal(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_greater_than_or_equal_no_value_error(graph): +def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { edgeFilter( filter: { - property: { - name: "eprop1" - operator: GREATER_THAN_OR_EQUAL + property: { + name: "eprop1" + where: { ge: { bool: true } } } } ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = ( - "Invalid filter: Operator GREATER_THAN_OR_EQUAL requires a value" - ) - run_graphql_error_test(query, expected_error_message, graph) - - -# Edge property filter is not supported yet for PersistentGraph -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "eprop1" - operator: GREATER_THAN_OR_EQUAL - value: { bool: true } - } - } - ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -286,27 +163,20 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): run_graphql_error_test(query, expected_error_message, graph) -# Edge property filter is not supported yet for PersistentGraph @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_less_than_or_equal(graph): query = """ query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: LESS_THAN_OR_EQUAL - value: { i64: 30 } - } - } - ) { - edges { - list { - src{name} - dst{name} + filter: { + property: { + name: "eprop1" + where: { le: { i64: 30 } } } } + ) { + edges { list { src { name } dst { name } } } } } } @@ -326,55 +196,15 @@ def test_graph_edge_property_filter_less_than_or_equal(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_less_than_or_equal_no_value_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "eprop1" - operator: LESS_THAN_OR_EQUAL - } - } - ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = ( - "Invalid filter: Operator LESS_THAN_OR_EQUAL requires a value" - ) - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_less_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: LESS_THAN_OR_EQUAL - value: { str: "shivam" } - } - } + filter: { property: { name: "eprop1", where: { le: { str: "shivam" } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -391,20 +221,9 @@ def test_graph_edge_property_filter_greater_than(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: GREATER_THAN - value: { i64: 30 } - } - } + filter: { property: { name: "eprop1", where: { gt: { i64: 30 } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -419,54 +238,15 @@ def test_graph_edge_property_filter_greater_than(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_greater_than_no_value_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "eprop1" - operator: GREATER_THAN - } - } - ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator GREATER_THAN requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - -# Edge property filter is not supported yet for PersistentGraph @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_greater_than_type_error(graph): query = """ query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: GREATER_THAN - value: { str: "shivam" } - } - } + filter: { property: { name: "eprop1", where: { gt: { str: "shivam" } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -477,27 +257,15 @@ def test_graph_edge_property_filter_greater_than_type_error(graph): run_graphql_error_test(query, expected_error_message, graph) -# Edge property filter is not supported yet for PersistentGraph @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_less_than(graph): query = """ query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: LESS_THAN - value: { i64: 30 } - } - } + filter: { property: { name: "eprop1", where: { lt: { i64: 30 } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -512,53 +280,15 @@ def test_graph_edge_property_filter_less_than(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_less_than_no_value_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "eprop1" - operator: LESS_THAN - } - } - ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator LESS_THAN requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_less_than_type_error(graph): query = """ query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: LESS_THAN - value: { str: "shivam" } - } - } + filter: { property: { name: "eprop1", where: { lt: { str: "shivam" } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -575,19 +305,9 @@ def test_graph_edge_property_filter_is_none(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop5" - operator: IS_NONE - } - } + filter: { property: { name: "eprop5", where: { isNone: true } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -602,19 +322,9 @@ def test_graph_edge_property_filter_is_some(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop5" - operator: IS_SOME - } - } + filter: { property: { name: "eprop5", where: { isSome: true } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -641,20 +351,9 @@ def test_graph_edge_property_filter_is_in(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: IS_IN - value: { list: [{i64: 10},{i64: 20},{i64: 30}]} - } - } + filter: { property: { name: "eprop1", where: { isIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -680,20 +379,9 @@ def test_graph_edge_property_filter_is_empty_list(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: IS_IN - value: { list: []} - } - } + filter: { property: { name: "eprop1", where: { isIn: { list: [] } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -702,59 +390,21 @@ def test_graph_edge_property_filter_is_empty_list(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_is_in_no_value_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "prop1" - operator: IS_IN - } - } - ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator IS_IN requires a list" - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_is_in_type_error(graph): query = """ query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: IS_IN - value: { str: "shivam" } - } - } + filter: { property: { name: "eprop1", where: { isIn: { str: "shivam" } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } """ expected_error_message = ( - "Invalid filter: Operator IS_IN requires a list value, got Str(shivam)" + "Invalid filter: isIn requires a list value, got Str(shivam)" ) run_graphql_error_test(query, expected_error_message, graph) @@ -765,20 +415,9 @@ def test_graph_edge_property_filter_is_not_in(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: IS_NOT_IN - value: { list: [{i64: 10},{i64: 20},{i64: 30}]} - } - } + filter: { property: { name: "eprop1", where: { isNotIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -799,20 +438,9 @@ def test_graph_edge_property_filter_is_not_in_empty_list(graph): query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: IS_NOT_IN - value: { list: []} - } - } + filter: { property: { name: "eprop1", where: { isNotIn: { list: [] } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -833,59 +461,21 @@ def test_graph_edge_property_filter_is_not_in_empty_list(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_edge_property_filter_is_not_in_no_value_error(graph): - query = """ - query { - graph(path: "g") { - edgeFilter( - filter: { - property: { - name: "eprop1" - operator: IS_NOT_IN - } - } - ) { - edges { - list { - src{name} - dst{name} - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator IS_NOT_IN requires a list" - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_edge_property_filter_is_not_in_type_error(graph): query = """ query { graph(path: "g") { edgeFilter( - filter: { - property: { - name: "eprop1" - operator: IS_NOT_IN - value: { str: "shivam" } - } - } + filter: { property: { name: "eprop1", where: { isNotIn: { str: "shivam" } } } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } """ expected_error_message = ( - "Invalid filter: Operator IS_NOT_IN requires a list value, got Str(shivam)" + "Invalid filter: isNotIn requires a list value, got Str(shivam)" ) run_graphql_error_test(query, expected_error_message, graph) @@ -897,22 +487,15 @@ def test_graph_edge_not_property_filter(graph): graph(path: "g") { edgeFilter ( filter: { - not: - { - property: { - name: "eprop5" - operator: EQUAL - value: { list: [{i64: 1},{i64: 2}]} - } + not: { + property: { + name: "eprop5" + where: { eq: { list: [{i64: 1},{i64: 2}] } } } + } } ) { - edges { - list { - src{name} - dst{name} - } - } + edges { list { src { name } dst { name } } } } } } @@ -936,24 +519,18 @@ def test_graph_edge_not_property_filter(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_edges_property_filter_starts_with(graph): query = """ - query { - graph(path: "g") { - edgeFilter(filter: { - property: { - name: "eprop3" - operator: STARTS_WITH - value: { str: "xyz" } - } - }) { - edges { - list { - src { name } - dst { name } - } - } - } + query { + graph(path: "g") { + edgeFilter(filter: { + property: { + name: "eprop3" + where: { startsWith: { str: "xyz" } } } + }) { + edges { list { src { name } dst { name } } } } + } + } """ expected_output = { "graph": { @@ -974,24 +551,18 @@ def test_edges_property_filter_starts_with(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_edges_property_filter_ends_with(graph): query = """ - query { - graph(path: "g") { - edgeFilter(filter: { - property: { - name: "eprop3" - operator: ENDS_WITH - value: { str: "123" } - } - }) { - edges { - list { - src { name } - dst { name } - } - } - } + query { + graph(path: "g") { + edgeFilter(filter: { + property: { + name: "eprop3" + where: { endsWith: { str: "123" } } } + }) { + edges { list { src { name } dst { name } } } } + } + } """ expected_output = { "graph": { diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index f6b238df81..95128896ea 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -9,55 +9,24 @@ @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_node_property_filter_equal(graph): - query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop5" - operator: EQUAL - value: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } - } - } - ) { - nodes { - list { - name - } - } - } - } - } - """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} - run_graphql_test(query, expected_output, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_equal_no_value_error(graph): query = """ query { graph(path: "g") { nodeFilter( filter: { - property: { - name: "prop5" - operator: EQUAL + property: { + name: "prop5" + where: { eq: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } """ - expected_error_message = "Invalid filter: Operator EQUAL requires a value" - run_graphql_error_test(query, expected_error_message, graph) + expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} + run_graphql_test(query, expected_output, graph) @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) @@ -67,18 +36,13 @@ def test_graph_node_property_filter_equal_type_error(graph): graph(path: "g") { nodeFilter( filter: { - property: { - name: "prop5" - operator: EQUAL - value: { i64: 1 } + property: { + name: "prop5" + where: { eq: { i64: 1 } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -96,18 +60,13 @@ def test_graph_node_property_filter_not_equal(graph): graph(path: "g") { nodeFilter( filter: { - property: { - name: "prop4" - operator: NOT_EQUAL - value: { bool: true } + property: { + name: "prop4" + where: { ne: { bool: true } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -118,51 +77,20 @@ def test_graph_node_property_filter_not_equal(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_not_equal_no_value_error(graph): - query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop4" - operator: NOT_EQUAL - } - } - ) { - nodes { - list { - name - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator NOT_EQUAL requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_node_property_filter_not_equal_type_error(graph): query = """ query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop4" - operator: NOT_EQUAL - value: { i64: 1 } - } - } - ) { - nodes { - list { - name + filter: { + property: { + name: "prop4" + where: { ne: { i64: 1 } } } } + ) { + nodes { list { name } } } } } @@ -180,18 +108,13 @@ def test_graph_node_property_filter_greater_than_or_equal(graph): graph(path: "g") { nodeFilter( filter: { - property: { - name: "prop1" - operator: GREATER_THAN_OR_EQUAL - value: { i64: 60 } + property: { + name: "prop1" + where: { ge: { i64: 60 } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -201,52 +124,19 @@ def test_graph_node_property_filter_greater_than_or_equal(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_greater_than_or_equal_no_value_error(graph): +def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { nodeFilter( filter: { - property: { - name: "prop1" - operator: GREATER_THAN_OR_EQUAL + property: { + name: "prop1" + where: { ge: { bool: true } } } } ) { - nodes { - list { - name - } - } - } - } - } - """ - expected_error_message = ( - "Invalid filter: Operator GREATER_THAN_OR_EQUAL requires a value" - ) - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): - query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: GREATER_THAN_OR_EQUAL - value: { bool: true } - } - } - ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -264,18 +154,10 @@ def test_graph_node_property_filter_less_than_or_equal(graph): graph(path: "g") { nodeFilter( filter: { - property: { - name: "prop1" - operator: LESS_THAN_OR_EQUAL - value: { i64: 30 } - } + property: { name: "prop1", where: { le: { i64: 30 } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -291,52 +173,14 @@ def test_graph_node_property_filter_less_than_or_equal(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_less_than_or_equal_no_value_error(graph): +def test_graph_node_property_filter_less_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN_OR_EQUAL - } - } + filter: { property: { name: "prop1", where: { le: { str: "shivam" } } } } ) { - nodes { - list { - name - } - } - } - } - } - """ - expected_error_message = ( - "Invalid filter: Operator LESS_THAN_OR_EQUAL requires a value" - ) - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_less_than_or_equal_type_error(graph): - query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN_OR_EQUAL - value: { str: "shivam" } - } - } - ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -353,19 +197,9 @@ def test_graph_node_property_filter_greater_than(graph): query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: GREATER_THAN - value: { i64: 30 } - } - } + filter: { property: { name: "prop1", where: { gt: { i64: 30 } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -374,51 +208,15 @@ def test_graph_node_property_filter_greater_than(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_greater_than_no_value_error(graph): - query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: GREATER_THAN - } - } - ) { - nodes { - list { - name - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator GREATER_THAN requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_node_property_filter_greater_than_type_error(graph): query = """ query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: GREATER_THAN - value: { str: "shivam" } - } - } - ) { - nodes { - list { - name - } - } + filter: { property: { name: "prop1", where: { gt: { str: "shivam" } } } } + ) { + nodes { list { name } } } } } @@ -435,19 +233,9 @@ def test_graph_node_property_filter_less_than(graph): query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN - value: { i64: 30 } - } - } + filter: { property: { name: "prop1", where: { lt: { i64: 30 } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -458,51 +246,15 @@ def test_graph_node_property_filter_less_than(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_less_than_no_value_error(graph): - query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN - } - } - ) { - nodes { - list { - name - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator LESS_THAN requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_node_property_filter_less_than_type_error(graph): query = """ query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN - value: { str: "shivam" } - } - } - ) { - nodes { - list { - name - } - } + filter: { property: { name: "prop1", where: { lt: { str: "shivam" } } } } + ) { + nodes { list { name } } } } } @@ -519,18 +271,9 @@ def test_graph_node_property_filter_is_none(graph): query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop5" - operator: IS_NONE - } - } + filter: { property: { name: "prop5", where: { isNone: true } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -546,19 +289,10 @@ def test_graph_node_property_filter_is_some(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop5" - operator: IS_SOME - } - } - ) { - nodes { - list { - name - } - } + nodeFilter( + filter: { property: { name: "prop5", where: { isSome: true } } } + ) { + nodes { list { name } } } } } @@ -575,19 +309,9 @@ def test_graph_node_property_filter_is_in(graph): query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_IN - value: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}]} - } - } + filter: { property: { name: "prop1", where: { isIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } @@ -604,18 +328,10 @@ def test_node_property_filter_is_in_empty_list(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_IN - value: { list: []} - } - } - ) { - list { - name - } + nodeFilter( + filter: { property: { name: "prop1", where: { isIn: { list: [] } } } } + ) { + list { name } } } } @@ -627,23 +343,14 @@ def test_node_property_filter_is_in_empty_list(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_node_property_filter_is_in_no_value(graph): + # With where-shape, an empty list is a valid value (yields empty result). query = """ query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_IN - value: { list: []} - } - } - ) { - nodes { - list { - name - } - } + filter: { property: { name: "prop1", where: { isIn: { list: [] } } } } + ) { + nodes { list { name } } } } } @@ -658,25 +365,15 @@ def test_graph_node_property_filter_is_in_type_error(graph): query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_IN - value: { str: "shivam" } - } - } + filter: { property: { name: "prop1", where: { isIn: { str: "shivam" } } } } ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } """ expected_error_message = ( - "Invalid filter: Operator IS_IN requires a list value, got Str(shivam)" + "Invalid filter: isIn requires a list value, got Str(shivam)" ) run_graphql_error_test(query, expected_error_message, graph) @@ -686,20 +383,10 @@ def test_graph_node_property_filter_is_not_in_any(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_NOT_IN - value: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}]} - } - } - ) { - nodes { - list { - name - } - } + nodeFilter( + filter: { property: { name: "prop1", where: { isNotIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } + ) { + nodes { list { name } } } } } @@ -716,18 +403,10 @@ def test_node_property_filter_not_is_not_in_empty_list(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_NOT_IN - value: { list: []} - } - } + nodeFilter( + filter: { property: { name: "prop1", where: { isNotIn: { list: [] } } } } ) { - list { - name - } + list { name } } } } @@ -746,56 +425,20 @@ def test_node_property_filter_not_is_not_in_empty_list(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_is_not_in_no_value_error(graph): +def test_graph_node_property_filter_is_not_in_type_error(graph): query = """ query { graph(path: "g") { nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_NOT_IN - } - } + filter: { property: { name: "prop1", where: { isNotIn: { str: "shivam" } } } } ) { - nodes { - list { - name - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator IS_NOT_IN requires a list" - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_node_property_filter_is_not_in_type_error(graph): - query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_NOT_IN - value: { str: "shivam" } - } - } - ) { - nodes { - list { - name - } - } + nodes { list { name } } } } } """ expected_error_message = ( - "Invalid filter: Operator IS_NOT_IN requires a list value, got Str(shivam)" + "Invalid filter: isNotIn requires a list value, got Str(shivam)" ) run_graphql_error_test(query, expected_error_message, graph) @@ -807,21 +450,15 @@ def test_graph_node_not_property_filter(graph): graph(path: "g") { nodeFilter ( filter: { - not: - { - property: { - name: "prop5" - operator: EQUAL - value: { list: [ {i64: 1}, {i64: 2} ] } - } + not: { + property: { + name: "prop5" + where: { eq: { list: [ {i64: 1}, {i64: 2} ] } } } - } - ) { - nodes { - list { - name } } + ) { + nodes { list { name } } } } } @@ -845,29 +482,23 @@ def test_graph_node_type_and_property_filter(graph): graph(path: "g") { nodes { nodeFilter(filter: { - and: [{ - node: { - field: NODE_TYPE, - operator: IS_IN, - value: { - list: [ - {str: "fire_nation"}, - {str: "water_tribe"} - ] - } - } - },{ - property: { - name: "prop2", - operator: GREATER_THAN, - value: { f64:1 } + and: [ + { + node: { + field: NODE_TYPE, + where: { isIn: { list: [ {str: "fire_nation"}, {str: "water_tribe"} ] } } + } + }, + { + property: { + name: "prop2", + where: { gt: { f64: 1 } } + } } - }] + ] }) { count - list { - name - } + list { name } } } } @@ -889,23 +520,18 @@ def test_graph_node_type_and_property_filter(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_nodes_property_filter_starts_with(graph): query = """ - query { - graph(path: "g") { - nodeFilter(filter: { - property: { - name: "prop3" - operator: STARTS_WITH - value: { str: "abc" } - } - }) { - nodes { - list { - name - } - } - } + query { + graph(path: "g") { + nodeFilter(filter: { + property: { + name: "prop3" + where: { startsWith: { str: "abc" } } } + }) { + nodes { list { name } } } + } + } """ expected_output = { "graph": { @@ -922,51 +548,40 @@ def test_graph_nodes_property_filter_starts_with(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_nodes_property_filter_ends_with(graph): query = """ - query { - graph(path: "g") { - nodeFilter(filter: { - property: { - name: "prop3" - operator: ENDS_WITH - value: { str: "123" } - } - }) { - nodes { - list { - name - } - } - } + query { + graph(path: "g") { + nodeFilter(filter: { + property: { + name: "prop3" + where: { endsWith: { str: "123" } } } + }) { + nodes { list { name } } } + } + } """ expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_graph_nodes_property_filter_starts_with(graph): +def test_graph_nodes_property_filter_starts_with_temporal_any(graph): query = """ - query { - graph(path: "g") { - nodeFilter( - filter: { - temporalProperty: { - name: "prop3", - ops: [ALL], - operator: STARTS_WITH - value: { str: "abc1" } - } - } - ) { - nodes { - list { - name - } - } - } + query { + graph(path: "g") { + nodeFilter( + filter: { + temporalProperty: { + name: "prop3", + where: { any: { startsWith: { str: "abc1" } } } + } } + ) { + nodes { list { name } } } + } + } """ expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py index 8f02c924f7..678a295d6e 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py @@ -18,23 +18,19 @@ def test_out_neighbours_found(graph): { node: { field: NODE_NAME, - operator: EQUAL, - value:{ str: "d" } + where: { eq: { str: "d" } } } }, { property: { name: "prop1" - operator: GREATER_THAN - value: { i64: 10 } + where: { gt: { i64: 10 } } } } ] }) { outNeighbours { - list { - name - } + list { name } } } } @@ -56,14 +52,11 @@ def test_out_neighbours_not_found(graph): nodeFilter(filter: { node: { field: NODE_NAME, - operator: EQUAL, - value:{ str: "e" } + where: { eq: { str: "e" } } } }) { outNeighbours { - list { - name - } + list { name } } } } @@ -83,16 +76,13 @@ def test_in_neighbours_found(graph): graph(path: "g") { node(name: "d") { nodeFilter(filter: { - property: { - name: "prop1" - operator: GREATER_THAN - value: { i64: 10 } - } + property: { + name: "prop1" + where: { gt: { i64: 10 } } + } }) { inNeighbours { - list { - name - } + list { name } } } } @@ -118,14 +108,11 @@ def test_in_neighbours_not_found(graph): nodeFilter(filter: { node: { field: NODE_NAME, - operator: EQUAL, - value:{ str: "e" } + where: { eq: { str: "e" } } } }) { inNeighbours { - list { - name - } + list { name } } } } @@ -147,14 +134,11 @@ def test_neighbours_found(graph): nodeFilter(filter: { node: { field: NODE_NAME, - operator: NOT_EQUAL, - value:{ str: "a" } + where: { ne: { str: "a" } } } }) { neighbours { - list { - name - } + list { name } } } } @@ -180,14 +164,11 @@ def test_neighbours_not_found(graph): nodeFilter(filter: { node: { field: NODE_NAME, - operator: EQUAL, - value:{ str: "e" } + where: { eq: { str: "e" } } } }) { neighbours { - list { - name - } + list { name } } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index 985aafa7fe..c0e9737024 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -13,19 +13,16 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { nodeFilter( - filter: { - node: { - field: NODE_ID - operator: EQUAL - value: { str: "1" } - } + filter: { + node: { + field: NODE_ID + where: { eq: { str: "1" } } } - ) { - nodes { - list { - name - } - } + } + ) { + nodes { + list { name } + } } } } @@ -40,19 +37,16 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql2(graph): query { graph(path: "g") { nodeFilter( - filter: { - node: { - field: NODE_ID - operator: EQUAL - value: { u64: 1 } - } + filter: { + node: { + field: NODE_ID + where: { eq: { u64: 1 } } } - ) { - nodes { - list { - name - } - } + } + ) { + nodes { + list { name } + } } } } @@ -71,19 +65,16 @@ def test_filter_nodes_with_num_ids_for_node_id_eq_gql(graph): query { graph(path: "g") { nodeFilter( - filter: { - node: { - field: NODE_ID - operator: EQUAL - value: { u64: 1 } - } + filter: { + node: { + field: NODE_ID + where: { eq: { u64: 1 } } } - ) { - nodes { - list { - name - } - } + } + ) { + nodes { + list { name } + } } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index e2859e4ccd..54923bb4e9 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -18,41 +18,16 @@ def test_node_property_filter_equal(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { - property: { - name: "prop5" - operator: EQUAL - value: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } - } - } - ) { - list { - name - } - } - } - } - } - """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}]}}}} - run_graphql_test(query, expected_output, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_equal_no_value_error(graph): - query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop5" - operator: EQUAL + nodeFilter( + filter: { + property: { + name: "prop5" + where: { + eq: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } } } - ) { + } + ) { list { name } @@ -61,8 +36,8 @@ def test_node_property_filter_equal_no_value_error(graph): } } """ - expected_error_message = "Invalid filter: Operator EQUAL requires a value" - run_graphql_error_test(query, expected_error_message, graph) + expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}]}}}} + run_graphql_test(query, expected_output, graph) @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) @@ -71,15 +46,16 @@ def test_node_property_filter_equal_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { - property: { - name: "prop5" - operator: EQUAL - value: { i64: 1 } + nodeFilter( + filter: { + property: { + name: "prop5" + where: { + eq: { i64: 1 } } } - ) { + } + ) { list { name } @@ -99,16 +75,17 @@ def test_node_property_filter_not_equal(graph): query = """ query { graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop4" - operator: NOT_EQUAL - value: { bool: true } + nodes { + nodeFilter( + filter: { + property: { + name: "prop4" + where: { + ne: { bool: true } } } - ) { + } + ) { list { name } @@ -124,46 +101,21 @@ def test_node_property_filter_not_equal(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_not_equal_no_value_error(graph): +def test_node_property_filter_not_equal_type_error(graph): query = """ query { graph(path: "g") { nodes { nodeFilter( - filter: { - property: { - name: "prop4" - operator: NOT_EQUAL + filter: { + property: { + name: "prop4" + where: { + ne: { i64: 1 } } } - ) { - list { - name } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator NOT_EQUAL requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_not_equal_type_error(graph): - query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop4" - operator: NOT_EQUAL - value: { i64: 1 } - } - } - ) { + ) { list { name } @@ -186,10 +138,11 @@ def test_node_property_filter_greater_than_or_equal(graph): nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: GREATER_THAN_OR_EQUAL - value: { i64: 60 } + property: { + name: "prop1" + where: { + ge: { i64: 60 } + } } } ) { @@ -206,16 +159,18 @@ def test_node_property_filter_greater_than_or_equal(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_greater_than_or_equal_no_value_error(graph): +def test_node_property_filter_greater_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: GREATER_THAN_OR_EQUAL + property: { + name: "prop1" + where: { + ge: { bool: true } + } } } ) { @@ -227,35 +182,6 @@ def test_node_property_filter_greater_than_or_equal_no_value_error(graph): } } """ - expected_error_message = ( - "Invalid filter: Operator GREATER_THAN_OR_EQUAL requires a value" - ) - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_greater_than_or_equal_type_error(graph): - query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: GREATER_THAN_OR_EQUAL - value: { bool: true } - } - } - ) { - list { - name - } - } - } - } - } - """ expected_error_message = ( "Wrong type for property prop1: expected I64 but actual type is Bool" ) @@ -270,10 +196,11 @@ def test_node_property_filter_less_than_or_equal(graph): nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: LESS_THAN_OR_EQUAL - value: { i64: 30 } + property: { + name: "prop1" + where: { + le: { i64: 30 } + } } } ) { @@ -296,51 +223,20 @@ def test_node_property_filter_less_than_or_equal(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_less_than_or_equal_no_value_error(graph): +def test_node_property_filter_less_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: LESS_THAN_OR_EQUAL + property: { + name: "prop1" + where: { le: { str: "shivam" } } } } ) { - list { - name - } - } - } - } - } - """ - expected_error_message = ( - "Invalid filter: Operator LESS_THAN_OR_EQUAL requires a value" - ) - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_less_than_or_equal_type_error(graph): - query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN_OR_EQUAL - value: { str: "shivam" } - } - } - ) { - list { - name - } + list { name } } } } @@ -360,16 +256,13 @@ def test_node_property_filter_greater_than(graph): nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: GREATER_THAN - value: { i64: 30 } + property: { + name: "prop1" + where: { gt: { i64: 30 } } } } ) { - list { - name - } + list { name } } } } @@ -379,32 +272,6 @@ def test_node_property_filter_greater_than(graph): run_graphql_test(query, expected_output, graph) -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_greater_than_no_value_error(graph): - query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: GREATER_THAN - } - } - ) { - list { - name - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator GREATER_THAN requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_node_property_filter_greater_than_type_error(graph): query = """ @@ -413,16 +280,13 @@ def test_node_property_filter_greater_than_type_error(graph): nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: GREATER_THAN - value: { str: "shivam" } + property: { + name: "prop1" + where: { gt: { str: "shivam" } } } } - ) { - list { - name - } + ) { + list { name } } } } @@ -441,17 +305,14 @@ def test_node_property_filter_less_than(graph): graph(path: "g") { nodes { nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN - value: { i64: 30 } - } + filter: { + property: { + name: "prop1" + where: { lt: { i64: 30 } } } - ) { - list { - name } + ) { + list { name } } } } @@ -464,49 +325,20 @@ def test_node_property_filter_less_than(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_less_than_no_value_error(graph): +def test_node_property_filter_less_than_type_error(graph): query = """ query { graph(path: "g") { nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: LESS_THAN + property: { + name: "prop1" + where: { lt: { str: "shivam" } } } } ) { - list { - name - } - } - } - } - } - """ - expected_error_message = "Invalid filter: Operator LESS_THAN requires a value" - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_less_than_type_error(graph): - query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: LESS_THAN - value: { str: "shivam" } - } - } - ) { - list { - name - } + list { name } } } } @@ -526,15 +358,13 @@ def test_node_property_filter_is_none(graph): nodes { nodeFilter( filter: { - property: { - name: "prop5" - operator: IS_NONE + property: { + name: "prop5" + where: { isNone: true } } } ) { - list { - name - } + list { name } } } } @@ -553,16 +383,14 @@ def test_node_property_filter_is_some(graph): graph(path: "g") { nodes { nodeFilter( - filter: { - property: { - name: "prop5" - operator: IS_SOME - } + filter: { + property: { + name: "prop5" + where: { isSome: true } } - ) { - list { - name } + ) { + list { name } } } } @@ -582,16 +410,13 @@ def test_node_property_filter_is_in(graph): nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: IS_IN - value: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}]} + property: { + name: "prop1" + where: { isIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } ) { - list { - name - } + list { name } } } } @@ -611,16 +436,13 @@ def test_node_property_filter_is_in_empty_list(graph): nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: IS_IN - value: { list: []} + property: { + name: "prop1" + where: { isIn: { list: [] } } } } ) { - list { - name - } + list { name } } } } @@ -632,22 +454,20 @@ def test_node_property_filter_is_in_empty_list(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_node_property_filter_is_in_no_value(graph): + # Keeping semantics: value list has no matching elements query = """ query { graph(path: "g") { nodes { nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_IN - value: { list: [{i64: 100}]} - } + filter: { + property: { + name: "prop1" + where: { isIn: { list: [{i64: 100}] } } } - ) { - list { - name } + ) { + list { name } } } } @@ -665,23 +485,20 @@ def test_node_property_filter_is_in_type_error(graph): nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: IS_IN - value: { str: "shivam" } + property: { + name: "prop1" + where: { isIn: { str: "shivam" } } } } ) { - list { - name - } + list { name } } } } } """ expected_error_message = ( - "Invalid filter: Operator IS_IN requires a list value, got Str(shivam)" + "Invalid filter: isIn requires a list value, got Str(shivam)" ) run_graphql_error_test(query, expected_error_message, graph) @@ -693,17 +510,14 @@ def test_node_property_filter_is_not_in(graph): graph(path: "g") { nodes { nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_NOT_IN - value: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}]} - } + filter: { + property: { + name: "prop1" + where: { isNotIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } - ) { - list { - name } + ) { + list { name } } } } @@ -722,17 +536,14 @@ def test_node_property_filter_is_not_in_empty_list(graph): graph(path: "g") { nodes { nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_NOT_IN - value: { list: []} - } + filter: { + property: { + name: "prop1" + where: { isNotIn: { list: [] } } } - ) { - list { - name } + ) { + list { name } } } } @@ -751,109 +562,70 @@ def test_node_property_filter_is_not_in_empty_list(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_is_not_in_no_value_error(graph): +def test_node_property_filter_is_not_in_type_error(graph): query = """ query { graph(path: "g") { nodes { nodeFilter( filter: { - property: { - name: "prop1" - operator: IS_NOT_IN + property: { + name: "prop1" + where: { isNotIn: { str: "shivam" } } } } ) { - list { - name - } + list { name } } } } } """ - expected_error_message = "Invalid filter: Operator IS_NOT_IN requires a list" + expected_error_message = ( + "Invalid filter: isNotIn requires a list value, got Str(shivam)" + ) run_graphql_error_test(query, expected_error_message, graph) @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_is_not_in_type_error(graph): +def test_node_property_filter_contains_wrong_value_type_error(graph): query = """ query { graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop1" - operator: IS_NOT_IN - value: { str: "shivam" } - } - } - ) { - list { - name - } + nodeFilter(filter: { + property: { + name: "p10" + where: { contains: { u64: 2 } } + } + }) { + nodes { + list { name } } } } } """ - expected_error_message = ( - "Invalid filter: Operator IS_NOT_IN requires a list value, got Str(shivam)" - ) - run_graphql_error_test(query, expected_error_message, graph) - - -@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_contains_wrong_value_type_error(graph): - query = """ - query { - graph(path: "g") { - nodeFilter(filter: { - property: { - name: "p10" - operator: CONTAINS - value: { u64: 2 } - } - }) { - nodes { - list { - name - } - } - } - } - } - """ - expected_error_message = ( - "Invalid filter: Operator CONTAINS requires a string value, got U64(2)" - ) + expected_error_message = "Property p10 does not exist" run_graphql_error_test(query, expected_error_message, graph) @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_nodes_property_filter_starts_with(graph): query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop3" - operator: STARTS_WITH - value: { str: "abc" } - } - } - ) { - list { - name - } - } + query { + graph(path: "g") { + nodes { + nodeFilter(filter: { + property: { + name: "prop3" + where: { startsWith: { str: "abc" } } } + }) { + list { name } } } + } + } """ expected_output = { "graph": { @@ -870,25 +642,20 @@ def test_nodes_property_filter_starts_with(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_nodes_property_filter_ends_with(graph): query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - property: { - name: "prop3" - operator: ENDS_WITH - value: { str: "333" } - } - } - ) { - list { - name - } - } + query { + graph(path: "g") { + nodes { + nodeFilter(filter: { + property: { + name: "prop3" + where: { endsWith: { str: "333" } } } + }) { + list { name } } } + } + } """ expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "c"}]}}}} run_graphql_test(query, expected_output, graph) @@ -897,26 +664,20 @@ def test_nodes_property_filter_ends_with(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_nodes_property_filter_temporal_first_starts_with(graph): query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - temporalProperty: { - name: "prop3", - ops: [FIRST] - operator: STARTS_WITH - value: { str: "abc" } - } - } - ) { - list { - name - } - } + query { + graph(path: "g") { + nodes { + nodeFilter(filter: { + temporalProperty: { + name: "prop3" + where: { first: { startsWith: { str: "abc" } } } } + }) { + list { name } } } + } + } """ expected_output = { "graph": { @@ -931,28 +692,22 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_nodes_property_filter_temporal_first_starts_with(graph): +def test_nodes_property_filter_temporal_all_starts_with(graph): query = """ - query { - graph(path: "g") { - nodes { - nodeFilter( - filter: { - temporalProperty: { - name: "prop3", - ops: [ALL] - operator: STARTS_WITH - value: { str: "abc1" } - } - } - ) { - list { - name - } - } + query { + graph(path: "g") { + nodes { + nodeFilter(filter: { + temporalProperty: { + name: "prop3" + where: { any: { startsWith: { str: "abc1" } } } } + }) { + list { name } } } + } + } """ expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -960,25 +715,20 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_nodes_property_filter_list_agg(graph): + # SUM(list(prop5)) == 6 query = """ - query { - graph(path: "g") { - nodeFilter(filter: { - property: { - name: "prop5" - operator: EQUAL - ops: [SUM] - value: { i64: 6 } - } - }) { - nodes { - list { - name - } - } - } + query { + graph(path: "g") { + nodeFilter(filter: { + property: { + name: "prop5" + where: { sum: { eq: { i64: 6 } } } } + }) { + nodes { list { name } } } + } + } """ expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -987,24 +737,18 @@ def test_nodes_property_filter_list_agg(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_nodes_property_filter_list_qualifier(graph): query = """ - query { - graph(path: "g") { - nodeFilter(filter: { - property: { - name: "prop5" - operator: EQUAL - ops: [ANY] - value: { i64: 6 } - } - }) { - nodes { - list { - name - } - } - } + query { + graph(path: "g") { + nodeFilter(filter: { + property: { + name: "prop5" + where: { any: { eq: { i64: 6 } } } } + }) { + nodes { list { name } } } + } + } """ expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "c"}]}}}} run_graphql_test(query, expected_output, graph) @@ -1017,24 +761,18 @@ def test_nodes_property_filter_list_qualifier(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_nodes_temporal_property_filter_agg(graph): query = """ - query { - graph(path: "g") { - nodeFilter(filter: { - temporalProperty: { - name: "p2" - operator: LESS_THAN - ops: [AVG] - value: { f64: 10.0 } - } - }) { - nodes { - list { - name - } - } - } + query { + graph(path: "g") { + nodeFilter(filter: { + temporalProperty: { + name: "p2" + where: { avg: { lt: { f64: 10.0 } } } } + }) { + nodes { list { name } } } + } + } """ expected_output = { "graph": {"nodeFilter": {"nodes": {"list": [{"name": "2"}, {"name": "3"}]}}} @@ -1048,25 +786,20 @@ def test_nodes_temporal_property_filter_agg(graph): @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_nodes_temporal_property_filter_any_avg(graph): + # ANY timepoint where AVG(list) < 10.0 query = """ - query { - graph(path: "g") { - nodeFilter(filter: { - temporalProperty: { - name: "prop5" - operator: LESS_THAN - ops: [ANY, AVG] - value: { f64: 10.0 } - } - }) { - nodes { - list { - name - } - } - } + query { + graph(path: "g") { + nodeFilter(filter: { + temporalProperty: { + name: "prop5" + where: { any: { avg: { lt: { f64: 10.0 } } } } } + }) { + nodes { list { name } } } + } + } """ expected_output = { "graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} diff --git a/raphtory-benchmark/benches/search_bench.rs b/raphtory-benchmark/benches/search_bench.rs index 11966812d1..8d685ebe85 100644 --- a/raphtory-benchmark/benches/search_bench.rs +++ b/raphtory-benchmark/benches/search_bench.rs @@ -218,16 +218,20 @@ where match filter_op { Eq => Some(M::property(prop_name).eq(sub_str)), Ne => Some(M::property(prop_name).ne(sub_str)), - In => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - NotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), + IsNotIn => { + sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)) + } _ => None, // No numeric comparison for strings } } else { match filter_op { Eq => Some(M::property(prop_name).eq(full_str)), Ne => Some(M::property(prop_name).ne(full_str)), - In => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - NotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), + IsNotIn => { + sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)) + } _ => None, // No numeric comparison for strings } } @@ -244,8 +248,8 @@ where Le => Some(M::property(prop_name).le(v)), Gt => Some(M::property(prop_name).gt(v)), Ge => Some(M::property(prop_name).ge(v)), - In => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - NotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), _ => return None, }), PropType::I64 => prop_value.into_i64().and_then(|v| match filter_op { @@ -255,8 +259,8 @@ where Le => Some(M::property(prop_name).le(v)), Gt => Some(M::property(prop_name).gt(v)), Ge => Some(M::property(prop_name).ge(v)), - In => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - NotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), _ => return None, }), PropType::F64 => prop_value.into_f64().and_then(|v| match filter_op { @@ -266,15 +270,15 @@ where Le => Some(M::property(prop_name).le(v)), Gt => Some(M::property(prop_name).gt(v)), Ge => Some(M::property(prop_name).ge(v)), - In => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - NotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), _ => return None, }), PropType::Bool => prop_value.into_bool().and_then(|v| match filter_op { Eq => Some(M::property(prop_name).eq(v)), Ne => Some(M::property(prop_name).ne(v)), - In => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - NotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), _ => return None, }), @@ -326,7 +330,7 @@ fn pick_node_property_filter( }?; let sampled_values = match filter_op { - In | NotIn => Some(get_node_property_samples(graph, prop_id, is_const)), + IsIn | IsNotIn => Some(get_node_property_samples(graph, prop_id, is_const)), _ => None, }; @@ -436,7 +440,7 @@ fn pick_edge_property_filter( }?; let sampled_values = match filter_op { - In | NotIn => Some(get_edge_property_samples(graph, prop_id, is_const)), + IsIn | IsNotIn => Some(get_edge_property_samples(graph, prop_id, is_const)), _ => None, }; @@ -559,8 +563,8 @@ macro_rules! bench_search_nodes_by_property_filter { "lt" => FilterOperator::Lt, "ge" => FilterOperator::Ge, "gt" => FilterOperator::Gt, - "in" => FilterOperator::In, - "not_in" => FilterOperator::NotIn, + "is_in" => FilterOperator::IsIn, + "is_not_in" => FilterOperator::IsNotIn, _ => panic!("Unknown filter type in function name"), }; bench_search_nodes_by_property_filter::( @@ -629,8 +633,8 @@ macro_rules! bench_search_edges_by_property_filter { "lt" => FilterOperator::Lt, "ge" => FilterOperator::Ge, "gt" => FilterOperator::Gt, - "in" => FilterOperator::In, - "not_in" => FilterOperator::NotIn, + "is_in" => FilterOperator::IsIn, + "is_not_in" => FilterOperator::IsNotIn, _ => panic!("Unknown filter type in function name"), }; bench_search_edges_by_property_filter::( diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index d02a95e9f6..b3cc6487ee 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -314,37 +314,13 @@ input EdgeAddition { } input EdgeFilter @oneOf { - """ - Source node. - """ - src: NodeFieldFilter - """ - Destination node. - """ - dst: NodeFieldFilter - """ - Property. - """ - property: PropertyFilterExpr - """ - Metadata. - """ - metadata: MetadataFilterExpr - """ - Temporal property. - """ - temporalProperty: TemporalPropertyFilterExpr - """ - AND operator. - """ + src: NodeFieldFilterNew + dst: NodeFieldFilterNew + property: PropertyFilterNew + metadata: PropertyFilterNew + temporalProperty: PropertyFilterNew and: [EdgeFilter!] - """ - OR operator. - """ or: [EdgeFilter!] - """ - NOT operator. - """ not: EdgeFilter } @@ -391,65 +367,20 @@ input EdgeSortBy { } input EdgeViewCollection @oneOf { - """ - Contains only the default layer. - """ defaultLayer: Boolean - """ - Latest time. - """ latest: Boolean - """ - Snapshot at latest time. - """ snapshotLatest: Boolean - """ - Snapshot at specified time. - """ snapshotAt: Int - """ - List of included layers. - """ layers: [String!] - """ - List of excluded layers. - """ excludeLayers: [String!] - """ - Single included layer. - """ layer: String - """ - Single excluded layer. - """ excludeLayer: String - """ - Window between a start and end time. - """ window: Window - """ - View at a specified time. - """ at: Int - """ - View before a specified time (end exclusive). - """ before: Int - """ - View after a specified time (start exclusive). - """ after: Int - """ - Shrink a Window to a specified start and end time. - """ shrinkWindow: Window - """ - Set the window start to a specified time. - """ shrinkStart: Int - """ - Set the window end to a specified time. - """ shrinkEnd: Int } @@ -581,65 +512,20 @@ type Edges { } input EdgesViewCollection @oneOf { - """ - Contains only the default layer. - """ defaultLayer: Boolean - """ - Latest time. - """ latest: Boolean - """ - Snapshot at latest time. - """ snapshotLatest: Boolean - """ - Snapshot at specified time. - """ snapshotAt: Int - """ - List of included layers. - """ layers: [String!] - """ - List of excluded layers. - """ excludeLayers: [String!] - """ - Single included layer. - """ layer: String - """ - Single excluded layer. - """ excludeLayer: String - """ - Window between a start and end time. - """ window: Window - """ - View at a specified time. - """ at: Int - """ - View before a specified time (end exclusive). - """ before: Int - """ - View after a specified time (start exclusive). - """ after: Int - """ - Shrink a Window to a specified start and end time. - """ shrinkWindow: Window - """ - Set the window start to a specified time. - """ shrinkStart: Int - """ - Set the window end to a specified time. - """ shrinkEnd: Int } @@ -897,89 +783,26 @@ enum GraphType { } input GraphViewCollection @oneOf { - """ - Contains only the default layer. - """ defaultLayer: Boolean - """ - List of included layers. - """ layers: [String!] - """ - List of excluded layers. - """ excludeLayers: [String!] - """ - Single included layer. - """ layer: String - """ - Single excluded layer. - """ excludeLayer: String - """ - Subgraph nodes. - """ subgraph: [String!] - """ - Subgraph node types. - """ subgraphNodeTypes: [String!] - """ - List of excluded nodes. - """ excludeNodes: [String!] - """ - Valid state. - """ valid: Boolean - """ - Window between a start and end time. - """ window: Window - """ - View at a specified time. - """ at: Int - """ - View at the latest time. - """ latest: Boolean - """ - Snapshot at specified time. - """ snapshotAt: Int - """ - Snapshot at latest time. - """ snapshotLatest: Boolean - """ - View before a specified time (end exclusive). - """ before: Int - """ - View after a specified time (start exclusive). - """ after: Int - """ - Shrink a Window to a specified start and end time. - """ shrinkWindow: Window - """ - Set the window start to a specified time. - """ shrinkStart: Int - """ - Set the window end to a specified time. - """ shrinkEnd: Int - """ - Node filter. - """ nodeFilter: NodeFilter - """ - Edge filter. - """ edgeFilter: EdgeFilter } @@ -1107,25 +930,6 @@ type Metadata { values(keys: [String!]): [Property!]! } -input MetadataFilterExpr { - """ - Node metadata to compare against. - """ - name: String! - """ - Operator. - """ - operator: Operator! - """ - Value. - """ - value: Value - """ - Ops List. - """ - ops: [OpName!] -} - type MutRoot { """ Returns a collection of mutation plugins. @@ -1508,63 +1312,38 @@ input NodeAddition { } enum NodeField { - """ - Node id. - """ NODE_ID - """ - Node name. - """ NODE_NAME - """ - Node type. - """ NODE_TYPE } -input NodeFieldFilter { - """ - Node component to compare against. - """ +input NodeFieldCondition @oneOf { + eq: Value + ne: Value + gt: Value + ge: Value + lt: Value + le: Value + startsWith: Value + endsWith: Value + contains: Value + notContains: Value + isIn: Value + isNotIn: Value +} + +input NodeFieldFilterNew { field: NodeField! - """ - Operator filter. - """ - operator: Operator! - """ - Value filter. - """ - value: Value! + where: NodeFieldCondition! } input NodeFilter @oneOf { - """ - Node filter. - """ - node: NodeFieldFilter - """ - Property filter. - """ - property: PropertyFilterExpr - """ - Metadata filter. - """ - metadata: MetadataFilterExpr - """ - Temporal property filter. - """ - temporalProperty: TemporalPropertyFilterExpr - """ - AND operator. - """ + node: NodeFieldFilterNew + property: PropertyFilterNew + metadata: PropertyFilterNew + temporalProperty: PropertyFilterNew and: [NodeFilter!] - """ - OR operator. - """ or: [NodeFilter!] - """ - NOT operator. - """ not: NodeFilter } @@ -1597,69 +1376,21 @@ input NodeSortBy { } input NodeViewCollection @oneOf { - """ - Contains only the default layer. - """ defaultLayer: Boolean - """ - View at the latest time. - """ latest: Boolean - """ - Snapshot at latest time. - """ snapshotLatest: Boolean - """ - Snapshot at specified time. - """ snapshotAt: Int - """ - List of included layers. - """ layers: [String!] - """ - List of excluded layers. - """ excludeLayers: [String!] - """ - Single included layer. - """ layer: String - """ - Single excluded layer. - """ excludeLayer: String - """ - Window between a start and end time. - """ window: Window - """ - View at a specified time. - """ at: Int - """ - View before a specified time (end exclusive). - """ before: Int - """ - View after a specified time (start exclusive). - """ after: Int - """ - Shrink a Window to a specified start and end time. - """ shrinkWindow: Window - """ - Set the window start to a specified time. - """ shrinkStart: Int - """ - Set the window end to a specified time. - """ shrinkEnd: Int - """ - Node filter. - """ nodeFilter: NodeFilter } @@ -1780,73 +1511,22 @@ type Nodes { } input NodesViewCollection @oneOf { - """ - Contains only the default layer. - """ defaultLayer: Boolean - """ - View at the latest time. - """ latest: Boolean - """ - Snapshot at latest time. - """ snapshotLatest: Boolean - """ - List of included layers. - """ layers: [String!] - """ - List of excluded layers. - """ excludeLayers: [String!] - """ - Single included layer. - """ layer: String - """ - Single excluded layer. - """ excludeLayer: String - """ - Window between a start and end time. - """ window: Window - """ - View at a specified time. - """ at: Int - """ - Snapshot at specified time. - """ snapshotAt: Int - """ - View before a specified time (end exclusive). - """ before: Int - """ - View after a specified time (start exclusive). - """ after: Int - """ - Shrink a Window to a specified start and end time. - """ shrinkWindow: Window - """ - Set the window start to a specified time. - """ shrinkStart: Int - """ - Set the window end to a specified time. - """ shrinkEnd: Int - """ - Node filter. - """ nodeFilter: NodeFilter - """ - List of types. - """ typeFilter: [String!] } @@ -1874,71 +1554,6 @@ input ObjectEntry { value: Value! } -enum OpName { - FIRST - LAST - ANY - ALL - LEN - SUM - AVG - MIN - MAX -} - -enum Operator { - """ - Equality operator. - """ - EQUAL - """ - Inequality operator. - """ - NOT_EQUAL - """ - Greater Than Or Equal operator. - """ - GREATER_THAN_OR_EQUAL - """ - Less Than Or Equal operator. - """ - LESS_THAN_OR_EQUAL - """ - Greater Than operator. - """ - GREATER_THAN - """ - Less Than operator. - """ - LESS_THAN - """ - Is None operator. - """ - IS_NONE - """ - Is Some operator. - """ - IS_SOME - """ - Is In operator. - """ - IS_IN - """ - Is Not In operator. - """ - IS_NOT_IN - STARTS_WITH - ENDS_WITH - """ - Contains operator. - """ - CONTAINS - """ - Not Contains operator. - """ - NOT_CONTAINS -} - """ PageRank score. """ @@ -2045,61 +1660,19 @@ type PathFromNode { } input PathFromNodeViewCollection @oneOf { - """ - Latest time. - """ latest: Boolean - """ - Latest snapshot. - """ snapshotLatest: Boolean - """ - Time. - """ snapshotAt: Int - """ - List of layers. - """ layers: [String!] - """ - List of excluded layers. - """ excludeLayers: [String!] - """ - Single layer. - """ layer: String - """ - Single layer to exclude. - """ excludeLayer: String - """ - Window between a start and end time. - """ window: Window - """ - View at a specified time. - """ at: Int - """ - View before a specified time (end exclusive). - """ before: Int - """ - View after a specified time (start exclusive). - """ after: Int - """ - Shrink a Window to a specified start and end time. - """ shrinkWindow: Window - """ - Set the window start to a specified time. - """ shrinkStart: Int - """ - Set the window end to a specified time. - """ shrinkEnd: Int } @@ -2116,6 +1689,36 @@ type PathFromNodeWindowSet { list: [PathFromNode!]! } +input PropCondition @oneOf { + eq: Value + ne: Value + gt: Value + ge: Value + lt: Value + le: Value + startsWith: Value + endsWith: Value + contains: Value + notContains: Value + isIn: Value + isNotIn: Value + isSome: Boolean + isNone: Boolean + between: [Value!] + and: [PropCondition!] + or: [PropCondition!] + not: PropCondition + first: PropCondition + last: PropCondition + any: PropCondition + all: PropCondition + sum: PropCondition + avg: PropCondition + min: PropCondition + max: PropCondition + len: PropCondition +} + type Properties { """ Get property value matching the specified key. @@ -2142,23 +1745,9 @@ type Property { value: PropertyOutput! } -input PropertyFilterExpr { - """ - Node property to compare against. - """ +input PropertyFilterNew { name: String! - """ - Operator. - """ - operator: Operator! - """ - Value. - """ - value: Value - """ - Ops List. - """ - ops: [OpName!] + where: PropCondition! } input PropertyInput { @@ -2316,25 +1905,6 @@ type TemporalProperty { orderedDedupe(latestTime: Boolean!): [PropertyTuple!]! } -input TemporalPropertyFilterExpr { - """ - Name. - """ - name: String! - """ - Operator. - """ - operator: Operator! - """ - Value. - """ - value: Value - """ - Ops List. - """ - ops: [OpName!] -} - input TemporalPropertyInput { """ Time. diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 6e859f7e82..3b51e5c336 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -26,6 +26,14 @@ use std::{ sync::Arc, }; +#[derive(InputObject, Clone, Debug)] +pub struct Window { + /// Window start time. + pub start: i64, + /// Window end time. + pub end: i64, +} + #[derive(OneOfInput, Clone, Debug)] pub enum GraphViewCollection { /// Contains only the default layer. @@ -246,76 +254,155 @@ pub enum PathFromNodeViewCollection { ShrinkEnd(i64), } -#[derive(InputObject, Clone, Debug)] -pub struct Window { - /// Window start time. - pub start: i64, - /// Window end time. - pub end: i64, +#[derive(Enum, Copy, Clone, Debug)] +pub enum NodeField { + /// Node id. + NodeId, + /// Node name. + NodeName, + /// Node type. + NodeType, } -#[derive(Enum, Copy, Clone, Debug)] -pub enum Operator { - /// Equality operator. - Equal, - /// Inequality operator. - NotEqual, - /// Greater Than Or Equal operator. - GreaterThanOrEqual, - /// Less Than Or Equal operator. - LessThanOrEqual, - /// Greater Than operator. - GreaterThan, - /// Less Than operator. - LessThan, - /// Is None operator. - IsNone, - /// Is Some operator. - IsSome, - /// Is In operator. - IsIn, - /// Is Not In operator. - IsNotIn, - StartsWith, - EndsWith, - /// Contains operator. - Contains, - /// Not Contains operator. - NotContains, -} - -impl Display for Operator { +impl Display for NodeField { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let op_str = match self { - Operator::Equal => "EQUAL", - Operator::NotEqual => "NOT_EQUAL", - Operator::GreaterThanOrEqual => "GREATER_THAN_OR_EQUAL", - Operator::LessThanOrEqual => "LESS_THAN_OR_EQUAL", - Operator::GreaterThan => "GREATER_THAN", - Operator::LessThan => "LESS_THAN", - Operator::IsNone => "IS_NONE", - Operator::IsSome => "IS_SOME", - Operator::IsIn => "IS_IN", - Operator::IsNotIn => "IS_NOT_IN", - Operator::StartsWith => "STARTS_WITH", - Operator::EndsWith => "ENDS_WITH", - Operator::Contains => "CONTAINS", - Operator::NotContains => "NOT_CONTAINS", - }; - write!(f, "{op_str}") + write!( + f, + "{}", + match self { + NodeField::NodeId => "node_id", + NodeField::NodeName => "node_name", + NodeField::NodeType => "node_type", + } + ) } } +#[derive(InputObject, Clone, Debug)] +pub struct PropertyFilterNew { + pub name: String, + #[graphql(name = "where")] + pub where_: PropCondition, +} + +#[derive(OneOfInput, Clone, Debug)] +pub enum PropCondition { + Eq(Value), + Ne(Value), + Gt(Value), + Ge(Value), + Lt(Value), + Le(Value), + + StartsWith(Value), + EndsWith(Value), + Contains(Value), + NotContains(Value), + + IsIn(Value), + IsNotIn(Value), + + IsSome(bool), + IsNone(bool), + + And(Vec), + Or(Vec), + Not(Wrapped), + + First(Wrapped), + Last(Wrapped), + Any(Wrapped), + All(Wrapped), + Sum(Wrapped), + Avg(Wrapped), + Min(Wrapped), + Max(Wrapped), + Len(Wrapped), +} + +impl PropCondition { + pub fn op_name(&self) -> Option<&'static str> { + use PropCondition::*; + Some(match self { + Eq(_) => "eq", + Ne(_) => "ne", + Gt(_) => "gt", + Ge(_) => "ge", + Lt(_) => "lt", + Le(_) => "le", + + StartsWith(_) => "startsWith", + EndsWith(_) => "endsWith", + Contains(_) => "contains", + NotContains(_) => "notContains", + + IsIn(_) => "isIn", + IsNotIn(_) => "isNotIn", + + IsSome(_) => "isSome", + IsNone(_) => "isNone", + + And(_) | Or(_) | Not(_) | First(_) | Last(_) | Any(_) | All(_) | Sum(_) | Avg(_) + | Min(_) | Max(_) | Len(_) => return None, + }) + } +} + +#[derive(OneOfInput, Clone, Debug)] +pub enum NodeFieldCondition { + Eq(Value), + Ne(Value), + Gt(Value), + Ge(Value), + Lt(Value), + Le(Value), + + StartsWith(Value), + EndsWith(Value), + Contains(Value), + NotContains(Value), + + IsIn(Value), + IsNotIn(Value), +} + +impl NodeFieldCondition { + pub fn op_name(&self) -> Option<&'static str> { + use NodeFieldCondition::*; + Some(match self { + Eq(_) => "eq", + Ne(_) => "ne", + Gt(_) => "gt", + Ge(_) => "ge", + Lt(_) => "lt", + Le(_) => "le", + StartsWith(_) => "startsWith", + EndsWith(_) => "endsWith", + Contains(_) => "contains", + NotContains(_) => "notContains", + IsIn(_) => "isIn", + IsNotIn(_) => "isNotIn", + }) + } +} + +#[derive(InputObject, Clone, Debug)] +pub struct NodeFieldFilterNew { + pub field: NodeField, + #[graphql(name = "where")] + pub where_: NodeFieldCondition, +} + #[derive(OneOfInput, Clone, Debug)] pub enum NodeFilter { /// Node filter. - Node(NodeFieldFilter), + Node(NodeFieldFilterNew), /// Property filter. - Property(PropertyFilterExpr), + Property(PropertyFilterNew), /// Metadata filter. - Metadata(MetadataFilterExpr), + Metadata(PropertyFilterNew), /// Temporal property filter. - TemporalProperty(TemporalPropertyFilterExpr), + TemporalProperty(PropertyFilterNew), /// AND operator. And(Vec), /// OR operator. @@ -324,12 +411,30 @@ pub enum NodeFilter { Not(Wrapped), } +#[derive(OneOfInput, Clone, Debug)] +pub enum EdgeFilter { + /// Source node filter. + Src(NodeFieldFilterNew), + /// Destination node filter. + Dst(NodeFieldFilterNew), + /// Property filter. + Property(PropertyFilterNew), + /// Metadata filter. + Metadata(PropertyFilterNew), + /// Temporal property filter. + TemporalProperty(PropertyFilterNew), + /// AND operator. + And(Vec), + /// OR operator. + Or(Vec), + /// NOT operator. + Not(Wrapped), +} + #[derive(Clone, Debug)] pub struct Wrapped(Box); - impl Deref for Wrapped { type Target = T; - fn deref(&self) -> &Self::Target { self.0.deref() } @@ -343,10 +448,9 @@ impl Register for Wrapped { impl FromValue for Wrapped { fn from_value(value: async_graphql::Result) -> InputValueResult { - match T::from_value(value) { - Ok(value) => Ok(Wrapped(Box::new(value))), - Err(err) => Err(err.propagate()), - } + T::from_value(value) + .map(|v| Wrapped(Box::new(v))) + .map_err(|e| e.propagate()) } } @@ -355,379 +459,491 @@ impl TypeName for Wrapped { T::get_type_name() } } - impl InputTypeName for Wrapped {} -#[derive(InputObject, Clone, Debug)] -pub struct NodeFieldFilter { - /// Node component to compare against. - pub field: NodeField, - /// Operator filter. - pub operator: Operator, - /// Value filter. - pub value: Value, -} +fn peel_prop_wrappers_and_collect_ops<'a>( + cond: &'a PropCondition, + ops: &mut Vec, +) -> Option<&'a PropCondition> { + use PropCondition::*; -impl NodeFieldFilter { - pub fn validate(&self) -> Result<(), GraphError> { - match self.field { - NodeField::NodeId => validate_id_operator_value_pair(self.operator, &self.value), - _ => validate_operator_value_pair(self.operator, Some(&self.value)), + match cond { + First(inner) => { + ops.push(Op::First); + Some(inner.deref()) + } + Last(inner) => { + ops.push(Op::Last); + Some(inner.deref()) + } + Any(inner) => { + ops.push(Op::Any); + Some(inner.deref()) + } + All(inner) => { + ops.push(Op::All); + Some(inner.deref()) + } + Sum(inner) => { + ops.push(Op::Sum); + Some(inner.deref()) + } + Avg(inner) => { + ops.push(Op::Avg); + Some(inner.deref()) + } + Min(inner) => { + ops.push(Op::Min); + Some(inner.deref()) + } + Max(inner) => { + ops.push(Op::Max); + Some(inner.deref()) + } + Len(inner) => { + ops.push(Op::Len); + Some(inner.deref()) } - } -} - -#[derive(Enum, Copy, Clone, Debug)] -pub enum NodeField { - /// Node id. - NodeId, - /// Node name. - NodeName, - /// Node type. - NodeType, -} -#[derive(OneOfInput, Clone, Debug)] -pub enum EdgeFilter { - /// Source node. - Src(NodeFieldFilter), - /// Destination node. - Dst(NodeFieldFilter), - /// Property. - Property(PropertyFilterExpr), - /// Metadata. - Metadata(MetadataFilterExpr), - /// Temporal property. - TemporalProperty(TemporalPropertyFilterExpr), - /// AND operator. - And(Vec), - /// OR operator. - Or(Vec), - /// NOT operator. - Not(Wrapped), + _ => None, + } } -#[derive(Enum, Debug, Clone, Copy, PartialEq, Eq)] -#[graphql(name = "ListAgg")] -pub enum GqlListAgg { - Len, - Sum, - Avg, - Min, - Max, +fn require_string_value(op: Option<&str>, v: &Value) -> Result { + if let Value::Str(s) = v { + Ok(s.clone()) + } else { + let name = op.unwrap_or("UNKNOWN"); + Err(GraphError::InvalidGqlFilter(format!( + "{name} requires a string value, got {v}" + ))) + } } -#[derive(Enum, Debug, Clone, Copy, PartialEq, Eq)] -#[graphql(name = "ListElemQualifier")] -pub enum GqlListElemQualifier { - Any, - All, +fn require_prop_list_value(op: Option<&str>, v: &Value) -> Result { + if let Value::List(vs) = v { + let props = vs + .iter() + .cloned() + .map(Prop::try_from) + .collect::, _>>()?; + Ok(PropertyFilterValue::Set(Arc::new( + props.into_iter().collect(), + ))) + } else { + let name = op.unwrap_or("UNKNOWN"); + Err(GraphError::InvalidGqlFilter(format!( + "{name} requires a list value, got {v}" + ))) + } } -impl From for Op { - fn from(q: GqlListElemQualifier) -> Self { - match q { - GqlListElemQualifier::Any => Op::Any, - GqlListElemQualifier::All => Op::All, - } +fn require_u64_value(op: Option<&str>, v: &Value) -> Result { + if let Value::U64(i) = v { + Ok(*i) + } else { + let name = op.unwrap_or("UNKNOWN"); + Err(GraphError::InvalidGqlFilter(format!( + "{name} requires a u64 value, got {v}" + ))) } } -impl From for Op { - fn from(a: GqlListAgg) -> Self { - match a { - GqlListAgg::Len => Op::Len, - GqlListAgg::Sum => Op::Sum, - GqlListAgg::Avg => Op::Avg, - GqlListAgg::Min => Op::Min, - GqlListAgg::Max => Op::Max, +fn parse_node_id_scalar(op: Option<&str>, v: &Value) -> Result { + match v { + Value::U64(i) => Ok(FilterValue::ID(GID::U64(*i))), + Value::Str(s) => Ok(FilterValue::ID(GID::Str(s.clone()))), + other => { + let name = op.unwrap_or("UNKNOWN"); + Err(GraphError::InvalidGqlFilter(format!( + "{name} requires int or str, got {other}" + ))) } } } -#[derive(InputObject, Clone, Debug)] -pub struct PropertyFilterExpr { - /// Node property to compare against. - pub name: String, - /// Operator. - pub operator: Operator, - /// Value. - pub value: Option, - /// Ops List. - pub ops: Option>, -} +fn parse_node_id_list(op: Option<&str>, v: &Value) -> Result { + let name = op.unwrap_or("UNKNOWN"); + let Value::List(vs) = v else { + return Err(GraphError::InvalidGqlFilter(format!( + "{name} requires a list value, got {v}" + ))); + }; -impl PropertyFilterExpr { - pub fn validate(&self) -> Result<(), GraphError> { - validate_operator_value_pair(self.operator, self.value.as_ref()) + let all_u64 = vs.iter().all(|v| matches!(v, Value::U64(_))); + let all_str = vs.iter().all(|v| matches!(v, Value::Str(_))); + if !(all_u64 || all_str) { + return Err(GraphError::InvalidGqlFilter(format!( + "{name} requires a homogeneous list of ints or strings" + ))); } -} - -#[derive(InputObject, Clone, Debug)] -pub struct MetadataFilterExpr { - /// Node metadata to compare against. - pub name: String, - /// Operator. - pub operator: Operator, - /// Value. - pub value: Option, - /// Ops List. - pub ops: Option>, -} -impl MetadataFilterExpr { - pub fn validate(&self) -> Result<(), GraphError> { - validate_operator_value_pair(self.operator, self.value.as_ref()) + let mut set = std::collections::HashSet::with_capacity(vs.len()); + if all_u64 { + for v in vs { + if let Value::U64(i) = v { + set.insert(GID::U64(*i)); + } + } + } else { + for v in vs { + if let Value::Str(s) = v { + set.insert(GID::Str(s.clone())); + } + } } + Ok(FilterValue::IDSet(Arc::new(set))) } -#[derive(InputObject, Clone, Debug)] -pub struct TemporalPropertyFilterExpr { - /// Name. - pub name: String, - /// Operator. - pub operator: Operator, - /// Value. - pub value: Option, - /// Ops List. - pub ops: Option>, -} +fn parse_string_list(op: Option<&str>, v: &Value) -> Result { + let name = op.unwrap_or("UNKNOWN"); + let Value::List(vs) = v else { + return Err(GraphError::InvalidGqlFilter(format!( + "{name} requires a list value, got {v}" + ))); + }; -impl TemporalPropertyFilterExpr { - pub fn validate(&self) -> Result<(), GraphError> { - validate_operator_value_pair(self.operator, self.value.as_ref()) - } + let strings = vs + .iter() + .map(|v| { + if let Value::Str(s) = v { + Ok(s.clone()) + } else { + Err(GraphError::InvalidGqlFilter(format!( + "Expected list of strings for {name}, got {v}" + ))) + } + }) + .collect::, _>>()?; + + Ok(FilterValue::Set(Arc::new(strings.into_iter().collect()))) } -#[derive(Enum, Copy, Clone, Debug)] -pub enum OpName { - First, - Last, - Any, - All, - Len, - Sum, - Avg, - Min, - Max, -} - -impl From for Op { - fn from(o: OpName) -> Self { - match o { - OpName::First => Op::First, - OpName::Last => Op::Last, - OpName::Any => Op::Any, - OpName::All => Op::All, - OpName::Len => Op::Len, - OpName::Sum => Op::Sum, - OpName::Avg => Op::Avg, - OpName::Min => Op::Min, - OpName::Max => Op::Max, - } - } +fn translate_node_field_where( + field: NodeField, + cond: &NodeFieldCondition, +) -> Result<(String, FilterValue, FilterOperator), GraphError> { + use FilterOperator as FO; + use NodeField::*; + use NodeFieldCondition::*; + + let field_name = field.to_string(); + let op = cond.op_name(); + + Ok(match (field, cond) { + (NodeId, Eq(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Eq), + (NodeId, Ne(v)) => (field_name, parse_node_id_scalar(op, v)?, FO::Ne), + (NodeId, Gt(v)) => ( + field_name, + FilterValue::ID(GID::U64(require_u64_value(op, v)?)), + FO::Gt, + ), + (NodeId, Ge(v)) => ( + field_name, + FilterValue::ID(GID::U64(require_u64_value(op, v)?)), + FO::Ge, + ), + (NodeId, Lt(v)) => ( + field_name, + FilterValue::ID(GID::U64(require_u64_value(op, v)?)), + FO::Lt, + ), + (NodeId, Le(v)) => ( + field_name, + FilterValue::ID(GID::U64(require_u64_value(op, v)?)), + FO::Le, + ), + + (NodeId, StartsWith(v)) => ( + field_name, + FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FO::StartsWith, + ), + (NodeId, EndsWith(v)) => ( + field_name, + FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FO::EndsWith, + ), + (NodeId, Contains(v)) => ( + field_name, + FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FO::Contains, + ), + (NodeId, NotContains(v)) => ( + field_name, + FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FO::NotContains, + ), + + (NodeId, IsIn(v)) => (field_name, parse_node_id_list(op, v)?, FO::IsIn), + (NodeId, IsNotIn(v)) => (field_name, parse_node_id_list(op, v)?, FO::IsNotIn), + + (NodeName, Eq(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Eq, + ), + (NodeName, Ne(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Ne, + ), + (NodeName, Gt(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Gt, + ), + (NodeName, Ge(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Ge, + ), + (NodeName, Lt(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Lt, + ), + (NodeName, Le(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Le, + ), + + (NodeName, StartsWith(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::StartsWith, + ), + (NodeName, EndsWith(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::EndsWith, + ), + (NodeName, Contains(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Contains, + ), + (NodeName, NotContains(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::NotContains, + ), + + (NodeName, IsIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsIn), + (NodeName, IsNotIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsNotIn), + + (NodeType, Eq(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Eq, + ), + (NodeType, Ne(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Ne, + ), + (NodeType, Gt(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Gt, + ), + (NodeType, Ge(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Ge, + ), + (NodeType, Lt(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Lt, + ), + (NodeType, Le(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Le, + ), + + (NodeType, StartsWith(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::StartsWith, + ), + (NodeType, EndsWith(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::EndsWith, + ), + (NodeType, Contains(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::Contains, + ), + (NodeType, NotContains(v)) => ( + field_name, + FilterValue::Single(require_string_value(op, v)?), + FO::NotContains, + ), + + (NodeType, IsIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsIn), + (NodeType, IsNotIn(v)) => (field_name, parse_string_list(op, v)?, FO::IsNotIn), + }) } -fn field_value(value: Value, operator: Operator) -> Result { - let prop = Prop::try_from(value.clone())?; - match (prop, operator) { - (Prop::List(list), Operator::IsIn | Operator::IsNotIn) => { - let strings: Vec = list - .iter() - .map(|p| match p { - Prop::Str(s) => Ok(s.to_string()), - _ => Err(GraphError::InvalidGqlFilter(format!( - "Invalid field value {:?} or operator {}", - value, operator - ))), - }) - .collect::>()?; - - Ok(FilterValue::Set(Arc::new( - strings.iter().cloned().collect(), - ))) +fn translate_prop_leaf_to_filter( + name_for_errors: &str, + cmp: &PropCondition, +) -> Result<(FilterOperator, PropertyFilterValue), GraphError> { + use FilterOperator as FO; + use PropCondition::*; + + let single = |v: &Value| -> Result { + Ok(PropertyFilterValue::Single(Prop::try_from(v.clone())?)) + }; + + Ok(match cmp { + Eq(v) => (FO::Eq, single(v)?), + Ne(v) => (FO::Ne, single(v)?), + Gt(v) => (FO::Gt, single(v)?), + Ge(v) => (FO::Ge, single(v)?), + Lt(v) => (FO::Lt, single(v)?), + Le(v) => (FO::Le, single(v)?), + + StartsWith(v) => ( + FO::StartsWith, + PropertyFilterValue::Single(Prop::Str(require_string_value(cmp.op_name(), v)?.into())), + ), + EndsWith(v) => ( + FO::EndsWith, + PropertyFilterValue::Single(Prop::Str(require_string_value(cmp.op_name(), v)?.into())), + ), + + Contains(v) => (FO::Contains, single(v)?), + NotContains(v) => (FO::NotContains, single(v)?), + + IsIn(v) => (FO::IsIn, require_prop_list_value(cmp.op_name(), v)?), + IsNotIn(v) => (FO::IsNotIn, require_prop_list_value(cmp.op_name(), v)?), + + IsSome(true) => (FO::IsSome, PropertyFilterValue::None), + IsNone(true) => (FO::IsNone, PropertyFilterValue::None), + + And(_) | Or(_) | Not(_) | First(_) | Last(_) | Any(_) | All(_) | Sum(_) + | Avg(_) | Min(_) | Max(_) | Len(_) | IsSome(false) | IsNone(false) => { + return Err(GraphError::InvalidGqlFilter(format!( + "Expected comparison at leaf for {}", + name_for_errors + ))); } - (Prop::Str(p), _) => Ok(FilterValue::Single(p.to_string())), - _ => Err(GraphError::InvalidGqlFilter(format!( - "Invalid field value {:?} or operator {}", - value, operator - ))), - } + }) } -fn node_field_value( - field: NodeField, - value: Value, - operator: Operator, -) -> Result { - match field { - NodeField::NodeId => id_field_value(value, operator), - NodeField::NodeName | NodeField::NodeType => string_field_value(value, operator), +fn build_property_filter_from_condition( + prop_ref: PropertyRef, + cond: &PropCondition, +) -> Result, GraphError> { + let mut ops: Vec = Vec::new(); + let mut cursor = cond; + while let Some(inner) = peel_prop_wrappers_and_collect_ops(cursor, &mut ops) { + cursor = inner; } + let (operator, prop_value) = translate_prop_leaf_to_filter(prop_ref.name(), cursor)?; + Ok(PropertyFilter { + prop_ref, + prop_value, + operator, + ops, + _phantom: PhantomData, + }) } -fn id_field_value(value: Value, operator: Operator) -> Result { - use Operator::*; - - match operator { - Equal | NotEqual => match value { - Value::U64(i) => { - let u = i - .try_into() - .map_err(|_| GraphError::InvalidGqlFilter("node_id must be >= 0".into()))?; - Ok(FilterValue::ID(GID::U64(u))) - } - Value::Str(s) => Ok(FilterValue::ID(GID::Str(s))), - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires int or string, got {v}" - ))), - }, - - GreaterThan | GreaterThanOrEqual | LessThan | LessThanOrEqual => match value { - Value::U64(i) => { - let u = i - .try_into() - .map_err(|_| GraphError::InvalidGqlFilter("node_id must be >= 0".into()))?; - Ok(FilterValue::ID(GID::U64(u))) +fn build_node_filter_from_prop_condition( + prop_ref: PropertyRef, + cond: &PropCondition, +) -> Result { + use PropCondition::*; + + match cond { + And(list) => { + let mut it = list.iter(); + let first = it + .next() + .ok_or_else(|| GraphError::InvalidGqlFilter("and expects non-empty list".into()))?; + let mut acc = build_node_filter_from_prop_condition(prop_ref.clone(), first)?; + for c in it { + let next = build_node_filter_from_prop_condition(prop_ref.clone(), c)?; + acc = CompositeNodeFilter::And(Box::new(acc), Box::new(next)); } - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires an integer (u64) value, got {v}" - ))), - }, - - StartsWith | EndsWith | Contains | NotContains => match value { - Value::Str(s) => Ok(FilterValue::ID(GID::Str(s))), - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires a string value, got {v}" - ))), - }, - - IsIn | IsNotIn => match value { - Value::List(items) => { - let all_u64 = items.iter().all(|v| matches!(v, Value::U64(_))); - let all_str = items.iter().all(|v| matches!(v, Value::Str(_))); - - if !(all_u64 || all_str) { - return Err(GraphError::InvalidGqlFilter( - "Operator {operator} on node_id requires a homogeneous list of ints or strings".into(), - )); - } - - let mut set = std::collections::HashSet::with_capacity(items.len()); - if all_u64 { - for v in items { - if let Value::U64(i) = v { - let u = i.try_into().map_err(|_| { - GraphError::InvalidGqlFilter("node_id must be >= 0".into()) - })?; - set.insert(GID::U64(u)); - } - } - } else { - for v in items { - if let Value::Str(s) = v { - set.insert(GID::Str(s)); - } - } - } - Ok(FilterValue::IDSet(Arc::new(set))) + Ok(acc) + } + Or(list) => { + let mut it = list.iter(); + let first = it + .next() + .ok_or_else(|| GraphError::InvalidGqlFilter("or expects non-empty list".into()))?; + let mut acc = build_node_filter_from_prop_condition(prop_ref.clone(), first)?; + for c in it { + let next = build_node_filter_from_prop_condition(prop_ref.clone(), c)?; + acc = CompositeNodeFilter::Or(Box::new(acc), Box::new(next)); } - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires a list, got {v}" - ))), - }, - - IsSome | IsNone => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} is not supported on node_id" - ))), - } -} - -fn string_field_value(value: Value, operator: Operator) -> Result { - use Operator::*; - match (value, operator) { - ( - Value::Str(s), - Equal | NotEqual | StartsWith | EndsWith | Contains | NotContains | GreaterThan - | GreaterThanOrEqual | LessThan | LessThanOrEqual, - ) => Ok(FilterValue::Single(s)), - (Value::List(items), IsIn | IsNotIn) => { - let strings = items - .into_iter() - .map(|v| match v { - Value::Str(s) => Ok(s), - other => Err(GraphError::InvalidGqlFilter(format!( - "Expected list of strings, got {other}" - ))), - }) - .collect::, _>>()?; - - Ok(FilterValue::Set(Arc::new(strings.into_iter().collect()))) + Ok(acc) + } + Not(inner) => { + let nf = build_node_filter_from_prop_condition(prop_ref, inner)?; + Ok(CompositeNodeFilter::Not(Box::new(nf))) + } + _ => { + let pf = build_property_filter_from_condition::< + raphtory::db::graph::views::filter::model::node_filter::NodeFilter, + >(prop_ref, cond)?; + Ok(CompositeNodeFilter::Property(pf)) } - (v, op) => Err(GraphError::InvalidGqlFilter(format!( - "Invalid value/operator combination for string field: value {v:?}, operator {op}" - ))), } } impl TryFrom for CompositeNodeFilter { type Error = GraphError; - fn try_from(filter: NodeFilter) -> Result { match filter { NodeFilter::Node(node) => { - node.validate()?; + let (field_name, field_value, operator) = + translate_node_field_where(node.field, &node.where_)?; Ok(CompositeNodeFilter::Node(Filter { - field_name: node.field.to_string(), - field_value: node_field_value(node.field, node.value, node.operator)?, - operator: node.operator.into(), + field_name, + field_value, + operator, })) } NodeFilter::Property(prop) => { - prop.validate()?; - Ok(CompositeNodeFilter::Property(prop.try_into()?)) + let prop_ref = PropertyRef::Property(prop.name); + build_node_filter_from_prop_condition(prop_ref, &prop.where_) } NodeFilter::Metadata(prop) => { - prop.validate()?; - Ok(CompositeNodeFilter::Property(prop.try_into()?)) + let prop_ref = PropertyRef::Metadata(prop.name); + build_node_filter_from_prop_condition(prop_ref, &prop.where_) } NodeFilter::TemporalProperty(prop) => { - prop.validate()?; - Ok(CompositeNodeFilter::Property(prop.try_into()?)) + let prop_ref = PropertyRef::TemporalProperty(prop.name); + build_node_filter_from_prop_condition(prop_ref, &prop.where_) } NodeFilter::And(and_filters) => { - let mut iter = and_filters - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()? - .into_iter(); - if let Some(first) = iter.next() { - let and_chain = iter.fold(first, |acc, next| { - CompositeNodeFilter::And(Box::new(acc), Box::new(next)) - }); - Ok(and_chain) - } else { - Err(GraphError::InvalidGqlFilter( - "Filter 'and' requires non-empty list".to_string(), - )) - } + let mut iter = and_filters.into_iter().map(TryInto::try_into); + let first = iter.next().ok_or_else(|| { + GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) + })??; + Ok(iter.try_fold(first, |acc, next| { + let n = next?; + Ok::<_, GraphError>(CompositeNodeFilter::And(Box::new(acc), Box::new(n))) + })?) } NodeFilter::Or(or_filters) => { - let mut iter = or_filters - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()? - .into_iter(); - if let Some(first) = iter.next() { - let or_chain = iter.fold(first, |acc, next| { - CompositeNodeFilter::Or(Box::new(acc), Box::new(next)) - }); - Ok(or_chain) - } else { - Err(GraphError::InvalidGqlFilter( - "Filter 'or' requires non-empty list".to_string(), - )) - } + let mut iter = or_filters.into_iter().map(TryInto::try_into); + let first = iter.next().ok_or_else(|| { + GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) + })??; + Ok(iter.try_fold(first, |acc, next| { + let n = next?; + Ok::<_, GraphError>(CompositeNodeFilter::Or(Box::new(acc), Box::new(n))) + })?) } NodeFilter::Not(not_filters) => { let inner = CompositeNodeFilter::try_from(not_filters.deref().clone())?; @@ -737,74 +953,111 @@ impl TryFrom for CompositeNodeFilter { } } +fn build_edge_filter_from_prop_condition( + prop_ref: PropertyRef, + cond: &PropCondition, +) -> Result { + use PropCondition::*; + + match cond { + And(list) => { + let mut it = list.iter(); + let first = it + .next() + .ok_or_else(|| GraphError::InvalidGqlFilter("and expects non-empty list".into()))?; + let mut acc = build_edge_filter_from_prop_condition(prop_ref.clone(), first)?; + for c in it { + let next = build_edge_filter_from_prop_condition(prop_ref.clone(), c)?; + acc = CompositeEdgeFilter::And(Box::new(acc), Box::new(next)); + } + Ok(acc) + } + Or(list) => { + let mut it = list.iter(); + let first = it + .next() + .ok_or_else(|| GraphError::InvalidGqlFilter("or expects non-empty list".into()))?; + let mut acc = build_edge_filter_from_prop_condition(prop_ref.clone(), first)?; + for c in it { + let next = build_edge_filter_from_prop_condition(prop_ref.clone(), c)?; + acc = CompositeEdgeFilter::Or(Box::new(acc), Box::new(next)); + } + Ok(acc) + } + Not(inner) => { + let ef = build_edge_filter_from_prop_condition(prop_ref, inner)?; + Ok(CompositeEdgeFilter::Not(Box::new(ef))) + } + _ => { + let pf = build_property_filter_from_condition::< + raphtory::db::graph::views::filter::model::edge_filter::EdgeFilter, + >(prop_ref, cond)?; + Ok(CompositeEdgeFilter::Property(pf)) + } + } +} + impl TryFrom for CompositeEdgeFilter { type Error = GraphError; - fn try_from(filter: EdgeFilter) -> Result { match filter { EdgeFilter::Src(src) => { - src.validate()?; + if matches!(src.field, NodeField::NodeType) { + return Err(GraphError::InvalidGqlFilter( + "Src filter does not support NODE_TYPE".into(), + )); + } + let (_, field_value, operator) = translate_node_field_where(src.field, &src.where_)?; Ok(CompositeEdgeFilter::Edge(Filter { field_name: "src".to_string(), - field_value: node_field_value(src.field, src.value, src.operator)?, - operator: src.operator.into(), + field_value, + operator, })) } EdgeFilter::Dst(dst) => { - dst.validate()?; + if matches!(dst.field, NodeField::NodeType) { + return Err(GraphError::InvalidGqlFilter( + "Dst filter does not support NODE_TYPE".into(), + )); + } + let (_, field_value, operator) = translate_node_field_where(dst.field, &dst.where_)?; Ok(CompositeEdgeFilter::Edge(Filter { field_name: "dst".to_string(), - field_value: node_field_value(dst.field, dst.value, dst.operator)?, - operator: dst.operator.into(), + field_value, + operator, })) } - EdgeFilter::Property(prop) => { - prop.validate()?; - Ok(CompositeEdgeFilter::Property(prop.try_into()?)) + EdgeFilter::Property(p) => { + let prop_ref = PropertyRef::Property(p.name); + build_edge_filter_from_prop_condition(prop_ref, &p.where_) } - EdgeFilter::Metadata(prop) => { - prop.validate()?; - Ok(CompositeEdgeFilter::Property(prop.try_into()?)) + EdgeFilter::Metadata(p) => { + let prop_ref = PropertyRef::Metadata(p.name); + build_edge_filter_from_prop_condition(prop_ref, &p.where_) } - EdgeFilter::TemporalProperty(prop) => { - prop.validate()?; - Ok(CompositeEdgeFilter::Property(prop.try_into()?)) + EdgeFilter::TemporalProperty(p) => { + let prop_ref = PropertyRef::TemporalProperty(p.name); + build_edge_filter_from_prop_condition(prop_ref, &p.where_) } EdgeFilter::And(and_filters) => { - let mut iter = and_filters - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()? - .into_iter(); - - if let Some(first) = iter.next() { - let and_chain = iter.fold(first, |acc, next| { - CompositeEdgeFilter::And(Box::new(acc), Box::new(next)) - }); - Ok(and_chain) - } else { - Err(GraphError::InvalidGqlFilter( - "Filter 'and' requires non-empty list".to_string(), - )) - } + let mut iter = and_filters.into_iter().map(TryInto::try_into); + let first = iter.next().ok_or_else(|| { + GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) + })??; + Ok(iter.try_fold(first, |acc, next| { + let n = next?; + Ok::<_, GraphError>(CompositeEdgeFilter::And(Box::new(acc), Box::new(n))) + })?) } EdgeFilter::Or(or_filters) => { - let mut iter = or_filters - .into_iter() - .map(TryInto::try_into) - .collect::, _>>()? - .into_iter(); - - if let Some(first) = iter.next() { - let or_chain = iter.fold(first, |acc, next| { - CompositeEdgeFilter::Or(Box::new(acc), Box::new(next)) - }); - Ok(or_chain) - } else { - Err(GraphError::InvalidGqlFilter( - "Filter 'or' requires non-empty list".to_string(), - )) - } + let mut iter = or_filters.into_iter().map(TryInto::try_into); + let first = iter.next().ok_or_else(|| { + GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) + })??; + Ok(iter.try_fold(first, |acc, next| { + let n = next?; + Ok::<_, GraphError>(CompositeEdgeFilter::Or(Box::new(acc), Box::new(n))) + })?) } EdgeFilter::Not(not_filters) => { let inner = CompositeEdgeFilter::try_from(not_filters.deref().clone())?; @@ -813,206 +1066,3 @@ impl TryFrom for CompositeEdgeFilter { } } } - -fn build_property_filter( - prop_ref: PropertyRef, - operator: Operator, - value: Option<&Value>, - ops: Vec, -) -> Result, GraphError> { - let prop = value.cloned().map(Prop::try_from).transpose()?; - - validate_operator_value_pair(operator, value)?; - - let prop_value = match (&prop, operator) { - (Some(Prop::List(list)), Operator::IsIn | Operator::IsNotIn) => { - PropertyFilterValue::Set(Arc::new(list.iter().cloned().collect())) - } - (Some(p), _) => PropertyFilterValue::Single(p.clone()), - (None, _) => PropertyFilterValue::None, - }; - - Ok(PropertyFilter { - prop_ref, - prop_value, - operator: operator.into(), - ops, - _phantom: PhantomData, - }) -} - -impl TryFrom for PropertyFilter { - type Error = GraphError; - - fn try_from(expr: PropertyFilterExpr) -> Result { - expr.validate()?; - - let ops: Vec<_> = expr.ops.into_iter().flatten().map(Into::into).collect(); - - build_property_filter( - PropertyRef::Property(expr.name), - expr.operator, - expr.value.as_ref(), - ops, - ) - } -} - -impl TryFrom for PropertyFilter { - type Error = GraphError; - - fn try_from(expr: MetadataFilterExpr) -> Result { - expr.validate()?; - - let ops: Vec<_> = expr.ops.into_iter().flatten().map(Into::into).collect(); - - build_property_filter( - PropertyRef::Metadata(expr.name), - expr.operator, - expr.value.as_ref(), - ops, - ) - } -} - -impl TryFrom for PropertyFilter { - type Error = GraphError; - - fn try_from(expr: TemporalPropertyFilterExpr) -> Result { - expr.validate()?; - - let ops: Vec<_> = expr.ops.into_iter().flatten().map(Into::into).collect(); - - build_property_filter( - PropertyRef::TemporalProperty(expr.name), - expr.operator, - expr.value.as_ref(), - ops, - ) - } -} - -impl Display for NodeField { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let field_name = match self { - NodeField::NodeId => "node_id", - NodeField::NodeName => "node_name", - NodeField::NodeType => "node_type", - }; - write!(f, "{}", field_name) - } -} - -impl From for FilterOperator { - fn from(op: Operator) -> Self { - match op { - Operator::Equal => FilterOperator::Eq, - Operator::NotEqual => FilterOperator::Ne, - Operator::GreaterThanOrEqual => FilterOperator::Ge, - Operator::LessThanOrEqual => FilterOperator::Le, - Operator::GreaterThan => FilterOperator::Gt, - Operator::LessThan => FilterOperator::Lt, - Operator::IsIn => FilterOperator::In, - Operator::IsNotIn => FilterOperator::NotIn, - Operator::IsSome => FilterOperator::IsSome, - Operator::IsNone => FilterOperator::IsNone, - Operator::StartsWith => FilterOperator::StartsWith, - Operator::EndsWith => FilterOperator::EndsWith, - Operator::Contains => FilterOperator::Contains, - Operator::NotContains => FilterOperator::NotContains, - } - } -} - -fn validate_id_operator_value_pair(operator: Operator, value: &Value) -> Result<(), GraphError> { - use Operator::*; - - match operator { - Equal | NotEqual => match value { - Value::U64(_) | Value::Str(_) => Ok(()), - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires int or string, got {v}" - ))), - }, - GreaterThan | GreaterThanOrEqual | LessThan | LessThanOrEqual => match value { - Value::U64(_) => Ok(()), - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires an integer (u64) value, got {v}" - ))), - }, - StartsWith | EndsWith | Contains | NotContains => match value { - Value::Str(_) => Ok(()), - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires a string value, got {v}" - ))), - }, - IsIn | IsNotIn => match value { - Value::List(items) => { - let all_u64 = items.iter().all(|v| matches!(v, Value::U64(_))); - let all_str = items.iter().all(|v| matches!(v, Value::Str(_))); - if all_u64 || all_str { - Ok(()) - } else { - Err(GraphError::InvalidGqlFilter( - format!("Operator {operator} on node_id requires a homogeneous list of ints or strings"), - )) - } - } - v => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} on node_id requires a list, got {v}" - ))), - }, - IsNone | IsSome => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} is not supported on node_id" - ))), - } -} - -fn validate_operator_value_pair( - operator: Operator, - value: Option<&Value>, -) -> Result<(), GraphError> { - use Operator::*; - - match operator { - IsSome | IsNone => { - if value.is_some() { - Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} does not accept a value" - ))) - } else { - Ok(()) - } - } - - IsIn | IsNotIn => match value { - Some(Value::List(_)) => Ok(()), - Some(v) => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} requires a list value, got {v}" - ))), - None => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} requires a list" - ))), - }, - - StartsWith | EndsWith | Contains | NotContains => match value { - Some(Value::Str(_)) => Ok(()), - Some(v) => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} requires a string value, got {v}" - ))), - None => Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} requires a string value" - ))), - }, - - Equal | NotEqual | LessThan | LessThanOrEqual | GreaterThan | GreaterThanOrEqual => { - if value.is_none() { - return Err(GraphError::InvalidGqlFilter(format!( - "Operator {operator} requires a value" - ))); - } - - Ok(()) - } - } -} diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 8c89360c91..d0ee6d4f5c 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -253,7 +253,7 @@ mod test_composite_filters { ); assert_eq!( - "((((node_type NOT_IN [fire_nation, water_tribe] AND p2 == 2) AND p1 == 1) AND (p3 <= 5 OR p4 IN [2, 10])) OR (node_name == pometry OR p5 == 9))", + "((((node_type IS_NOT_IN [fire_nation, water_tribe] AND p2 == 2) AND p1 == 1) AND (p3 <= 5 OR p4 IS_IN [2, 10])) OR (node_name == pometry OR p5 == 9))", CompositeNodeFilter::Or(Box::new(CompositeNodeFilter::And( Box::new(CompositeNodeFilter::And( Box::new(CompositeNodeFilter::And( @@ -321,7 +321,7 @@ mod test_composite_filters { ); assert_eq!( - "((((edge_type NOT_IN [fire_nation, water_tribe] AND p2 == 2) AND p1 == 1) AND (p3 <= 5 OR p4 IN [2, 10])) OR (src == pometry OR p5 == 9))", + "((((edge_type IS_NOT_IN [fire_nation, water_tribe] AND p2 == 2) AND p1 == 1) AND (p3 <= 5 OR p4 IS_IN [2, 10])) OR (src == pometry OR p5 == 9))", CompositeEdgeFilter::Or( Box::new(CompositeEdgeFilter::And( Box::new(CompositeEdgeFilter::And( diff --git a/raphtory/src/db/graph/views/filter/model/filter_operator.rs b/raphtory/src/db/graph/views/filter/model/filter_operator.rs index eb77a11999..a70c919064 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -11,8 +11,8 @@ pub enum FilterOperator { Le, Gt, Ge, - In, - NotIn, + IsIn, + IsNotIn, IsSome, IsNone, StartsWith, @@ -34,8 +34,8 @@ impl Display for FilterOperator { FilterOperator::Le => "<=", FilterOperator::Gt => ">", FilterOperator::Ge => ">=", - FilterOperator::In => "IN", - FilterOperator::NotIn => "NOT_IN", + FilterOperator::IsIn => "IS_IN", + FilterOperator::IsNotIn => "IS_NOT_IN", FilterOperator::IsSome => "IS_SOME", FilterOperator::IsNone => "IS_NONE", FilterOperator::StartsWith => "STARTS_WITH", @@ -106,8 +106,8 @@ impl FilterOperator { T: Eq + std::hash::Hash, { match self { - FilterOperator::In => |set: &HashSet, value: &T| set.contains(value), - FilterOperator::NotIn => |set: &HashSet, value: &T| !set.contains(value), + FilterOperator::IsIn => |set: &HashSet, value: &T| set.contains(value), + FilterOperator::IsNotIn => |set: &HashSet, value: &T| !set.contains(value), _ => panic!("Collection operation not supported for this operator"), } } @@ -186,18 +186,18 @@ impl FilterOperator { } } - In | NotIn | IsSome | IsNone => false, + IsIn | IsNotIn | IsSome | IsNone => false, }, Set(set) => match self { - In => { + IsIn => { if let Some(r) = right { set.contains(r) } else { false } } - NotIn => { + IsNotIn => { if let Some(r) = right { !set.contains(r) } else { @@ -231,9 +231,9 @@ impl FilterOperator { }, FilterValue::Set(l) => match self { - FilterOperator::In | FilterOperator::NotIn => match right { + FilterOperator::IsIn | FilterOperator::IsNotIn => match right { Some(r) => self.collection_operation()(l, &r.to_string()), - None => matches!(self, FilterOperator::NotIn), + None => matches!(self, FilterOperator::IsNotIn), }, _ => unreachable!(), }, @@ -278,13 +278,13 @@ impl FilterOperator { FilterValue::IDSet(set) => match right { GidRef::U64(r) => match self { - FilterOperator::In => set.contains(&GID::U64(r)), - FilterOperator::NotIn => !set.contains(&GID::U64(r)), + FilterOperator::IsIn => set.contains(&GID::U64(r)), + FilterOperator::IsNotIn => !set.contains(&GID::U64(r)), _ => false, }, GidRef::Str(s) => match self { - FilterOperator::In => set.contains(&GID::Str(s.to_string())), - FilterOperator::NotIn => !set.contains(&GID::Str(s.to_string())), + FilterOperator::IsIn => set.contains(&GID::Str(s.to_string())), + FilterOperator::IsNotIn => !set.contains(&GID::Str(s.to_string())), _ => false, }, }, @@ -292,8 +292,8 @@ impl FilterOperator { FilterValue::Set(set) => match right { GidRef::U64(_) => false, GidRef::Str(s) => match self { - FilterOperator::In => set.contains(s), - FilterOperator::NotIn => !set.contains(s), + FilterOperator::IsIn => set.contains(s), + FilterOperator::IsNotIn => !set.contains(s), _ => false, }, }, diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index e16192c55b..3d934244c8 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -95,7 +95,7 @@ impl Filter { Self { field_name: field_name.into(), field_value: FilterValue::Set(Arc::new(field_values.into_iter().collect())), - operator: FilterOperator::In, + operator: FilterOperator::IsIn, } } @@ -106,7 +106,7 @@ impl Filter { Self { field_name: field_name.into(), field_value: FilterValue::Set(Arc::new(field_values.into_iter().collect())), - operator: FilterOperator::NotIn, + operator: FilterOperator::IsNotIn, } } @@ -183,7 +183,7 @@ impl Filter { Self { field_name: field_name.into(), field_value: FilterValue::IDSet(Arc::new(set)), - operator: FilterOperator::In, + operator: FilterOperator::IsIn, } } @@ -196,7 +196,7 @@ impl Filter { Self { field_name: field_name.into(), field_value: FilterValue::IDSet(Arc::new(set)), - operator: FilterOperator::NotIn, + operator: FilterOperator::IsNotIn, } } @@ -262,7 +262,7 @@ impl Filter { } else { // No endpoint node -> no value present. match self.operator { - FilterOperator::Ne | FilterOperator::NotIn => true, + FilterOperator::Ne | FilterOperator::IsNotIn => true, _ => false, } } diff --git a/raphtory/src/db/graph/views/filter/model/node_filter.rs b/raphtory/src/db/graph/views/filter/model/node_filter.rs index eae156eb48..2c2326d9db 100644 --- a/raphtory/src/db/graph/views/filter/model/node_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/node_filter.rs @@ -349,7 +349,10 @@ impl NodeFilter { }; let op_allowed = match kind { - U64 => matches!(filter.operator, Eq | Ne | Lt | Le | Gt | Ge | In | NotIn), + U64 => matches!( + filter.operator, + Eq | Ne | Lt | Le | Gt | Ge | IsIn | IsNotIn + ), Str => matches!( filter.operator, Eq | Ne @@ -358,8 +361,8 @@ impl NodeFilter { | Contains | NotContains | FuzzySearch { .. } - | In - | NotIn + | IsIn + | IsNotIn ), }; @@ -379,7 +382,7 @@ impl NodeFilter { } match filter.operator { - In | NotIn => { + IsIn | IsNotIn => { if !matches!( filter.field_value, FilterValue::IDSet(_) | FilterValue::Set(_) diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index bab1ed6aaa..6c791696b7 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -287,7 +287,7 @@ impl PropertyFilter { Self { prop_ref, prop_value: PropertyFilterValue::Set(Arc::new(prop_values.into_iter().collect())), - operator: FilterOperator::In, + operator: FilterOperator::IsIn, ops: vec![], _phantom: PhantomData, } @@ -297,7 +297,7 @@ impl PropertyFilter { Self { prop_ref, prop_value: PropertyFilterValue::Set(Arc::new(prop_values.into_iter().collect())), - operator: FilterOperator::NotIn, + operator: FilterOperator::IsNotIn, ops: vec![], _phantom: PhantomData, } @@ -471,7 +471,7 @@ impl PropertyFilter { return Err(GraphError::InvalidFilterCmp(fd)); } } - FilterOperator::In | FilterOperator::NotIn => match &self.prop_value { + FilterOperator::IsIn | FilterOperator::IsNotIn => match &self.prop_value { PropertyFilterValue::Set(_) => {} PropertyFilterValue::None => { return Err(GraphError::InvalidFilterExpectSetGotNone(self.operator)) diff --git a/raphtory/src/search/query_builder.rs b/raphtory/src/search/query_builder.rs index 8d1b0a5d0e..6013930e49 100644 --- a/raphtory/src/search/query_builder.rs +++ b/raphtory/src/search/query_builder.rs @@ -114,8 +114,8 @@ impl<'a> QueryBuilder<'a> { .collect(); let terms = terms?; match &filter.operator { - FilterOperator::In => create_in_query(terms), - FilterOperator::NotIn => create_not_in_query(terms), + FilterOperator::IsIn => create_in_query(terms), + FilterOperator::IsNotIn => create_not_in_query(terms), _ => unreachable!(), } } @@ -178,8 +178,8 @@ impl<'a> QueryBuilder<'a> { .collect(); let terms = terms?; match operator { - FilterOperator::In => create_in_query(terms), - FilterOperator::NotIn => create_not_in_query(terms), + FilterOperator::IsIn => create_in_query(terms), + FilterOperator::IsNotIn => create_not_in_query(terms), _ => unreachable!(), } } @@ -242,8 +242,8 @@ impl<'a> QueryBuilder<'a> { .collect(); let terms = terms?; match operator { - FilterOperator::In => create_in_query(terms), - FilterOperator::NotIn => create_not_in_query(terms), + FilterOperator::IsIn => create_in_query(terms), + FilterOperator::IsNotIn => create_not_in_query(terms), _ => unreachable!(), } } From bd833e665000ae17e7813e9e74fcf7c9520d1909 Mon Sep 17 00:00:00 2001 From: shivamka1 <4599890+shivamka1@users.noreply.github.com> Date: Thu, 6 Nov 2025 14:38:58 +0000 Subject: [PATCH 7/8] add review suggestions --- python/python/raphtory/filter/__init__.pyi | 81 ++- .../test_filters/test_node_property_filter.py | 4 +- raphtory-graphql/schema.graphql | 351 ++++++++++- raphtory-graphql/src/model/graph/filtering.rs | 169 +++--- raphtory-graphql/src/model/graph/graph.rs | 10 +- raphtory-graphql/src/model/graph/node.rs | 4 +- raphtory-graphql/src/model/graph/nodes.rs | 4 +- .../views/filter/model/property_filter.rs | 36 ++ raphtory/src/python/filter/filter_expr.rs | 23 +- raphtory/src/python/filter/mod.rs | 9 +- .../python/filter/property_filter_builders.rs | 551 +++--------------- raphtory/src/python/graph/node.rs | 2 +- raphtory/src/python/graph/views/graph_view.rs | 2 +- .../types/macros/trait_impl/filter_ops.rs | 29 +- 14 files changed, 612 insertions(+), 663 deletions(-) diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 009c7c5670..4819150fd9 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -30,8 +30,7 @@ __all__ = [ "EdgeEndpoint", "Edge", "ExplodedEdge", - "Property", - "Metadata", + "FilterOps", ] class FilterExpr(object): @@ -50,43 +49,8 @@ class FilterExpr(object): def __ror__(self, value): """Return value|self.""" -class PropertyFilterOps(object): - def __eq__(self, value): - """Return self==value.""" - - def __ge__(self, value): - """Return self>=value.""" - - def __gt__(self, value): - """Return self>value.""" - - def __le__(self, value): - """Return self<=value.""" - - def __lt__(self, value): - """Return self=value.""" + + def __gt__(self, value): + """Return self>value.""" + + def __le__(self, value): + """Return self<=value.""" + + def __lt__(self, value): + """Return self 0 with pytest.raises( Exception, match=r"List aggregation len cannot be used after an element qualifier \(any/all\)", ): graph.filter(filter_expr).nodes.id - filter_expr = filter.Node.property("prop8").sum().any() + filter_expr = filter.Node.property("prop8").sum().any() > 0 with pytest.raises( Exception, match=r"Element qualifiers \(any/all\) cannot be used after a list aggregation \(len/sum/avg/min/max\).", diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index b3cc6487ee..60d16aa028 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -314,13 +314,37 @@ input EdgeAddition { } input EdgeFilter @oneOf { + """ + Source node filter. + """ src: NodeFieldFilterNew + """ + Destination node filter. + """ dst: NodeFieldFilterNew + """ + Property filter. + """ property: PropertyFilterNew + """ + Metadata filter. + """ metadata: PropertyFilterNew + """ + Temporal property filter. + """ temporalProperty: PropertyFilterNew + """ + AND operator. + """ and: [EdgeFilter!] + """ + OR operator. + """ or: [EdgeFilter!] + """ + NOT operator. + """ not: EdgeFilter } @@ -367,20 +391,65 @@ input EdgeSortBy { } input EdgeViewCollection @oneOf { + """ + Contains only the default layer. + """ defaultLayer: Boolean + """ + Latest time. + """ latest: Boolean + """ + Snapshot at latest time. + """ snapshotLatest: Boolean + """ + Snapshot at specified time. + """ snapshotAt: Int + """ + List of included layers. + """ layers: [String!] + """ + List of excluded layers. + """ excludeLayers: [String!] + """ + Single included layer. + """ layer: String + """ + Single excluded layer. + """ excludeLayer: String + """ + Window between a start and end time. + """ window: Window + """ + View at a specified time. + """ at: Int + """ + View before a specified time (end exclusive). + """ before: Int + """ + View after a specified time (start exclusive). + """ after: Int + """ + Shrink a Window to a specified start and end time. + """ shrinkWindow: Window + """ + Set the window start to a specified time. + """ shrinkStart: Int + """ + Set the window end to a specified time. + """ shrinkEnd: Int } @@ -512,20 +581,65 @@ type Edges { } input EdgesViewCollection @oneOf { + """ + Contains only the default layer. + """ defaultLayer: Boolean + """ + Latest time. + """ latest: Boolean + """ + Snapshot at latest time. + """ snapshotLatest: Boolean + """ + Snapshot at specified time. + """ snapshotAt: Int + """ + List of included layers. + """ layers: [String!] + """ + List of excluded layers. + """ excludeLayers: [String!] + """ + Single included layer. + """ layer: String + """ + Single excluded layer. + """ excludeLayer: String + """ + Window between a start and end time. + """ window: Window + """ + View at a specified time. + """ at: Int + """ + View before a specified time (end exclusive). + """ before: Int + """ + View after a specified time (start exclusive). + """ after: Int + """ + Shrink a Window to a specified start and end time. + """ shrinkWindow: Window + """ + Set the window start to a specified time. + """ shrinkStart: Int + """ + Set the window end to a specified time. + """ shrinkEnd: Int } @@ -762,8 +876,8 @@ type Graph { } type GraphAlgorithmPlugin { - shortest_path(source: String!, targets: [String!]!, direction: String): [ShortestPathOutput!]! pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! + shortest_path(source: String!, targets: [String!]!, direction: String): [ShortestPathOutput!]! } type GraphSchema { @@ -783,26 +897,89 @@ enum GraphType { } input GraphViewCollection @oneOf { + """ + Contains only the default layer. + """ defaultLayer: Boolean + """ + List of included layers. + """ layers: [String!] + """ + List of excluded layers. + """ excludeLayers: [String!] + """ + Single included layer. + """ layer: String + """ + Single excluded layer. + """ excludeLayer: String + """ + Subgraph nodes. + """ subgraph: [String!] + """ + Subgraph node types. + """ subgraphNodeTypes: [String!] + """ + List of excluded nodes. + """ excludeNodes: [String!] + """ + Valid state. + """ valid: Boolean + """ + Window between a start and end time. + """ window: Window + """ + View at a specified time. + """ at: Int + """ + View at the latest time. + """ latest: Boolean + """ + Snapshot at specified time. + """ snapshotAt: Int + """ + Snapshot at latest time. + """ snapshotLatest: Boolean + """ + View before a specified time (end exclusive). + """ before: Int + """ + View after a specified time (start exclusive). + """ after: Int + """ + Shrink a Window to a specified start and end time. + """ shrinkWindow: Window + """ + Set the window start to a specified time. + """ shrinkStart: Int + """ + Set the window end to a specified time. + """ shrinkEnd: Int + """ + Node filter. + """ nodeFilter: NodeFilter + """ + Edge filter. + """ edgeFilter: EdgeFilter } @@ -1312,8 +1489,17 @@ input NodeAddition { } enum NodeField { + """ + Node id. + """ NODE_ID + """ + Node name. + """ NODE_NAME + """ + Node type. + """ NODE_TYPE } @@ -1338,12 +1524,33 @@ input NodeFieldFilterNew { } input NodeFilter @oneOf { + """ + Node filter. + """ node: NodeFieldFilterNew + """ + Property filter. + """ property: PropertyFilterNew + """ + Metadata filter. + """ metadata: PropertyFilterNew + """ + Temporal property filter. + """ temporalProperty: PropertyFilterNew + """ + AND operator. + """ and: [NodeFilter!] + """ + OR operator. + """ or: [NodeFilter!] + """ + NOT operator. + """ not: NodeFilter } @@ -1376,21 +1583,69 @@ input NodeSortBy { } input NodeViewCollection @oneOf { + """ + Contains only the default layer. + """ defaultLayer: Boolean + """ + View at the latest time. + """ latest: Boolean + """ + Snapshot at latest time. + """ snapshotLatest: Boolean + """ + Snapshot at specified time. + """ snapshotAt: Int + """ + List of included layers. + """ layers: [String!] + """ + List of excluded layers. + """ excludeLayers: [String!] + """ + Single included layer. + """ layer: String + """ + Single excluded layer. + """ excludeLayer: String + """ + Window between a start and end time. + """ window: Window + """ + View at a specified time. + """ at: Int + """ + View before a specified time (end exclusive). + """ before: Int + """ + View after a specified time (start exclusive). + """ after: Int + """ + Shrink a Window to a specified start and end time. + """ shrinkWindow: Window + """ + Set the window start to a specified time. + """ shrinkStart: Int + """ + Set the window end to a specified time. + """ shrinkEnd: Int + """ + Node filter. + """ nodeFilter: NodeFilter } @@ -1511,22 +1766,73 @@ type Nodes { } input NodesViewCollection @oneOf { + """ + Contains only the default layer. + """ defaultLayer: Boolean + """ + View at the latest time. + """ latest: Boolean + """ + Snapshot at latest time. + """ snapshotLatest: Boolean + """ + List of included layers. + """ layers: [String!] + """ + List of excluded layers. + """ excludeLayers: [String!] + """ + Single included layer. + """ layer: String + """ + Single excluded layer. + """ excludeLayer: String + """ + Window between a start and end time. + """ window: Window + """ + View at a specified time. + """ at: Int + """ + Snapshot at specified time. + """ snapshotAt: Int + """ + View before a specified time (end exclusive). + """ before: Int + """ + View after a specified time (start exclusive). + """ after: Int + """ + Shrink a Window to a specified start and end time. + """ shrinkWindow: Window + """ + Set the window start to a specified time. + """ shrinkStart: Int + """ + Set the window end to a specified time. + """ shrinkEnd: Int + """ + Node filter. + """ nodeFilter: NodeFilter + """ + List of types. + """ typeFilter: [String!] } @@ -1660,19 +1966,61 @@ type PathFromNode { } input PathFromNodeViewCollection @oneOf { + """ + Latest time. + """ latest: Boolean + """ + Latest snapshot. + """ snapshotLatest: Boolean + """ + Time. + """ snapshotAt: Int + """ + List of layers. + """ layers: [String!] + """ + List of excluded layers. + """ excludeLayers: [String!] + """ + Single layer. + """ layer: String + """ + Single layer to exclude. + """ excludeLayer: String + """ + Window between a start and end time. + """ window: Window + """ + View at a specified time. + """ at: Int + """ + View before a specified time (end exclusive). + """ before: Int + """ + View after a specified time (start exclusive). + """ after: Int + """ + Shrink a Window to a specified start and end time. + """ shrinkWindow: Window + """ + Set the window start to a specified time. + """ shrinkStart: Int + """ + Set the window end to a specified time. + """ shrinkEnd: Int } @@ -1704,7 +2052,6 @@ input PropCondition @oneOf { isNotIn: Value isSome: Boolean isNone: Boolean - between: [Value!] and: [PropCondition!] or: [PropCondition!] not: PropCondition diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 3b51e5c336..5c37eb34a2 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -8,9 +8,9 @@ use dynamic_graphql::{ }; use raphtory::{ db::graph::views::filter::model::{ - edge_filter::CompositeEdgeFilter, + edge_filter::{CompositeEdgeFilter, EdgeFilter}, filter_operator::FilterOperator, - node_filter::CompositeNodeFilter, + node_filter::{CompositeNodeFilter, NodeFilter}, property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, Filter, FilterValue, }, @@ -19,6 +19,7 @@ use raphtory::{ use raphtory_api::core::entities::{properties::prop::Prop, GID}; use std::{ borrow::Cow, + collections::HashSet, fmt, fmt::{Display, Formatter}, marker::PhantomData, @@ -75,9 +76,9 @@ pub enum GraphViewCollection { /// Set the window end to a specified time. ShrinkEnd(i64), /// Node filter. - NodeFilter(NodeFilter), + NodeFilter(GqlNodeFilter), /// Edge filter. - EdgeFilter(EdgeFilter), + EdgeFilter(GqlEdgeFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -113,7 +114,7 @@ pub enum NodesViewCollection { /// Set the window end to a specified time. ShrinkEnd(i64), /// Node filter. - NodeFilter(NodeFilter), + NodeFilter(GqlNodeFilter), /// List of types. TypeFilter(Vec), } @@ -151,7 +152,7 @@ pub enum NodeViewCollection { /// Set the window end to a specified time. ShrinkEnd(i64), /// Node filter. - NodeFilter(NodeFilter), + NodeFilter(GqlNodeFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -321,30 +322,42 @@ pub enum PropCondition { } impl PropCondition { - pub fn op_name(&self) -> Option<&'static str> { + pub fn op_name(&self) -> &'static str { use PropCondition::*; - Some(match self { + match self { Eq(_) => "eq", Ne(_) => "ne", Gt(_) => "gt", Ge(_) => "ge", Lt(_) => "lt", Le(_) => "le", - + StartsWith(_) => "startsWith", EndsWith(_) => "endsWith", Contains(_) => "contains", NotContains(_) => "notContains", - + IsIn(_) => "isIn", IsNotIn(_) => "isNotIn", - + IsSome(_) => "isSome", IsNone(_) => "isNone", - - And(_) | Or(_) | Not(_) | First(_) | Last(_) | Any(_) | All(_) | Sum(_) | Avg(_) - | Min(_) | Max(_) | Len(_) => return None, - }) + + And(_) => "and", + Or(_) => "or", + Not(_) => "not", + + First(_) => "first", + Last(_) => "last", + Any(_) => "any", + All(_) => "all", + + Sum(_) => "sum", + Avg(_) => "avg", + Min(_) => "min", + Max(_) => "max", + Len(_) => "len", + } } } @@ -367,9 +380,9 @@ pub enum NodeFieldCondition { } impl NodeFieldCondition { - pub fn op_name(&self) -> Option<&'static str> { + pub fn op_name(&self) -> &'static str { use NodeFieldCondition::*; - Some(match self { + match self { Eq(_) => "eq", Ne(_) => "ne", Gt(_) => "gt", @@ -382,7 +395,7 @@ impl NodeFieldCondition { NotContains(_) => "notContains", IsIn(_) => "isIn", IsNotIn(_) => "isNotIn", - }) + } } } @@ -394,7 +407,8 @@ pub struct NodeFieldFilterNew { } #[derive(OneOfInput, Clone, Debug)] -pub enum NodeFilter { +#[graphql(name = "NodeFilter")] +pub enum GqlNodeFilter { /// Node filter. Node(NodeFieldFilterNew), /// Property filter. @@ -404,15 +418,16 @@ pub enum NodeFilter { /// Temporal property filter. TemporalProperty(PropertyFilterNew), /// AND operator. - And(Vec), + And(Vec), /// OR operator. - Or(Vec), + Or(Vec), /// NOT operator. - Not(Wrapped), + Not(Wrapped), } #[derive(OneOfInput, Clone, Debug)] -pub enum EdgeFilter { +#[graphql(name = "EdgeFilter")] +pub enum GqlEdgeFilter { /// Source node filter. Src(NodeFieldFilterNew), /// Destination node filter. @@ -424,11 +439,11 @@ pub enum EdgeFilter { /// Temporal property filter. TemporalProperty(PropertyFilterNew), /// AND operator. - And(Vec), + And(Vec), /// OR operator. - Or(Vec), + Or(Vec), /// NOT operator. - Not(Wrapped), + Not(Wrapped), } #[derive(Clone, Debug)] @@ -509,18 +524,17 @@ fn peel_prop_wrappers_and_collect_ops<'a>( } } -fn require_string_value(op: Option<&str>, v: &Value) -> Result { +fn require_string_value(op: &str, v: &Value) -> Result { if let Value::Str(s) = v { Ok(s.clone()) } else { - let name = op.unwrap_or("UNKNOWN"); Err(GraphError::InvalidGqlFilter(format!( - "{name} requires a string value, got {v}" + "{op} requires a string value, got {v}" ))) } } -fn require_prop_list_value(op: Option<&str>, v: &Value) -> Result { +fn require_prop_list_value(op: &str, v: &Value) -> Result { if let Value::List(vs) = v { let props = vs .iter() @@ -531,42 +545,36 @@ fn require_prop_list_value(op: Option<&str>, v: &Value) -> Result, v: &Value) -> Result { +fn require_u64_value(op: &str, v: &Value) -> Result { if let Value::U64(i) = v { Ok(*i) } else { - let name = op.unwrap_or("UNKNOWN"); Err(GraphError::InvalidGqlFilter(format!( - "{name} requires a u64 value, got {v}" + "{op} requires a u64 value, got {v}" ))) } } -fn parse_node_id_scalar(op: Option<&str>, v: &Value) -> Result { +fn parse_node_id_scalar(op: &str, v: &Value) -> Result { match v { Value::U64(i) => Ok(FilterValue::ID(GID::U64(*i))), Value::Str(s) => Ok(FilterValue::ID(GID::Str(s.clone()))), - other => { - let name = op.unwrap_or("UNKNOWN"); - Err(GraphError::InvalidGqlFilter(format!( - "{name} requires int or str, got {other}" - ))) - } + other => Err(GraphError::InvalidGqlFilter(format!( + "{op} requires int or str, got {other}" + ))), } } -fn parse_node_id_list(op: Option<&str>, v: &Value) -> Result { - let name = op.unwrap_or("UNKNOWN"); +fn parse_node_id_list(op: &str, v: &Value) -> Result { let Value::List(vs) = v else { return Err(GraphError::InvalidGqlFilter(format!( - "{name} requires a list value, got {v}" + "{op} requires a list value, got {v}" ))); }; @@ -574,11 +582,11 @@ fn parse_node_id_list(op: Option<&str>, v: &Value) -> Result, v: &Value) -> Result, v: &Value) -> Result { - let name = op.unwrap_or("UNKNOWN"); +fn parse_string_list(op: &str, v: &Value) -> Result { let Value::List(vs) = v else { return Err(GraphError::InvalidGqlFilter(format!( - "{name} requires a list value, got {v}" + "{op} requires a list value, got {v}" ))); }; @@ -610,7 +617,7 @@ fn parse_string_list(op: Option<&str>, v: &Value) -> Result (FO::IsSome, PropertyFilterValue::None), IsNone(true) => (FO::IsNone, PropertyFilterValue::None), - And(_) | Or(_) | Not(_) | First(_) | Last(_) | Any(_) | All(_) | Sum(_) - | Avg(_) | Min(_) | Max(_) | Len(_) | IsSome(false) | IsNone(false) => { + And(_) | Or(_) | Not(_) | First(_) | Last(_) | Any(_) | All(_) | Sum(_) | Avg(_) + | Min(_) | Max(_) | Len(_) | IsSome(false) | IsNone(false) => { + let op = cmp.op_name(); return Err(GraphError::InvalidGqlFilter(format!( - "Expected comparison at leaf for {}", - name_for_errors + "Expected comparison at leaf for {name_for_errors}; got '{op}'" ))); } }) @@ -892,19 +899,17 @@ fn build_node_filter_from_prop_condition( Ok(CompositeNodeFilter::Not(Box::new(nf))) } _ => { - let pf = build_property_filter_from_condition::< - raphtory::db::graph::views::filter::model::node_filter::NodeFilter, - >(prop_ref, cond)?; + let pf = build_property_filter_from_condition::(prop_ref, cond)?; Ok(CompositeNodeFilter::Property(pf)) } } } -impl TryFrom for CompositeNodeFilter { +impl TryFrom for CompositeNodeFilter { type Error = GraphError; - fn try_from(filter: NodeFilter) -> Result { + fn try_from(filter: GqlNodeFilter) -> Result { match filter { - NodeFilter::Node(node) => { + GqlNodeFilter::Node(node) => { let (field_name, field_value, operator) = translate_node_field_where(node.field, &node.where_)?; Ok(CompositeNodeFilter::Node(Filter { @@ -913,19 +918,19 @@ impl TryFrom for CompositeNodeFilter { operator, })) } - NodeFilter::Property(prop) => { + GqlNodeFilter::Property(prop) => { let prop_ref = PropertyRef::Property(prop.name); build_node_filter_from_prop_condition(prop_ref, &prop.where_) } - NodeFilter::Metadata(prop) => { + GqlNodeFilter::Metadata(prop) => { let prop_ref = PropertyRef::Metadata(prop.name); build_node_filter_from_prop_condition(prop_ref, &prop.where_) } - NodeFilter::TemporalProperty(prop) => { + GqlNodeFilter::TemporalProperty(prop) => { let prop_ref = PropertyRef::TemporalProperty(prop.name); build_node_filter_from_prop_condition(prop_ref, &prop.where_) } - NodeFilter::And(and_filters) => { + GqlNodeFilter::And(and_filters) => { let mut iter = and_filters.into_iter().map(TryInto::try_into); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) @@ -935,7 +940,7 @@ impl TryFrom for CompositeNodeFilter { Ok::<_, GraphError>(CompositeNodeFilter::And(Box::new(acc), Box::new(n))) })?) } - NodeFilter::Or(or_filters) => { + GqlNodeFilter::Or(or_filters) => { let mut iter = or_filters.into_iter().map(TryInto::try_into); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) @@ -945,7 +950,7 @@ impl TryFrom for CompositeNodeFilter { Ok::<_, GraphError>(CompositeNodeFilter::Or(Box::new(acc), Box::new(n))) })?) } - NodeFilter::Not(not_filters) => { + GqlNodeFilter::Not(not_filters) => { let inner = CompositeNodeFilter::try_from(not_filters.deref().clone())?; Ok(CompositeNodeFilter::Not(Box::new(inner))) } @@ -989,57 +994,57 @@ fn build_edge_filter_from_prop_condition( Ok(CompositeEdgeFilter::Not(Box::new(ef))) } _ => { - let pf = build_property_filter_from_condition::< - raphtory::db::graph::views::filter::model::edge_filter::EdgeFilter, - >(prop_ref, cond)?; + let pf = build_property_filter_from_condition::(prop_ref, cond)?; Ok(CompositeEdgeFilter::Property(pf)) } } } -impl TryFrom for CompositeEdgeFilter { +impl TryFrom for CompositeEdgeFilter { type Error = GraphError; - fn try_from(filter: EdgeFilter) -> Result { + fn try_from(filter: GqlEdgeFilter) -> Result { match filter { - EdgeFilter::Src(src) => { + GqlEdgeFilter::Src(src) => { if matches!(src.field, NodeField::NodeType) { return Err(GraphError::InvalidGqlFilter( "Src filter does not support NODE_TYPE".into(), )); } - let (_, field_value, operator) = translate_node_field_where(src.field, &src.where_)?; + let (_, field_value, operator) = + translate_node_field_where(src.field, &src.where_)?; Ok(CompositeEdgeFilter::Edge(Filter { field_name: "src".to_string(), field_value, operator, })) } - EdgeFilter::Dst(dst) => { + GqlEdgeFilter::Dst(dst) => { if matches!(dst.field, NodeField::NodeType) { return Err(GraphError::InvalidGqlFilter( "Dst filter does not support NODE_TYPE".into(), )); } - let (_, field_value, operator) = translate_node_field_where(dst.field, &dst.where_)?; + let (_, field_value, operator) = + translate_node_field_where(dst.field, &dst.where_)?; Ok(CompositeEdgeFilter::Edge(Filter { field_name: "dst".to_string(), field_value, operator, })) } - EdgeFilter::Property(p) => { + GqlEdgeFilter::Property(p) => { let prop_ref = PropertyRef::Property(p.name); build_edge_filter_from_prop_condition(prop_ref, &p.where_) } - EdgeFilter::Metadata(p) => { + GqlEdgeFilter::Metadata(p) => { let prop_ref = PropertyRef::Metadata(p.name); build_edge_filter_from_prop_condition(prop_ref, &p.where_) } - EdgeFilter::TemporalProperty(p) => { + GqlEdgeFilter::TemporalProperty(p) => { let prop_ref = PropertyRef::TemporalProperty(p.name); build_edge_filter_from_prop_condition(prop_ref, &p.where_) } - EdgeFilter::And(and_filters) => { + GqlEdgeFilter::And(and_filters) => { let mut iter = and_filters.into_iter().map(TryInto::try_into); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'and' requires non-empty list".into()) @@ -1049,7 +1054,7 @@ impl TryFrom for CompositeEdgeFilter { Ok::<_, GraphError>(CompositeEdgeFilter::And(Box::new(acc), Box::new(n))) })?) } - EdgeFilter::Or(or_filters) => { + GqlEdgeFilter::Or(or_filters) => { let mut iter = or_filters.into_iter().map(TryInto::try_into); let first = iter.next().ok_or_else(|| { GraphError::InvalidGqlFilter("Filter 'or' requires non-empty list".into()) @@ -1059,7 +1064,7 @@ impl TryFrom for CompositeEdgeFilter { Ok::<_, GraphError>(CompositeEdgeFilter::Or(Box::new(acc), Box::new(n))) })?) } - EdgeFilter::Not(not_filters) => { + GqlEdgeFilter::Not(not_filters) => { let inner = CompositeEdgeFilter::try_from(not_filters.deref().clone())?; Ok(CompositeEdgeFilter::Not(Box::new(inner))) } diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index d10c7bcaf7..ed6293d256 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -4,7 +4,7 @@ use crate::{ graph::{ edge::GqlEdge, edges::GqlEdges, - filtering::{EdgeFilter, GraphViewCollection, NodeFilter}, + filtering::{GqlEdgeFilter, GqlNodeFilter, GraphViewCollection}, index::GqlIndexSpec, node::GqlNode, nodes::GqlNodes, @@ -502,7 +502,7 @@ impl GqlGraph { .await } - async fn node_filter(&self, filter: NodeFilter) -> Result { + async fn node_filter(&self, filter: GqlNodeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { let filter: CompositeNodeFilter = filter.try_into()?; @@ -515,7 +515,7 @@ impl GqlGraph { .await } - async fn edge_filter(&self, filter: EdgeFilter) -> Result { + async fn edge_filter(&self, filter: GqlEdgeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { let filter: CompositeEdgeFilter = filter.try_into()?; @@ -557,7 +557,7 @@ impl GqlGraph { /// Uses Tantivy's exact search. async fn search_nodes( &self, - filter: NodeFilter, + filter: GqlNodeFilter, limit: usize, offset: usize, ) -> Result, GraphError> { @@ -583,7 +583,7 @@ impl GqlGraph { /// Uses Tantivy's exact search. async fn search_edges( &self, - filter: EdgeFilter, + filter: GqlEdgeFilter, limit: usize, offset: usize, ) -> Result, GraphError> { diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index 29befda8ca..f470367a59 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -1,7 +1,7 @@ use crate::{ model::graph::{ edges::GqlEdges, - filtering::{NodeFilter, NodeViewCollection}, + filtering::{GqlNodeFilter, NodeViewCollection}, nodes::GqlNodes, path_from_node::GqlPathFromNode, property::{GqlMetadata, GqlProperties}, @@ -365,7 +365,7 @@ impl GqlNode { GqlPathFromNode::new(self.vv.out_neighbours()) } - async fn node_filter(&self, filter: NodeFilter) -> Result { + async fn node_filter(&self, filter: GqlNodeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { let filter: CompositeNodeFilter = filter.try_into()?; diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index 1b269f35f3..5a02b175a0 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -1,7 +1,7 @@ use crate::{ model::{ graph::{ - filtering::{NodeFilter, NodesViewCollection}, + filtering::{GqlNodeFilter, NodesViewCollection}, node::GqlNode, windowset::GqlNodesWindowSet, WindowDuration, @@ -181,7 +181,7 @@ impl GqlNodes { } /// Returns a view of the node types. - async fn node_filter(&self, filter: NodeFilter) -> Result { + async fn node_filter(&self, filter: GqlNodeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { let filter: CompositeNodeFilter = filter.try_into()?; diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index 6c791696b7..43e4c5e292 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -1542,49 +1542,63 @@ impl PropertyFilterOps for T { fn eq(&self, value: impl Into) -> PropertyFilter { PropertyFilter::eq(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } + fn ne(&self, value: impl Into) -> PropertyFilter { PropertyFilter::ne(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } + fn le(&self, value: impl Into) -> PropertyFilter { PropertyFilter::le(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } + fn ge(&self, value: impl Into) -> PropertyFilter { PropertyFilter::ge(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } + fn lt(&self, value: impl Into) -> PropertyFilter { PropertyFilter::lt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } + fn gt(&self, value: impl Into) -> PropertyFilter { PropertyFilter::gt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) } + fn is_in(&self, values: impl IntoIterator) -> PropertyFilter { PropertyFilter::is_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) } + fn is_not_in(&self, values: impl IntoIterator) -> PropertyFilter { PropertyFilter::is_not_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) } + fn is_none(&self) -> PropertyFilter { PropertyFilter::is_none(self.property_ref()).with_ops(self.ops().iter().copied()) } + fn is_some(&self) -> PropertyFilter { PropertyFilter::is_some(self.property_ref()).with_ops(self.ops().iter().copied()) } + fn starts_with(&self, value: impl Into) -> PropertyFilter { PropertyFilter::starts_with(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } + fn ends_with(&self, value: impl Into) -> PropertyFilter { PropertyFilter::ends_with(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } + fn contains(&self, value: impl Into) -> PropertyFilter { PropertyFilter::contains(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } + fn not_contains(&self, value: impl Into) -> PropertyFilter { PropertyFilter::not_contains(self.property_ref(), value.into()) .with_ops(self.ops().iter().copied()) } + fn fuzzy_search( &self, prop_value: impl Into, @@ -1713,6 +1727,7 @@ pub trait ElemQualifierOps: InternalPropertyFilterOps { } } } + impl ElemQualifierOps for T {} impl PropertyFilterBuilder { @@ -1733,6 +1748,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } + fn sum(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), @@ -1740,6 +1756,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } + fn avg(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), @@ -1747,6 +1764,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } + fn min(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), @@ -1754,6 +1772,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } + fn max(&self) -> OpChainBuilder { OpChainBuilder { prop_ref: self.property_ref(), @@ -1761,5 +1780,22 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { _phantom: PhantomData, } } + + fn first(&self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::First]).collect(), + _phantom: PhantomData, + } + } + + fn last(&self) -> OpChainBuilder { + OpChainBuilder { + prop_ref: self.property_ref(), + ops: self.ops().iter().copied().chain([Op::Last]).collect(), + _phantom: PhantomData, + } + } } + impl ListAggOps for T {} diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index 8a7dd3525c..d9a9076157 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -12,7 +12,7 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, python::filter::{ - create_filter::DynInternalFilterOps, property_filter_builders::PyPropertyFilterOps, + create_filter::DynInternalFilterOps, property_filter_builders::PyPropertyFilterBuilder, }, }; use pyo3::{exceptions::PyTypeError, prelude::*}; @@ -64,24 +64,3 @@ impl CreateFilter for PyFilterExpr { self.0.create_filter(graph) } } - -pub enum AcceptFilter { - Expr(PyFilterExpr), - Chain(Py), -} - -impl<'py> FromPyObject<'py> for AcceptFilter { - fn extract_bound(ob: &Bound<'py, PyAny>) -> PyResult { - if let Ok(expr) = ob.extract::() { - return Ok(AcceptFilter::Expr(expr)); - } - - if let Ok(chain) = ob.extract::>() { - return Ok(AcceptFilter::Chain(chain)); - } - - Err(PyTypeError::new_err( - "Expected a FilterExpr or a PropertyFilterOps chain", - )) - } -} diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 2a896ec88c..1253cb050f 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -2,9 +2,7 @@ use crate::python::filter::{ edge_filter_builders::{PyEdgeEndpoint, PyEdgeFilter, PyEdgeFilterOp, PyExplodedEdgeFilter}, filter_expr::PyFilterExpr, node_filter_builders::PyNodeFilter, - property_filter_builders::{ - PyMetadataFilterBuilder, PyPropertyFilterBuilder, PyPropertyFilterOps, - }, + property_filter_builders::{PyFilterOps, PyPropertyFilterBuilder}, }; use pyo3::{ prelude::{PyModule, PyModuleMethods}, @@ -20,14 +18,13 @@ pub mod property_filter_builders; pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> { let filter_module = PyModule::new(py, "filter")?; filter_module.add_class::()?; - filter_module.add_class::()?; + filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; - filter_module.add_class::()?; - filter_module.add_class::()?; + filter_module.add_class::()?; Ok(filter_module) } diff --git a/raphtory/src/python/filter/property_filter_builders.rs b/raphtory/src/python/filter/property_filter_builders.rs index f44729b590..ce829c1ec8 100644 --- a/raphtory/src/python/filter/property_filter_builders.rs +++ b/raphtory/src/python/filter/property_filter_builders.rs @@ -12,9 +12,7 @@ use crate::{ prelude::PropertyFilter, python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; -use pyo3::{ - exceptions::PyTypeError, pyclass, pymethods, Bound, IntoPyObject, PyErr, PyResult, Python, -}; +use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyErr, PyResult, Python}; use raphtory_api::core::entities::properties::prop::Prop; use std::sync::Arc; @@ -54,163 +52,29 @@ pub trait DynFilterOps: Send + Sync { prefix_match: bool, ) -> PyFilterExpr; - fn any(&self) -> PyResult; - - fn all(&self) -> PyResult; - - fn len(&self) -> PyResult; - - fn sum(&self) -> PyResult; - - fn avg(&self) -> PyResult; - - fn min(&self) -> PyResult; - - fn max(&self) -> PyResult; - - fn first(&self) -> PyResult; - - fn last(&self) -> PyResult; - - fn temporal(&self) -> PyResult; -} - -impl DynFilterOps for Arc { - #[inline] - fn __eq__(&self, v: Prop) -> PyFilterExpr { - (**self).__eq__(v) - } - - #[inline] - fn __ne__(&self, v: Prop) -> PyFilterExpr { - (**self).__ne__(v) - } - - #[inline] - fn __lt__(&self, v: Prop) -> PyFilterExpr { - (**self).__lt__(v) - } - - #[inline] - fn __le__(&self, v: Prop) -> PyFilterExpr { - (**self).__le__(v) - } - - #[inline] - fn __gt__(&self, v: Prop) -> PyFilterExpr { - (**self).__gt__(v) - } - - #[inline] - fn __ge__(&self, v: Prop) -> PyFilterExpr { - (**self).__ge__(v) - } - - #[inline] - fn is_in(&self, vs: FromIterable) -> PyFilterExpr { - (**self).is_in(vs) - } - - #[inline] - fn is_not_in(&self, vs: FromIterable) -> PyFilterExpr { - (**self).is_not_in(vs) - } - - #[inline] - fn is_none(&self) -> PyFilterExpr { - (**self).is_none() - } - - #[inline] - fn is_some(&self) -> PyFilterExpr { - (**self).is_some() - } - - #[inline] - fn starts_with(&self, v: Prop) -> PyFilterExpr { - (**self).starts_with(v) - } - - #[inline] - fn ends_with(&self, v: Prop) -> PyFilterExpr { - (**self).ends_with(v) - } - - #[inline] - fn contains(&self, v: Prop) -> PyFilterExpr { - (**self).contains(v) - } + fn any(&self) -> PyResult; - #[inline] - fn not_contains(&self, v: Prop) -> PyFilterExpr { - (**self).not_contains(v) - } + fn all(&self) -> PyResult; - #[inline] - fn fuzzy_search( - &self, - prop_value: String, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - (**self).fuzzy_search(prop_value, levenshtein_distance, prefix_match) - } + fn len(&self) -> PyResult; - #[inline] - fn any(&self) -> PyResult { - (**self).any() - } + fn sum(&self) -> PyResult; - #[inline] - fn all(&self) -> PyResult { - (**self).all() - } + fn avg(&self) -> PyResult; - #[inline] - fn len(&self) -> PyResult { - (**self).len() - } + fn min(&self) -> PyResult; - #[inline] - fn sum(&self) -> PyResult { - (**self).sum() - } + fn max(&self) -> PyResult; - #[inline] - fn avg(&self) -> PyResult { - (**self).avg() - } - - #[inline] - fn min(&self) -> PyResult { - (**self).min() - } - - #[inline] - fn max(&self) -> PyResult { - (**self).max() - } + fn first(&self) -> PyResult; - #[inline] - fn first(&self) -> PyResult { - (**self).first() - } - - #[inline] - fn last(&self) -> PyResult { - (**self).last() - } - - #[inline] - fn temporal(&self) -> PyResult { - (**self).temporal() - } + fn last(&self) -> PyResult; } -impl DynFilterOps for PropertyFilterBuilder +impl DynFilterOps for T where - M: Clone + Send + Sync + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, + T: PropertyFilterOps + ElemQualifierOps + ListAggOps + Clone + Send + Sync + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, { fn __eq__(&self, value: Prop) -> PyFilterExpr { PyFilterExpr(Arc::new(PropertyFilterOps::eq(self, value))) @@ -282,302 +146,50 @@ where ))) } - fn any(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::any(self))) - } - - fn all(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::all(self))) + fn any(&self) -> PyResult { + Ok(PyFilterOps::wrap(ElemQualifierOps::any(self))) } - fn len(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::len(self))) + fn all(&self) -> PyResult { + Ok(PyFilterOps::wrap(ElemQualifierOps::all(self))) } - fn sum(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::sum(self))) + fn len(&self) -> PyResult { + Ok(PyFilterOps::wrap(ListAggOps::len(self))) } - fn avg(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::avg(self))) + fn sum(&self) -> PyResult { + Ok(PyFilterOps::wrap(ListAggOps::sum(self))) } - fn min(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::min(self))) + fn avg(&self) -> PyResult { + Ok(PyFilterOps::wrap(ListAggOps::avg(self))) } - fn max(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::max(self))) + fn min(&self) -> PyResult { + Ok(PyFilterOps::wrap(ListAggOps::min(self))) } - fn first(&self) -> PyResult { - Err(PyTypeError::new_err( - "first() is only valid on temporal properties. Call temporal() first.", - )) + fn max(&self) -> PyResult { + Ok(PyFilterOps::wrap(ListAggOps::max(self))) } - fn last(&self) -> PyResult { - Err(PyTypeError::new_err( - "last() is only valid on temporal properties. Call temporal() first.", - )) + fn first(&self) -> PyResult { + Ok(PyFilterOps::wrap(ListAggOps::first(self))) } - fn temporal(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().temporal())) + fn last(&self) -> PyResult { + Ok(PyFilterOps::wrap(ListAggOps::last(self))) } } -impl DynFilterOps for MetadataFilterBuilder -where - M: Clone + Send + Sync + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn __eq__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::eq(self, value))) - } - - fn __ne__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::ne(self, value))) - } - - fn __lt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::lt(self, value))) - } - - fn __le__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::le(self, value))) - } - - fn __gt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::gt(self, value))) - } - - fn __ge__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::ge(self, value))) - } - - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_in(self, values))) - } - - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_not_in(self, values))) - } - - fn is_none(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_none(self))) - } - - fn is_some(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_some(self))) - } - - fn starts_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::starts_with(self, value))) - } - - fn ends_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::ends_with(self, value))) - } - - fn contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::contains(self, value))) - } - - fn not_contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::not_contains(self, value))) - } - - fn fuzzy_search( - &self, - prop_value: String, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::fuzzy_search( - self, - prop_value, - levenshtein_distance, - prefix_match, - ))) - } - - fn any(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::any(self))) - } - - fn all(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ElemQualifierOps::all(self))) - } - - fn len(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::len(self))) - } - - fn sum(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::sum(self))) - } - - fn avg(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::avg(self))) - } - - fn min(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::min(self))) - } - - fn max(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(ListAggOps::max(self))) - } - - fn first(&self) -> PyResult { - Err(PyTypeError::new_err( - "first() is only valid on temporal properties.", - )) - } - - fn last(&self) -> PyResult { - Err(PyTypeError::new_err( - "last() is only valid on temporal properties.", - )) - } - - fn temporal(&self) -> PyResult { - Err(PyTypeError::new_err( - "temporal() is only valid on standard properties, not metadata.", - )) - } -} - -impl DynFilterOps for OpChainBuilder -where - M: Send + Sync + Clone + 'static, - PropertyFilter: CreateFilter + TryAsCompositeFilter, -{ - fn __eq__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::eq(self, value))) - } - - fn __ne__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::ne(self, value))) - } - - fn __lt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::lt(self, value))) - } - - fn __le__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::le(self, value))) - } - - fn __gt__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::gt(self, value))) - } - - fn __ge__(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::ge(self, value))) - } - - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_in(self, values))) - } - - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_not_in(self, values))) - } - - fn is_none(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_none(self))) - } - - fn is_some(&self) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::is_some(self))) - } - - fn starts_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::starts_with(self, value))) - } - - fn ends_with(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::ends_with(self, value))) - } - - fn contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::contains(self, value))) - } - - fn not_contains(&self, value: Prop) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::not_contains(self, value))) - } - - fn fuzzy_search( - &self, - prop_value: String, - levenshtein_distance: usize, - prefix_match: bool, - ) -> PyFilterExpr { - PyFilterExpr(Arc::new(PropertyFilterOps::fuzzy_search( - self, - prop_value, - levenshtein_distance, - prefix_match, - ))) - } - - fn any(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().any())) - } - - fn all(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().all())) - } - - fn len(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().len())) - } - - fn sum(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().sum())) - } - - fn avg(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().avg())) - } - - fn min(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().min())) - } - - fn max(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().max())) - } - - fn first(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().first())) - } - - fn last(&self) -> PyResult { - Ok(PyPropertyFilterOps::wrap(self.clone().last())) - } - - fn temporal(&self) -> PyResult { - Err(PyTypeError::new_err( - "temporal() must be called on a property builder, not on an existing chain.", - )) - } -} - -#[pyclass( - frozen, - name = "PropertyFilterOps", - module = "raphtory.filter", - subclass -)] -pub struct PyPropertyFilterOps { +#[pyclass(frozen, name = "FilterOps", module = "raphtory.filter", subclass)] +#[derive(Clone)] +pub struct PyFilterOps { ops: Arc, } -impl PyPropertyFilterOps { +impl PyFilterOps { fn wrap(t: T) -> Self { Self { ops: Arc::new(t) } } @@ -588,7 +200,7 @@ impl PyPropertyFilterOps { } #[pymethods] -impl PyPropertyFilterOps { +impl PyFilterOps { fn __eq__(&self, value: Prop) -> PyFilterExpr { self.ops.__eq__(value) } @@ -655,64 +267,84 @@ impl PyPropertyFilterOps { .fuzzy_search(prop_value, levenshtein_distance, prefix_match) } - pub fn first(&self) -> PyResult { + pub fn first(&self) -> PyResult { self.ops.first() } - pub fn last(&self) -> PyResult { + pub fn last(&self) -> PyResult { self.ops.last() } - pub fn any(&self) -> PyResult { + pub fn any(&self) -> PyResult { self.ops.any() } - pub fn all(&self) -> PyResult { + pub fn all(&self) -> PyResult { self.ops.all() } - fn len(&self) -> PyResult { + fn len(&self) -> PyResult { self.ops.len() } - fn sum(&self) -> PyResult { + fn sum(&self) -> PyResult { self.ops.sum() } - fn avg(&self) -> PyResult { + fn avg(&self) -> PyResult { self.ops.avg() } - fn min(&self) -> PyResult { + fn min(&self) -> PyResult { self.ops.min() } - fn max(&self) -> PyResult { + fn max(&self) -> PyResult { self.ops.max() } } +#[pyclass( + frozen, + name = "PropertyFilterOps", + module = "raphtory.filter", + extends = PyFilterOps +)] +#[derive(Clone)] +pub struct PyPropertyFilterBuilder { + ops: Arc, +} + +impl PyPropertyFilterBuilder { + fn wrap(t: T) -> Self { + Self { ops: Arc::new(t) } + } + + fn from_arc(ops: Arc) -> Self { + Self { ops } + } +} + trait DynPropertyFilterBuilderOps: DynFilterOps { - fn as_dyn(self: Arc) -> Arc; + fn temporal(&self) -> PyFilterOps; } -impl DynPropertyFilterBuilderOps for T +impl DynPropertyFilterBuilderOps for PropertyFilterBuilder where - T: DynFilterOps + 'static, + PropertyFilter: CreateFilter + TryAsCompositeFilter, + T: Clone + Send + Sync + 'static, { - fn as_dyn(self: Arc) -> Arc { - self + fn temporal(&self) -> PyFilterOps { + PyFilterOps::wrap(self.clone().temporal()) } } -#[pyclass( - frozen, - name = "Property", - module = "raphtory.filter", - extends = PyPropertyFilterOps -)] -#[derive(Clone)] -pub struct PyPropertyFilterBuilder(Arc); +#[pymethods] +impl PyPropertyFilterBuilder { + fn temporal(&self) -> PyFilterOps { + self.ops.temporal() + } +} impl<'py, M: Clone + Send + Sync + 'static> IntoPyObject<'py> for PropertyFilterBuilder where @@ -724,40 +356,21 @@ where fn into_pyobject(self, py: Python<'py>) -> Result { let inner: Arc> = Arc::new(self); - let parent = PyPropertyFilterOps::from_arc(inner.clone().as_dyn()); - let child = PyPropertyFilterBuilder(inner); + let child = PyPropertyFilterBuilder::from_arc(inner.clone()); + let parent = PyFilterOps::from_arc(inner); Bound::new(py, (child, parent)) } } -#[pymethods] -impl PyPropertyFilterBuilder { - fn temporal(&self) -> PyResult { - self.0.temporal() - } -} - -#[pyclass( - frozen, - name = "Metadata", - module = "raphtory.filter", - extends = PyPropertyFilterOps -)] -#[derive(Clone)] -pub struct PyMetadataFilterBuilder; - impl<'py, M: Send + Sync + Clone + 'static> IntoPyObject<'py> for MetadataFilterBuilder where PropertyFilter: CreateFilter + TryAsCompositeFilter, { - type Target = PyMetadataFilterBuilder; + type Target = PyFilterOps; type Output = Bound<'py, Self::Target>; type Error = PyErr; fn into_pyobject(self, py: Python<'py>) -> Result { - let inner: Arc> = Arc::new(self); - let parent = PyPropertyFilterOps::from_arc(inner.clone().as_dyn()); - let child = PyMetadataFilterBuilder; - Bound::new(py, (child, parent)) + PyFilterOps::wrap(self).into_pyobject(py) } } diff --git a/raphtory/src/python/graph/node.rs b/raphtory/src/python/graph/node.rs index 4614c211e5..9703842ffd 100644 --- a/raphtory/src/python/graph/node.rs +++ b/raphtory/src/python/graph/node.rs @@ -24,7 +24,7 @@ use crate::{ errors::GraphError, prelude::PropertiesOps, python::{ - filter::filter_expr::{AcceptFilter, PyFilterExpr}, + filter::filter_expr::PyFilterExpr, graph::{ node::internal::BaseFilter, properties::{MetadataView, PropertiesView, PyMetadataListList, PyNestedPropsIterable}, diff --git a/raphtory/src/python/graph/views/graph_view.rs b/raphtory/src/python/graph/views/graph_view.rs index 8f0afd18b2..d5d1f9f09e 100644 --- a/raphtory/src/python/graph/views/graph_view.rs +++ b/raphtory/src/python/graph/views/graph_view.rs @@ -33,7 +33,7 @@ use crate::{ errors::GraphError, prelude::*, python::{ - filter::filter_expr::{AcceptFilter, PyFilterExpr}, + filter::filter_expr::PyFilterExpr, graph::{edge::PyEdge, node::PyNode}, types::repr::{Repr, StructReprBuilder}, utils::PyNodeRef, diff --git a/raphtory/src/python/types/macros/trait_impl/filter_ops.rs b/raphtory/src/python/types/macros/trait_impl/filter_ops.rs index ee19eaa7bf..bbc522c12b 100644 --- a/raphtory/src/python/types/macros/trait_impl/filter_ops.rs +++ b/raphtory/src/python/types/macros/trait_impl/filter_ops.rs @@ -18,35 +18,10 @@ macro_rules! impl_filter_ops { #[doc=concat!(" ", $name, ": The filtered view")] fn filter( &self, - filter: AcceptFilter, + filter: PyFilterExpr, ) -> Result<<$base_type as BaseFilter<'static>>::Filtered, GraphError> { - match filter { - AcceptFilter::Expr(expr) => { - Ok(self.$field.clone().filter(expr)?.into_dyn_hop()) - } - AcceptFilter::Chain(chain) => { - // Force Rust-side validation by turning the chain into a temporary FilterExpr. - pyo3::Python::with_gil(|py| -> Result<(), GraphError> { - // Call .is_some() to produce a FilterExpr (any operator is fine). - let obj = chain - .bind(py) - .call_method0("is_some") - .map_err(|e| GraphError::InvalidFilter(e.to_string()))?; - let probe: PyFilterExpr = obj - .extract() - .map_err(|e| GraphError::InvalidFilter(e.to_string()))?; - // This triggers resolve/validate in Rust: - let _ = self.$field.clone().filter(probe)?; - Ok(()) - })?; - - // If we get here, the chain was syntactically valid but not a full FilterExpr. - Err(GraphError::InvalidFilter( - "Expected FilterExpr; got a property chain. Add a comparison (e.g., ... > 2).".into(), - )) - } - } + Ok(self.$field.clone().filter(filter)?.into_dyn_hop()) } } }; From 5502db14a08d8eeaa46711551843244e0e0e4126 Mon Sep 17 00:00:00 2001 From: Shivam <4599890+shivamka1@users.noreply.github.com> Date: Thu, 6 Nov 2025 16:11:47 +0000 Subject: [PATCH 8/8] Features/gql apis (#2350) * impl nodes select filtering in gql * change semantics of filters in gql, add missing filter apis in edges, fix all tests * add more edge filter tests * add filtering to path from node, add tests * fix apply views * rename filter-iter to select * add select as args * impl window filter (#2359) * impl window filter * impl window filter in python, add tests * impl gql window filter, add tests * ref * impl review suggestions * fixes * fix py and gql * add review suggestions --- python/python/raphtory/filter/__init__.pyi | 25 + .../test_filters/test_node_property_filter.py | 35 + .../test_graphql/test_apply_views.py | 8 +- .../test_filters/test_edge_filter_gql.py | 38 +- .../test_graph_edges_property_filter.py | 253 +- .../test_graph_nodes_property_filter.py | 134 +- .../test_filters/test_neighbours_filter.py | 228 +- .../test_filters/test_node_filter_gql.py | 48 +- .../test_nodes_property_filter.py | 398 +++- .../test_graphql/test_nodes.py | 34 +- raphtory-benchmark/benches/search_bench.rs | 80 +- raphtory-graphql/schema.graphql | 62 +- raphtory-graphql/src/model/graph/edge.rs | 23 +- raphtory-graphql/src/model/graph/edges.rs | 30 +- raphtory-graphql/src/model/graph/filtering.rs | 67 +- raphtory-graphql/src/model/graph/graph.rs | 46 +- raphtory-graphql/src/model/graph/node.rs | 93 +- raphtory-graphql/src/model/graph/nodes.rs | 37 +- .../src/model/graph/path_from_node.rs | 29 +- .../graph/storage_ops/time_semantics.rs | 4 +- raphtory/src/db/api/view/filter_ops.rs | 2 +- raphtory/src/db/graph/edges.rs | 9 + raphtory/src/db/graph/path.rs | 9 + raphtory/src/db/graph/views/cached_view.rs | 12 +- .../filter/edge_property_filtered_graph.rs | 72 +- .../filter/exploded_edge_property_filter.rs | 112 +- raphtory/src/db/graph/views/filter/mod.rs | 2055 ++++++++++------- .../graph/views/filter/model/edge_filter.rs | 28 +- .../src/db/graph/views/filter/model/mod.rs | 67 +- .../graph/views/filter/model/node_filter.rs | 16 +- .../views/filter/model/property_filter.rs | 425 ++-- .../filter/node_property_filtered_graph.rs | 92 +- .../views/filter/node_type_filtered_graph.rs | 28 +- raphtory/src/db/graph/views/layer_graph.rs | 44 +- raphtory/src/db/graph/views/node_subgraph.rs | 24 +- raphtory/src/db/graph/views/window_graph.rs | 334 +-- .../src/python/filter/edge_filter_builders.rs | 35 +- raphtory/src/python/filter/filter_expr.rs | 6 +- raphtory/src/python/filter/mod.rs | 6 + .../src/python/filter/node_filter_builders.rs | 21 +- .../python/filter/property_filter_builders.rs | 14 +- raphtory/src/python/filter/window_filter.rs | 79 + raphtory/src/python/graph/edges.rs | 4 +- raphtory/src/python/graph/node.rs | 6 +- raphtory/src/search/edge_filter_executor.rs | 23 +- .../search/exploded_edge_filter_executor.rs | 28 +- raphtory/src/search/graph_index.rs | 12 +- raphtory/src/search/mod.rs | 72 +- raphtory/src/search/node_filter_executor.rs | 24 +- raphtory/src/search/searcher.rs | 12 +- 50 files changed, 3469 insertions(+), 1874 deletions(-) create mode 100644 raphtory/src/python/filter/window_filter.rs diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 4819150fd9..345f300486 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -31,6 +31,10 @@ __all__ = [ "Edge", "ExplodedEdge", "FilterOps", + "PropertyFilterOps", + "NodeWindow", + "EdgeWindow", + "ExplodedEdgeWindow", ] class FilterExpr(object): @@ -84,6 +88,8 @@ class Node(object): @staticmethod def property(name): ... + @staticmethod + def window(py_start, py_end): ... class EdgeFilterOp(object): def __eq__(self, value): @@ -125,12 +131,16 @@ class Edge(object): def property(name): ... @staticmethod def src(): ... + @staticmethod + def window(py_start, py_end): ... class ExplodedEdge(object): @staticmethod def metadata(name): ... @staticmethod def property(name): ... + @staticmethod + def window(py_start, py_end): ... class FilterOps(object): def __eq__(self, value): @@ -169,3 +179,18 @@ class FilterOps(object): def not_contains(self, value): ... def starts_with(self, value): ... def sum(self): ... + +class PropertyFilterOps(FilterOps): + def temporal(self): ... + +class NodeWindow(object): + def metadata(self, name): ... + def property(self, name): ... + +class EdgeWindow(object): + def metadata(self, name): ... + def property(self, name): ... + +class ExplodedEdgeWindow(object): + def metadata(self, name): ... + def property(self, name): ... 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 9cc4e7a20a..4114aef26e 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 @@ -1104,3 +1104,38 @@ def check(graph): graph.filter(filter_expr).nodes.id return check + + +@with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) +def test_filter_nodes_temporal_window_sum_ge(): + def check(graph): + expr = filter.Node.window(1, 2).property("prop5").temporal().last().sum() >= 12 + assert sorted(graph.filter(expr).nodes.id) == ["c"] + + expr = filter.Node.window(1, 2).property("prop5").temporal().last().sum() >= 6 + assert sorted(graph.filter(expr).nodes.id) == ["a", "c"] + + return check + + +@with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) +def test_filter_nodes_two_windows_and(): + def check(graph): + filter1 = ( + filter.Node.window(1, 2).property("prop5").temporal().first().sum() == 6 + ) + filter2 = ( + filter.Node.window(2, 3).property("prop6").temporal().last().sum() == 12 + ) + assert sorted(graph.filter(filter1 & filter2).nodes.id) == ["a"] + + return check + + +@with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) +def test_filter_nodes_window_out_of_range_is_empty(): + def check(graph): + expr = filter.Node.window(10, 20).property("prop5").temporal().sum() >= 0 + assert list(graph.filter(expr).nodes.id) == [] + + return check diff --git a/python/tests/test_base_install/test_graphql/test_apply_views.py b/python/tests/test_base_install/test_graphql/test_apply_views.py index 68dc608c1f..fc85fc945c 100644 --- a/python/tests/test_base_install/test_graphql/test_apply_views.py +++ b/python/tests/test_base_install/test_graphql/test_apply_views.py @@ -1649,7 +1649,13 @@ def test_apply_view_a_lot_of_views(): "graph": { "nodes": { "applyViews": { - "list": [{"history": [1735689600000, 1735776000000], "name": "1"}] + "list": [ + {"history": [1735689600000], "name": "1"}, + {"history": [], "name": "2"}, + {"history": [], "name": "3"}, + {"history": [], "name": "6"}, + {"history": [], "name": "7"}, + ] } } } diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py index e172d489cd..b15f64ac2a 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_edge_filter_gql.py @@ -12,7 +12,7 @@ def test_filter_edges_with_str_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - edgeFilter(filter: { + filterEdges(expr: { src: { field: NODE_ID where: { eq: { str: "3" } } @@ -30,7 +30,7 @@ def test_filter_edges_with_str_ids_for_node_id_eq_gql(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"dst": {"name": "1"}, "src": {"name": "3"}}, @@ -52,7 +52,7 @@ def test_filter_edges_with_num_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - edgeFilter(filter: { + filterEdges(expr: { src: { field: NODE_ID where: { eq: { u64: 1 } } @@ -70,9 +70,39 @@ def test_filter_edges_with_num_ids_for_node_id_eq_gql(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": {"list": [{"src": {"name": "1"}, "dst": {"name": "2"}}]} } } } run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_chained_selection_with_edge_filter(graph): + query = """ + query { + graph(path: "g") { + edges { + select(expr: { dst: { + field: NODE_ID + where: { eq: { u64: 2 } } + } }) { + select(expr: { property: { name: "p2", where: { gt:{ i64: 2 } } } }) { + list { src { name } dst { name } } + } + } + } + } + } + """ + expected_output = { + "graph": { + "edges": { + "select": { + "select": {"list": [{"dst": {"name": "2"}, "src": {"name": "1"}}]} + } + } + } + } + run_graphql_test(query, expected_output, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py index 43519c960a..579f4b3016 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_edges_property_filter.py @@ -1,6 +1,6 @@ import pytest from raphtory import Graph, PersistentGraph -from filters_setup import create_test_graph +from filters_setup import create_test_graph, init_graph2 from utils import run_graphql_test, run_graphql_error_test EVENT_GRAPH = create_test_graph(Graph()) @@ -12,8 +12,8 @@ def test_graph_edge_property_filter_equal(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { + filterEdges( + expr: { property: { name: "eprop5" where: { eq: { list: [{i64: 1},{i64: 2},{i64: 3}] } } @@ -27,7 +27,7 @@ def test_graph_edge_property_filter_equal(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": {"list": [{"src": {"name": "a"}, "dst": {"name": "d"}}]} } } @@ -40,8 +40,8 @@ def test_graph_edge_property_filter_equal_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { + filterEdges( + expr: { property: { name: "eprop5" where: { eq: { i64: 1 } } @@ -64,8 +64,8 @@ def test_graph_edge_property_filter_not_equal(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { + filterEdges( + expr: { property: { name: "eprop4" where: { ne: { bool: true } } @@ -79,7 +79,7 @@ def test_graph_edge_property_filter_not_equal(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": {"list": [{"src": {"name": "c"}, "dst": {"name": "d"}}]} } } @@ -92,8 +92,8 @@ def test_graph_edge_property_filter_not_equal_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { + filterEdges( + expr: { property: { name: "eprop4" where: { ne: { i64: 1 } } @@ -116,8 +116,8 @@ def test_graph_edge_property_filter_greater_than_or_equal(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { + filterEdges( + expr: { property: { name: "eprop1" where: { ge: { i64: 60 } } @@ -131,7 +131,7 @@ def test_graph_edge_property_filter_greater_than_or_equal(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": {"list": [{"src": {"name": "a"}, "dst": {"name": "d"}}]} } } @@ -144,8 +144,8 @@ def test_graph_edge_property_filter_greater_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { + filterEdges( + expr: { property: { name: "eprop1" where: { ge: { bool: true } } @@ -168,8 +168,8 @@ def test_graph_edge_property_filter_less_than_or_equal(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { + filterEdges( + expr: { property: { name: "eprop1" where: { le: { i64: 30 } } @@ -183,7 +183,7 @@ def test_graph_edge_property_filter_less_than_or_equal(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"src": {"name": "b"}, "dst": {"name": "d"}}, @@ -201,8 +201,8 @@ def test_graph_edge_property_filter_less_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { le: { str: "shivam" } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { le: { str: "shivam" } } } } ) { edges { list { src { name } dst { name } } } } @@ -220,8 +220,8 @@ def test_graph_edge_property_filter_greater_than(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { gt: { i64: 30 } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { gt: { i64: 30 } } } } ) { edges { list { src { name } dst { name } } } } @@ -230,7 +230,7 @@ def test_graph_edge_property_filter_greater_than(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": {"list": [{"src": {"name": "a"}, "dst": {"name": "d"}}]} } } @@ -243,8 +243,8 @@ def test_graph_edge_property_filter_greater_than_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { gt: { str: "shivam" } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { gt: { str: "shivam" } } } } ) { edges { list { src { name } dst { name } } } } @@ -262,8 +262,8 @@ def test_graph_edge_property_filter_less_than(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { lt: { i64: 30 } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { lt: { i64: 30 } } } } ) { edges { list { src { name } dst { name } } } } @@ -272,7 +272,7 @@ def test_graph_edge_property_filter_less_than(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": {"list": [{"src": {"name": "b"}, "dst": {"name": "d"}}]} } } @@ -285,8 +285,8 @@ def test_graph_edge_property_filter_less_than_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { lt: { str: "shivam" } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { lt: { str: "shivam" } } } } ) { edges { list { src { name } dst { name } } } } @@ -304,15 +304,15 @@ def test_graph_edge_property_filter_is_none(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop5", where: { isNone: true } } } + filterEdges( + expr: { property: { name: "eprop5", where: { isNone: true } } } ) { edges { list { src { name } dst { name } } } } } } """ - expected_output = {"graph": {"edgeFilter": {"edges": {"list": []}}}} + expected_output = {"graph": {"filterEdges": {"edges": {"list": []}}}} run_graphql_test(query, expected_output, graph) @@ -321,8 +321,8 @@ def test_graph_edge_property_filter_is_some(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop5", where: { isSome: true } } } + filterEdges( + expr: { property: { name: "eprop5", where: { isSome: true } } } ) { edges { list { src { name } dst { name } } } } @@ -331,7 +331,7 @@ def test_graph_edge_property_filter_is_some(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"src": {"name": "a"}, "dst": {"name": "d"}}, @@ -350,8 +350,8 @@ def test_graph_edge_property_filter_is_in(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { isIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { isIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } ) { edges { list { src { name } dst { name } } } } @@ -360,7 +360,7 @@ def test_graph_edge_property_filter_is_in(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"src": {"name": "b"}, "dst": {"name": "d"}}, @@ -378,15 +378,15 @@ def test_graph_edge_property_filter_is_empty_list(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { isIn: { list: [] } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { isIn: { list: [] } } } } ) { edges { list { src { name } dst { name } } } } } } """ - expected_output = {"graph": {"edgeFilter": {"edges": {"list": []}}}} + expected_output = {"graph": {"filterEdges": {"edges": {"list": []}}}} run_graphql_test(query, expected_output, graph) @@ -395,8 +395,8 @@ def test_graph_edge_property_filter_is_in_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { isIn: { str: "shivam" } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { isIn: { str: "shivam" } } } } ) { edges { list { src { name } dst { name } } } } @@ -414,8 +414,8 @@ def test_graph_edge_property_filter_is_not_in(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { isNotIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { isNotIn: { list: [{i64: 10},{i64: 20},{i64: 30}] } } } } ) { edges { list { src { name } dst { name } } } } @@ -424,7 +424,7 @@ def test_graph_edge_property_filter_is_not_in(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": {"list": [{"src": {"name": "a"}, "dst": {"name": "d"}}]} } } @@ -437,8 +437,8 @@ def test_graph_edge_property_filter_is_not_in_empty_list(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { isNotIn: { list: [] } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { isNotIn: { list: [] } } } } ) { edges { list { src { name } dst { name } } } } @@ -447,7 +447,7 @@ def test_graph_edge_property_filter_is_not_in_empty_list(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"src": {"name": "a"}, "dst": {"name": "d"}}, @@ -466,8 +466,8 @@ def test_graph_edge_property_filter_is_not_in_type_error(graph): query = """ query { graph(path: "g") { - edgeFilter( - filter: { property: { name: "eprop1", where: { isNotIn: { str: "shivam" } } } } + filterEdges( + expr: { property: { name: "eprop1", where: { isNotIn: { str: "shivam" } } } } ) { edges { list { src { name } dst { name } } } } @@ -485,8 +485,8 @@ def test_graph_edge_not_property_filter(graph): query = """ query { graph(path: "g") { - edgeFilter ( - filter: { + filterEdges ( + expr: { not: { property: { name: "eprop5" @@ -502,7 +502,7 @@ def test_graph_edge_not_property_filter(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"dst": {"name": "d"}, "src": {"name": "a"}}, @@ -521,7 +521,7 @@ def test_edges_property_filter_starts_with(graph): query = """ query { graph(path: "g") { - edgeFilter(filter: { + filterEdges(expr: { property: { name: "eprop3" where: { startsWith: { str: "xyz" } } @@ -534,7 +534,7 @@ def test_edges_property_filter_starts_with(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"src": {"name": "a"}, "dst": {"name": "d"}}, @@ -553,7 +553,7 @@ def test_edges_property_filter_ends_with(graph): query = """ query { graph(path: "g") { - edgeFilter(filter: { + filterEdges(expr: { property: { name: "eprop3" where: { endsWith: { str: "123" } } @@ -566,7 +566,7 @@ def test_edges_property_filter_ends_with(graph): """ expected_output = { "graph": { - "edgeFilter": { + "filterEdges": { "edges": { "list": [ {"src": {"name": "a"}, "dst": {"name": "d"}}, @@ -578,3 +578,136 @@ def test_edges_property_filter_ends_with(graph): } } run_graphql_test(query, expected_output, graph) + + +EVENT_GRAPH = init_graph2(Graph()) +PERSISTENT_GRAPH = init_graph2(PersistentGraph()) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_selection(graph): + query = """ + query { + graph(path: "g") { + edges(select: { property: { name: "p2", where: { gt: { i64: 3 } } } }) { + list { src { name } dst { name } } + } + } + } + """ + expected_output = { + "graph": { + "edges": { + "list": [ + {"dst": {"name": "2"}, "src": {"name": "1"}}, + {"dst": {"name": "1"}, "src": {"name": "3"}}, + {"dst": {"name": "4"}, "src": {"name": "3"}}, + {"dst": {"name": "1"}, "src": {"name": "2"}}, + ] + } + } + } + run_graphql_test(query, expected_output, graph) + + +# The inner edges filter has no effect on the list of edges returned from selection filter +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_selection_edges_filter_paired(graph): + query = """ + query { + graph(path: "g") { + edges(select: { property: { name: "p2", where: { gt: { i64: 3 } } } }) { + filter(expr:{ + property: { name: "p3", where: { eq:{ i64: 5 } } } + }) { + list { src { name } dst { name } } + } + } + } + } + """ + expected_output = { + "graph": { + "edges": { + "filter": { + "list": [ + {"dst": {"name": "2"}, "src": {"name": "1"}}, + {"dst": {"name": "1"}, "src": {"name": "3"}}, + {"dst": {"name": "4"}, "src": {"name": "3"}}, + {"dst": {"name": "1"}, "src": {"name": "2"}}, + ] + } + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_chained_selection_edges_filter_paired(graph): + query = """ + query { + graph(path: "g") { + edges(select: { property: { name: "p2", where: { gt: { i64: 3 } } } }) { + select(expr: { property: { name: "p2", where: { lt: { i64: 5 } } } }) { + filter(expr: { + dst: { + field: NODE_ID + where: { eq: { u64: 2 } } + } + }) { + list { src { name } dst { name } } + } + } + } + } + } + """ + expected_output = { + "graph": { + "edges": { + "select": { + "filter": {"list": [{"dst": {"name": "2"}, "src": {"name": "1"}}]} + } + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_chained_selection_edges_filter_paired_ver2(graph): + query = """ + query { + graph(path: "g") { + edges { + select(expr: { property: { name: "p2", where: { gt: { i64: 3 } } } }) { + select(expr: { property: { name: "p2", where: { lt: { i64: 5 } } } }) { + filter(expr: { + dst: { + field: NODE_ID + where: { eq: { u64: 2 } } + } + }) { + list { src { name } dst { name } } + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "edges": { + "select": { + "select": { + "filter": { + "list": [{"dst": {"name": "2"}, "src": {"name": "1"}}] + } + } + } + } + } + } + run_graphql_test(query, expected_output, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py index 95128896ea..a8f088b278 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_graph_nodes_property_filter.py @@ -12,8 +12,8 @@ def test_graph_node_property_filter_equal(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { property: { name: "prop5" where: { eq: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } } @@ -25,7 +25,7 @@ def test_graph_node_property_filter_equal(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -34,8 +34,8 @@ def test_graph_node_property_filter_equal_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { property: { name: "prop5" where: { eq: { i64: 1 } } @@ -58,8 +58,8 @@ def test_graph_node_property_filter_not_equal(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { property: { name: "prop4" where: { ne: { bool: true } } @@ -72,7 +72,7 @@ def test_graph_node_property_filter_not_equal(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "b"}, {"name": "d"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "b"}, {"name": "d"}]}}} } run_graphql_test(query, expected_output, graph) @@ -82,8 +82,8 @@ def test_graph_node_property_filter_not_equal_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { property: { name: "prop4" where: { ne: { i64: 1 } } @@ -106,8 +106,8 @@ def test_graph_node_property_filter_greater_than_or_equal(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { property: { name: "prop1" where: { ge: { i64: 60 } } @@ -119,7 +119,7 @@ def test_graph_node_property_filter_greater_than_or_equal(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -128,8 +128,8 @@ def test_graph_node_property_filter_greater_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { property: { name: "prop1" where: { ge: { bool: true } } @@ -152,8 +152,8 @@ def test_graph_node_property_filter_less_than_or_equal(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { property: { name: "prop1", where: { le: { i64: 30 } } } } ) { @@ -164,7 +164,7 @@ def test_graph_node_property_filter_less_than_or_equal(graph): """ expected_output = { "graph": { - "nodeFilter": { + "filterNodes": { "nodes": {"list": [{"name": "b"}, {"name": "c"}, {"name": "d"}]} } } @@ -177,8 +177,8 @@ def test_graph_node_property_filter_less_than_or_equal_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { le: { str: "shivam" } } } } + filterNodes( + expr: { property: { name: "prop1", where: { le: { str: "shivam" } } } } ) { nodes { list { name } } } @@ -196,15 +196,15 @@ def test_graph_node_property_filter_greater_than(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { gt: { i64: 30 } } } } + filterNodes( + expr: { property: { name: "prop1", where: { gt: { i64: 30 } } } } ) { nodes { list { name } } } } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -213,8 +213,8 @@ def test_graph_node_property_filter_greater_than_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { gt: { str: "shivam" } } } } + filterNodes( + expr: { property: { name: "prop1", where: { gt: { str: "shivam" } } } } ) { nodes { list { name } } } @@ -232,8 +232,8 @@ def test_graph_node_property_filter_less_than(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { lt: { i64: 30 } } } } + filterNodes( + expr: { property: { name: "prop1", where: { lt: { i64: 30 } } } } ) { nodes { list { name } } } @@ -241,7 +241,7 @@ def test_graph_node_property_filter_less_than(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "b"}, {"name": "c"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "b"}, {"name": "c"}]}}} } run_graphql_test(query, expected_output, graph) @@ -251,8 +251,8 @@ def test_graph_node_property_filter_less_than_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { lt: { str: "shivam" } } } } + filterNodes( + expr: { property: { name: "prop1", where: { lt: { str: "shivam" } } } } ) { nodes { list { name } } } @@ -270,8 +270,8 @@ def test_graph_node_property_filter_is_none(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop5", where: { isNone: true } } } + filterNodes( + expr: { property: { name: "prop5", where: { isNone: true } } } ) { nodes { list { name } } } @@ -279,7 +279,7 @@ def test_graph_node_property_filter_is_none(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "b"}, {"name": "d"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "b"}, {"name": "d"}]}}} } run_graphql_test(query, expected_output, graph) @@ -289,8 +289,8 @@ def test_graph_node_property_filter_is_some(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop5", where: { isSome: true } } } + filterNodes( + expr: { property: { name: "prop5", where: { isSome: true } } } ) { nodes { list { name } } } @@ -298,7 +298,7 @@ def test_graph_node_property_filter_is_some(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} } run_graphql_test(query, expected_output, graph) @@ -308,8 +308,8 @@ def test_graph_node_property_filter_is_in(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { isIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } + filterNodes( + expr: { property: { name: "prop1", where: { isIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } ) { nodes { list { name } } } @@ -317,7 +317,7 @@ def test_graph_node_property_filter_is_in(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "b"}, {"name": "d"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "b"}, {"name": "d"}]}}} } run_graphql_test(query, expected_output, graph) @@ -328,8 +328,8 @@ def test_node_property_filter_is_in_empty_list(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { property: { name: "prop1", where: { isIn: { list: [] } } } } + select( + expr: { property: { name: "prop1", where: { isIn: { list: [] } } } } ) { list { name } } @@ -337,7 +337,7 @@ def test_node_property_filter_is_in_empty_list(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": []}}}} + expected_output = {"graph": {"nodes": {"select": {"list": []}}}} run_graphql_test(query, expected_output, graph) @@ -347,15 +347,15 @@ def test_graph_node_property_filter_is_in_no_value(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { isIn: { list: [] } } } } + filterNodes( + expr: { property: { name: "prop1", where: { isIn: { list: [] } } } } ) { nodes { list { name } } } } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": []}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": []}}}} run_graphql_test(query, expected_output, graph) @@ -364,8 +364,8 @@ def test_graph_node_property_filter_is_in_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { isIn: { str: "shivam" } } } } + filterNodes( + expr: { property: { name: "prop1", where: { isIn: { str: "shivam" } } } } ) { nodes { list { name } } } @@ -383,8 +383,8 @@ def test_graph_node_property_filter_is_not_in_any(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { isNotIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } + filterNodes( + expr: { property: { name: "prop1", where: { isNotIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } } } ) { nodes { list { name } } } @@ -392,7 +392,7 @@ def test_graph_node_property_filter_is_not_in_any(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} } run_graphql_test(query, expected_output, graph) @@ -403,8 +403,8 @@ def test_node_property_filter_not_is_not_in_empty_list(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { property: { name: "prop1", where: { isNotIn: { list: [] } } } } + filter( + expr: { property: { name: "prop1", where: { isNotIn: { list: [] } } } } ) { list { name } } @@ -415,7 +415,7 @@ def test_node_property_filter_not_is_not_in_empty_list(graph): expected_output = { "graph": { "nodes": { - "nodeFilter": { + "filter": { "list": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}] } } @@ -429,8 +429,8 @@ def test_graph_node_property_filter_is_not_in_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { property: { name: "prop1", where: { isNotIn: { str: "shivam" } } } } + filterNodes( + expr: { property: { name: "prop1", where: { isNotIn: { str: "shivam" } } } } ) { nodes { list { name } } } @@ -448,8 +448,8 @@ def test_graph_node_not_property_filter(graph): query = """ query { graph(path: "g") { - nodeFilter ( - filter: { + filterNodes ( + expr: { not: { property: { name: "prop5" @@ -465,7 +465,7 @@ def test_graph_node_not_property_filter(graph): """ expected_output = { "graph": { - "nodeFilter": { + "filterNodes": { "nodes": { "list": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}] } @@ -481,7 +481,7 @@ def test_graph_node_type_and_property_filter(graph): query { graph(path: "g") { nodes { - nodeFilter(filter: { + select(expr: { and: [ { node: { @@ -507,7 +507,7 @@ def test_graph_node_type_and_property_filter(graph): expected_output = { "graph": { "nodes": { - "nodeFilter": { + "select": { "count": 3, "list": [{"name": "a"}, {"name": "b"}, {"name": "c"}], } @@ -522,7 +522,7 @@ def test_graph_nodes_property_filter_starts_with(graph): query = """ query { graph(path: "g") { - nodeFilter(filter: { + filterNodes(expr: { property: { name: "prop3" where: { startsWith: { str: "abc" } } @@ -535,7 +535,7 @@ def test_graph_nodes_property_filter_starts_with(graph): """ expected_output = { "graph": { - "nodeFilter": { + "filterNodes": { "nodes": { "list": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}] } @@ -550,7 +550,7 @@ def test_graph_nodes_property_filter_ends_with(graph): query = """ query { graph(path: "g") { - nodeFilter(filter: { + filterNodes(expr: { property: { name: "prop3" where: { endsWith: { str: "123" } } @@ -561,7 +561,7 @@ def test_graph_nodes_property_filter_ends_with(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -570,8 +570,8 @@ def test_graph_nodes_property_filter_starts_with_temporal_any(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { temporalProperty: { name: "prop3", where: { any: { startsWith: { str: "abc1" } } } @@ -583,5 +583,5 @@ def test_graph_nodes_property_filter_starts_with_temporal_any(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py index 678a295d6e..7079132e49 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_neighbours_filter.py @@ -1,6 +1,6 @@ import pytest from raphtory import Graph, PersistentGraph -from filters_setup import create_test_graph +from filters_setup import create_test_graph, init_graph2 from utils import run_graphql_test, run_graphql_error_test EVENT_GRAPH = create_test_graph(Graph()) @@ -13,7 +13,7 @@ def test_out_neighbours_found(graph): query { graph(path: "g") { node(name: "a") { - nodeFilter(filter: { + filter(expr: { and: [ { node: { @@ -38,7 +38,48 @@ def test_out_neighbours_found(graph): } """ expected_output = { - "graph": {"node": {"nodeFilter": {"outNeighbours": {"list": [{"name": "d"}]}}}} + "graph": {"node": {"filter": {"outNeighbours": {"list": [{"name": "d"}]}}}} + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_out_neighbours_found_select(graph): + query = """ + query { + graph(path: "g") { + node(name: "a") { + filter(expr: { + and: [ + { + node: { + field: NODE_NAME, + where: { eq: { str: "d" } } + } + }, + { + property: { + name: "prop1" + where: { gt: { i64: 10 } } + } + } + ] + }) { + outNeighbours(select: { + node: { + field: NODE_NAME + where: { eq: { str: "d" } } + } + }) { + list { name } + } + } + } + } + } + """ + expected_output = { + "graph": {"node": {"filter": {"outNeighbours": {"list": [{"name": "d"}]}}}} } run_graphql_test(query, expected_output, graph) @@ -49,7 +90,7 @@ def test_out_neighbours_not_found(graph): query { graph(path: "g") { node(name: "a") { - nodeFilter(filter: { + filter(expr: { node: { field: NODE_NAME, where: { eq: { str: "e" } } @@ -63,9 +104,7 @@ def test_out_neighbours_not_found(graph): } } """ - expected_output = { - "graph": {"node": {"nodeFilter": {"outNeighbours": {"list": []}}}} - } + expected_output = {"graph": {"node": {"filter": {"outNeighbours": {"list": []}}}}} run_graphql_test(query, expected_output, graph) @@ -75,7 +114,7 @@ def test_in_neighbours_found(graph): query { graph(path: "g") { node(name: "d") { - nodeFilter(filter: { + filter(expr: { property: { name: "prop1" where: { gt: { i64: 10 } } @@ -92,9 +131,40 @@ def test_in_neighbours_found(graph): expected_output = { "graph": { "node": { - "nodeFilter": {"inNeighbours": {"list": [{"name": "a"}, {"name": "c"}]}} + "filter": {"inNeighbours": {"list": [{"name": "a"}, {"name": "c"}]}} + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_in_neighbours_found_select(graph): + query = """ + query { + graph(path: "g") { + node(name: "d") { + filter(expr: { + property: { + name: "prop1" + where: { gt: { i64: 10 } } + } + }) { + inNeighbours(select: { + node: { + field: NODE_NAME + where: { eq: { str: "c" } } + } + }) { + list { name } + } + } } + } } + """ + expected_output = { + "graph": {"node": {"filter": {"inNeighbours": {"list": [{"name": "c"}]}}}} } run_graphql_test(query, expected_output, graph) @@ -105,7 +175,7 @@ def test_in_neighbours_not_found(graph): query { graph(path: "g") { node(name: "d") { - nodeFilter(filter: { + filter(expr: { node: { field: NODE_NAME, where: { eq: { str: "e" } } @@ -119,9 +189,7 @@ def test_in_neighbours_not_found(graph): } } """ - expected_output = { - "graph": {"node": {"nodeFilter": {"inNeighbours": {"list": []}}}} - } + expected_output = {"graph": {"node": {"filter": {"inNeighbours": {"list": []}}}}} run_graphql_test(query, expected_output, graph) @@ -131,7 +199,7 @@ def test_neighbours_found(graph): query { graph(path: "g") { node(name: "d") { - nodeFilter(filter: { + filter(expr: { node: { field: NODE_NAME, where: { ne: { str: "a" } } @@ -147,10 +215,39 @@ def test_neighbours_found(graph): """ expected_output = { "graph": { - "node": { - "nodeFilter": {"neighbours": {"list": [{"name": "b"}, {"name": "c"}]}} + "node": {"filter": {"neighbours": {"list": [{"name": "b"}, {"name": "c"}]}}} + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_neighbours_found_select(graph): + query = """ + query { + graph(path: "g") { + node(name: "d") { + filter(expr: { + node: { + field: NODE_NAME, + where: { ne: { str: "a" } } + } + }) { + neighbours(select: { + node: { + field: NODE_NAME + where: { eq: { str: "b" } } + } + }) { + list { name } + } + } } + } } + """ + expected_output = { + "graph": {"node": {"filter": {"neighbours": {"list": [{"name": "b"}]}}}} } run_graphql_test(query, expected_output, graph) @@ -161,7 +258,7 @@ def test_neighbours_not_found(graph): query { graph(path: "g") { node(name: "d") { - nodeFilter(filter: { + filter(expr: { node: { field: NODE_NAME, where: { eq: { str: "e" } } @@ -175,5 +272,100 @@ def test_neighbours_not_found(graph): } } """ - expected_output = {"graph": {"node": {"nodeFilter": {"neighbours": {"list": []}}}}} + expected_output = {"graph": {"node": {"filter": {"neighbours": {"list": []}}}}} + run_graphql_test(query, expected_output, graph) + + +EVENT_GRAPH = init_graph2(Graph()) +PERSISTENT_GRAPH = init_graph2(PersistentGraph()) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_neighbours_selection(graph): + query = """ + query { + graph(path: "g") { + nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + list { + neighbours { + select(expr: { + property: { name: "p2", where: { gt: { i64: 3 } } } + }) { + list { + name + } + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "list": [ + {"neighbours": {"select": {"list": [{"name": "3"}]}}}, + {"neighbours": {"select": {"list": []}}}, + ] + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_neighbours_neighbours_filtering(graph): + query = """ + query { + graph(path: "g") { + nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + list { + neighbours { + filter(expr: { + property: { name: "p2", where: { gt: { i64: 3 } } } + }) { + list { + neighbours { + list { + name + } + } + } + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "list": [ + { + "neighbours": { + "filter": { + "list": [ + {"neighbours": {"list": [{"name": "3"}]}}, + {"neighbours": {"list": []}}, + ] + } + } + }, + { + "neighbours": { + "filter": { + "list": [ + {"neighbours": {"list": [{"name": "3"}]}}, + {"neighbours": {"list": [{"name": "3"}]}}, + {"neighbours": {"list": [{"name": "3"}]}}, + ] + } + } + }, + ] + } + } + } run_graphql_test(query, expected_output, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py index c0e9737024..6eed06369d 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_node_filter_gql.py @@ -12,8 +12,8 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { node: { field: NODE_ID where: { eq: { str: "1" } } @@ -27,7 +27,7 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "1"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "1"}]}}}} run_graphql_test(query, expected_output, graph) @@ -36,8 +36,8 @@ def test_filter_nodes_with_str_ids_for_node_id_eq_gql2(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { node: { field: NODE_ID where: { eq: { u64: 1 } } @@ -64,8 +64,8 @@ def test_filter_nodes_with_num_ids_for_node_id_eq_gql(graph): query = """ query { graph(path: "g") { - nodeFilter( - filter: { + filterNodes( + expr: { node: { field: NODE_ID where: { eq: { u64: 1 } } @@ -79,5 +79,37 @@ def test_filter_nodes_with_num_ids_for_node_id_eq_gql(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "1"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "1"}]}}}} + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_chained_selection_with_node_filter(graph): + query = """ + query { + graph(path: "g") { + nodes { + select(expr: { node: { + field: NODE_TYPE + where: { eq: { str: "fire_nation" } } + } }) { + select(expr: { property: { name: "p9", where: { eq:{ i64: 5 } } } }) { + filter(expr:{ + property: { name: "p100", where: { gt: { i64: 30 } } } + }) { + list { + name + } + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": {"select": {"select": {"filter": {"list": [{"name": "1"}]}}}} + } + } run_graphql_test(query, expected_output, graph) diff --git a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py index 54923bb4e9..c281ee5a22 100644 --- a/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py +++ b/python/tests/test_base_install/test_graphql/test_filters/test_nodes_property_filter.py @@ -5,6 +5,7 @@ create_test_graph2, create_test_graph3, init_graph, + init_graph2, ) from utils import run_graphql_test, run_graphql_error_test @@ -13,13 +14,13 @@ @pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) -def test_node_property_filter_equal(graph): +def test_node_property_filter_equal2(graph): query = """ query { graph(path: "g") { nodes { - nodeFilter( - filter: { + filter( + expr: { property: { name: "prop5" where: { @@ -29,14 +30,57 @@ def test_node_property_filter_equal(graph): } ) { list { - name + neighbours { + list { + name + } + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "filter": { + "list": [ + {"neighbours": {"list": []}}, + {"neighbours": {"list": []}}, + {"neighbours": {"list": []}}, + {"neighbours": {"list": [{"name": "a"}]}}, + ] + } } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_node_property_filter_equal3(graph): + query = """ + query { + graph(path: "g") { + nodes { + select( + expr: { + property: { + name: "prop5" + where: { + eq: { list: [ {i64: 1}, {i64: 2}, {i64: 3} ] } + } + } + } + ) { + list { name } } } } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"nodes": {"select": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -46,8 +90,8 @@ def test_node_property_filter_equal_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop5" where: { @@ -76,8 +120,8 @@ def test_node_property_filter_not_equal(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop4" where: { @@ -95,7 +139,7 @@ def test_node_property_filter_not_equal(graph): } """ expected_output = { - "graph": {"nodes": {"nodeFilter": {"list": [{"name": "b"}, {"name": "d"}]}}} + "graph": {"nodes": {"select": {"list": [{"name": "b"}, {"name": "d"}]}}} } run_graphql_test(query, expected_output, graph) @@ -106,8 +150,8 @@ def test_node_property_filter_not_equal_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop4" where: { @@ -136,8 +180,8 @@ def test_node_property_filter_greater_than_or_equal(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { @@ -154,7 +198,7 @@ def test_node_property_filter_greater_than_or_equal(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"nodes": {"select": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -164,8 +208,8 @@ def test_node_property_filter_greater_than_or_equal_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { @@ -194,8 +238,8 @@ def test_node_property_filter_less_than_or_equal(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { @@ -214,9 +258,7 @@ def test_node_property_filter_less_than_or_equal(graph): """ expected_output = { "graph": { - "nodes": { - "nodeFilter": {"list": [{"name": "b"}, {"name": "c"}, {"name": "d"}]} - } + "nodes": {"select": {"list": [{"name": "b"}, {"name": "c"}, {"name": "d"}]}} } } run_graphql_test(query, expected_output, graph) @@ -228,8 +270,8 @@ def test_node_property_filter_less_than_or_equal_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { le: { str: "shivam" } } @@ -254,8 +296,8 @@ def test_node_property_filter_greater_than(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { gt: { i64: 30 } } @@ -268,7 +310,7 @@ def test_node_property_filter_greater_than(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"nodes": {"select": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -278,8 +320,8 @@ def test_node_property_filter_greater_than_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { gt: { str: "shivam" } } @@ -304,8 +346,8 @@ def test_node_property_filter_less_than(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { lt: { i64: 30 } } @@ -319,7 +361,7 @@ def test_node_property_filter_less_than(graph): } """ expected_output = { - "graph": {"nodes": {"nodeFilter": {"list": [{"name": "b"}, {"name": "c"}]}}} + "graph": {"nodes": {"select": {"list": [{"name": "b"}, {"name": "c"}]}}} } run_graphql_test(query, expected_output, graph) @@ -330,8 +372,8 @@ def test_node_property_filter_less_than_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { lt: { str: "shivam" } } @@ -356,8 +398,8 @@ def test_node_property_filter_is_none(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop5" where: { isNone: true } @@ -371,7 +413,7 @@ def test_node_property_filter_is_none(graph): } """ expected_output = { - "graph": {"nodes": {"nodeFilter": {"list": [{"name": "b"}, {"name": "d"}]}}} + "graph": {"nodes": {"select": {"list": [{"name": "b"}, {"name": "d"}]}}} } run_graphql_test(query, expected_output, graph) @@ -382,8 +424,8 @@ def test_node_property_filter_is_some(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop5" where: { isSome: true } @@ -397,7 +439,7 @@ def test_node_property_filter_is_some(graph): } """ expected_output = { - "graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}, {"name": "c"}]}}} + "graph": {"nodes": {"select": {"list": [{"name": "a"}, {"name": "c"}]}}} } run_graphql_test(query, expected_output, graph) @@ -408,8 +450,8 @@ def test_node_property_filter_is_in(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { isIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } @@ -423,7 +465,7 @@ def test_node_property_filter_is_in(graph): } """ expected_output = { - "graph": {"nodes": {"nodeFilter": {"list": [{"name": "b"}, {"name": "d"}]}}} + "graph": {"nodes": {"select": {"list": [{"name": "b"}, {"name": "d"}]}}} } run_graphql_test(query, expected_output, graph) @@ -434,8 +476,8 @@ def test_node_property_filter_is_in_empty_list(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { isIn: { list: [] } } @@ -448,7 +490,7 @@ def test_node_property_filter_is_in_empty_list(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": []}}}} + expected_output = {"graph": {"nodes": {"select": {"list": []}}}} run_graphql_test(query, expected_output, graph) @@ -459,8 +501,8 @@ def test_node_property_filter_is_in_no_value(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { isIn: { list: [{i64: 100}] } } @@ -473,7 +515,7 @@ def test_node_property_filter_is_in_no_value(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": []}}}} + expected_output = {"graph": {"nodes": {"select": {"list": []}}}} run_graphql_test(query, expected_output, graph) @@ -483,8 +525,8 @@ def test_node_property_filter_is_in_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { isIn: { str: "shivam" } } @@ -509,8 +551,8 @@ def test_node_property_filter_is_not_in(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { isNotIn: { list: [{i64: 10},{i64: 30},{i64: 50},{i64: 70}] } } @@ -524,7 +566,7 @@ def test_node_property_filter_is_not_in(graph): } """ expected_output = { - "graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}, {"name": "c"}]}}} + "graph": {"nodes": {"select": {"list": [{"name": "a"}, {"name": "c"}]}}} } run_graphql_test(query, expected_output, graph) @@ -535,8 +577,8 @@ def test_node_property_filter_is_not_in_empty_list(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { isNotIn: { list: [] } } @@ -552,7 +594,7 @@ def test_node_property_filter_is_not_in_empty_list(graph): expected_output = { "graph": { "nodes": { - "nodeFilter": { + "select": { "list": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}] } } @@ -567,8 +609,8 @@ def test_node_property_filter_is_not_in_type_error(graph): query { graph(path: "g") { nodes { - nodeFilter( - filter: { + select( + expr: { property: { name: "prop1" where: { isNotIn: { str: "shivam" } } @@ -592,7 +634,7 @@ def test_node_property_filter_contains_wrong_value_type_error(graph): query = """ query { graph(path: "g") { - nodeFilter(filter: { + filterNodes(expr: { property: { name: "p10" where: { contains: { u64: 2 } } @@ -615,7 +657,7 @@ def test_nodes_property_filter_starts_with(graph): query { graph(path: "g") { nodes { - nodeFilter(filter: { + select(expr: { property: { name: "prop3" where: { startsWith: { str: "abc" } } @@ -630,7 +672,7 @@ def test_nodes_property_filter_starts_with(graph): expected_output = { "graph": { "nodes": { - "nodeFilter": { + "select": { "list": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}] } } @@ -645,7 +687,7 @@ def test_nodes_property_filter_ends_with(graph): query { graph(path: "g") { nodes { - nodeFilter(filter: { + select(expr: { property: { name: "prop3" where: { endsWith: { str: "333" } } @@ -657,7 +699,7 @@ def test_nodes_property_filter_ends_with(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "c"}]}}}} + expected_output = {"graph": {"nodes": {"select": {"list": [{"name": "c"}]}}}} run_graphql_test(query, expected_output, graph) @@ -667,7 +709,7 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): query { graph(path: "g") { nodes { - nodeFilter(filter: { + select(expr: { temporalProperty: { name: "prop3" where: { first: { startsWith: { str: "abc" } } } @@ -682,7 +724,7 @@ def test_nodes_property_filter_temporal_first_starts_with(graph): expected_output = { "graph": { "nodes": { - "nodeFilter": { + "select": { "list": [{"name": "a"}, {"name": "b"}, {"name": "c"}, {"name": "d"}] } } @@ -697,7 +739,7 @@ def test_nodes_property_filter_temporal_all_starts_with(graph): query { graph(path: "g") { nodes { - nodeFilter(filter: { + select(expr: { temporalProperty: { name: "prop3" where: { any: { startsWith: { str: "abc1" } } } @@ -709,7 +751,7 @@ def test_nodes_property_filter_temporal_all_starts_with(graph): } } """ - expected_output = {"graph": {"nodes": {"nodeFilter": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"nodes": {"select": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -719,7 +761,7 @@ def test_nodes_property_filter_list_agg(graph): query = """ query { graph(path: "g") { - nodeFilter(filter: { + filterNodes(expr: { property: { name: "prop5" where: { sum: { eq: { i64: 6 } } } @@ -730,7 +772,7 @@ def test_nodes_property_filter_list_agg(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}]}}}} run_graphql_test(query, expected_output, graph) @@ -739,7 +781,7 @@ def test_nodes_property_filter_list_qualifier(graph): query = """ query { graph(path: "g") { - nodeFilter(filter: { + filterNodes(expr: { property: { name: "prop5" where: { any: { eq: { i64: 6 } } } @@ -750,7 +792,7 @@ def test_nodes_property_filter_list_qualifier(graph): } } """ - expected_output = {"graph": {"nodeFilter": {"nodes": {"list": [{"name": "c"}]}}}} + expected_output = {"graph": {"filterNodes": {"nodes": {"list": [{"name": "c"}]}}}} run_graphql_test(query, expected_output, graph) @@ -763,7 +805,7 @@ def test_nodes_temporal_property_filter_agg(graph): query = """ query { graph(path: "g") { - nodeFilter(filter: { + filterNodes(expr: { temporalProperty: { name: "p2" where: { avg: { lt: { f64: 10.0 } } } @@ -775,7 +817,7 @@ def test_nodes_temporal_property_filter_agg(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "2"}, {"name": "3"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "2"}, {"name": "3"}]}}} } run_graphql_test(query, expected_output, graph) @@ -790,7 +832,7 @@ def test_nodes_temporal_property_filter_any_avg(graph): query = """ query { graph(path: "g") { - nodeFilter(filter: { + filterNodes(expr: { temporalProperty: { name: "prop5" where: { any: { avg: { lt: { f64: 10.0 } } } } @@ -802,6 +844,210 @@ def test_nodes_temporal_property_filter_any_avg(graph): } """ expected_output = { - "graph": {"nodeFilter": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} + "graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} + } + run_graphql_test(query, expected_output, graph) + + +EVENT_GRAPH = init_graph2(Graph()) +PERSISTENT_GRAPH = init_graph2(PersistentGraph()) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_neighbours_selection_with_prop_filter(graph): + query = """ + query { + graph(path: "g") { + nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + list { + neighbours { + list { + name + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "list": [ + {"neighbours": {"list": [{"name": "2"}, {"name": "3"}]}}, + { + "neighbours": { + "list": [{"name": "1"}, {"name": "2"}, {"name": "4"}] + } + }, + ] + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_selection(graph): + query = """ + query { + graph(path: "g") { + nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + list { + name + } + } + } + } + """ + expected_output = {"graph": {"nodes": {"list": [{"name": "1"}, {"name": "3"}]}}} + run_graphql_test(query, expected_output, graph) + + +# The inner nodes filter has no effect on the list of nodes returned from selection filter +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_selection_nodes_filter_paired(graph): + query = """ + query { + graph(path: "g") { + nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + filter(expr:{ + property: { name: "p9", where: { eq:{ i64: 5 } } } + }) { + list { + name + } + } + } + } + } + """ + expected_output = { + "graph": {"nodes": {"filter": {"list": [{"name": "1"}, {"name": "3"}]}}} + } + run_graphql_test(query, expected_output, graph) + + +# The inner nodes filter has effect on the neighbours list +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_selection_nodes_filter_paired2(graph): + query = """ + query { + graph(path: "g") { + nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + filter(expr:{ + property: { name: "p9", where: { eq:{ i64: 5 } } } + }) { + list { + neighbours { + list { + name + } + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": { + "filter": { + "list": [ + {"neighbours": {"list": []}}, + {"neighbours": {"list": [{"name": "1"}]}}, + ] + } + } + } + } + run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_chained_selection_node_filter_paired(graph): + query = """ + query { + graph(path: "g") { + nodes(select: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + select(expr: { property: { name: "p9", where: { eq:{ i64: 5 } } } }) { + filter(expr:{ + node: { + field: NODE_TYPE + where: { eq: { str: "fire_nation" } } + } + }) { + list { + name + } + } + } + } + } + } + """ + expected_output = { + "graph": {"nodes": {"select": {"filter": {"list": [{"name": "1"}]}}}} } run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_chained_selection_node_filter_paired_ver2(graph): + query = """ + query { + graph(path: "g") { + nodes { + select(expr: { property: { name: "p100", where: { gt: { i64: 30 } } } }) { + select(expr: { property: { name: "p9", where: { eq:{ i64: 5 } } } }) { + filter(expr:{ + node: { + field: NODE_TYPE + where: { eq: { str: "fire_nation" } } + } + }) { + list { + name + } + } + } + } + } + } + } + """ + expected_output = { + "graph": { + "nodes": {"select": {"select": {"filter": {"list": [{"name": "1"}]}}}} + } + } + run_graphql_test(query, expected_output, graph) + + +EVENT_GRAPH = create_test_graph(Graph()) +PERSISTENT_GRAPH = create_test_graph(PersistentGraph()) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_nodes_temporal_property_filter_any_avg_with_window(graph): + query = """ + query { + graph(path: "g") { + filterNodes(expr: { + temporalProperty: { + name: "prop5" + window: { start: 1, end: 3 } + where: { any: { avg: { lt: { f64: 10.0 } } } } + } + }) { + nodes { list { name } } + } + } + } + """ + + expected = { + "graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} + } + run_graphql_test(query, expected, graph) diff --git a/python/tests/test_base_install/test_graphql/test_nodes.py b/python/tests/test_base_install/test_graphql/test_nodes.py index a4baf69ea5..058554c4d2 100644 --- a/python/tests/test_base_install/test_graphql/test_nodes.py +++ b/python/tests/test_base_install/test_graphql/test_nodes.py @@ -2,26 +2,28 @@ from raphtory import Graph, PersistentGraph from utils import run_graphql_test +from filters_setup import init_graph, init_graph2 -graph = Graph() -graph.add_edge(0, 0, 1) +EVENT_GRAPH = init_graph2(Graph()) +PERSISTENT_GRAPH = init_graph2(PersistentGraph()) -persistent_graph = graph.persistent_graph() - -@pytest.mark.parametrize("graph", [graph, persistent_graph]) +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) def test_graph_node_sort_by_nothing(graph): - query = """ - { - graph(path: "g") { - nodes(ids: ["0"]) { - list { - id - degree - } - } + query = """{ + graph(path: "g") { + nodes(select: { + node: { + field: NODE_ID + where: { eq: { u64: 1 } } + } + }) { + list { + name + degree } } - """ - expected_output = {"graph": {"nodes": {"list": [{"id": "0", "degree": 1}]}}} + } + }""" + expected_output = {"graph": {"nodes": {"list": [{"degree": 2, "name": "1"}]}}} run_graphql_test(query, expected_output, graph) diff --git a/raphtory-benchmark/benches/search_bench.rs b/raphtory-benchmark/benches/search_bench.rs index 8d685ebe85..b9e6efea3d 100644 --- a/raphtory-benchmark/benches/search_bench.rs +++ b/raphtory-benchmark/benches/search_bench.rs @@ -199,7 +199,7 @@ fn convert_to_property_filter( sampled_values: Option>, ) -> Option> where - M: PropertyFilterFactory, + M: PropertyFilterFactory + Default, PropertyFilterBuilder: PropertyFilterOps + InternalPropertyFilterOps, { let mut rng = thread_rng(); @@ -216,22 +216,24 @@ where let sub_str = tokens[start..=end].join(" "); match filter_op { - Eq => Some(M::property(prop_name).eq(sub_str)), - Ne => Some(M::property(prop_name).ne(sub_str)), - IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - IsNotIn => { - sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)) + Eq => Some(M::default().property(prop_name).eq(sub_str)), + Ne => Some(M::default().property(prop_name).ne(sub_str)), + IsIn => { + sampled_values.map(|vals| M::default().property(prop_name).is_in(vals)) } + IsNotIn => sampled_values + .map(|vals| M::default().property(prop_name).is_not_in(vals)), _ => None, // No numeric comparison for strings } } else { match filter_op { - Eq => Some(M::property(prop_name).eq(full_str)), - Ne => Some(M::property(prop_name).ne(full_str)), - IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - IsNotIn => { - sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)) + Eq => Some(M::default().property(prop_name).eq(full_str)), + Ne => Some(M::default().property(prop_name).ne(full_str)), + IsIn => { + sampled_values.map(|vals| M::default().property(prop_name).is_in(vals)) } + IsNotIn => sampled_values + .map(|vals| M::default().property(prop_name).is_not_in(vals)), _ => None, // No numeric comparison for strings } } @@ -242,43 +244,43 @@ where // Numeric properties support all comparison operators PropType::U64 => prop_value.into_u64().and_then(|v| match filter_op { - Eq => Some(M::property(prop_name).eq(v)), - Ne => Some(M::property(prop_name).ne(v)), - Lt => Some(M::property(prop_name).lt(v)), - Le => Some(M::property(prop_name).le(v)), - Gt => Some(M::property(prop_name).gt(v)), - Ge => Some(M::property(prop_name).ge(v)), - IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + Eq => Some(M::default().property(prop_name).eq(v)), + Ne => Some(M::default().property(prop_name).ne(v)), + Lt => Some(M::default().property(prop_name).lt(v)), + Le => Some(M::default().property(prop_name).le(v)), + Gt => Some(M::default().property(prop_name).gt(v)), + Ge => Some(M::default().property(prop_name).ge(v)), + IsIn => sampled_values.map(|vals| M::default().property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::default().property(prop_name).is_not_in(vals)), _ => return None, }), PropType::I64 => prop_value.into_i64().and_then(|v| match filter_op { - Eq => Some(M::property(prop_name).eq(v)), - Ne => Some(M::property(prop_name).ne(v)), - Lt => Some(M::property(prop_name).lt(v)), - Le => Some(M::property(prop_name).le(v)), - Gt => Some(M::property(prop_name).gt(v)), - Ge => Some(M::property(prop_name).ge(v)), - IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + Eq => Some(M::default().property(prop_name).eq(v)), + Ne => Some(M::default().property(prop_name).ne(v)), + Lt => Some(M::default().property(prop_name).lt(v)), + Le => Some(M::default().property(prop_name).le(v)), + Gt => Some(M::default().property(prop_name).gt(v)), + Ge => Some(M::default().property(prop_name).ge(v)), + IsIn => sampled_values.map(|vals| M::default().property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::default().property(prop_name).is_not_in(vals)), _ => return None, }), PropType::F64 => prop_value.into_f64().and_then(|v| match filter_op { - Eq => Some(M::property(prop_name).eq(v)), - Ne => Some(M::property(prop_name).ne(v)), - Lt => Some(M::property(prop_name).lt(v)), - Le => Some(M::property(prop_name).le(v)), - Gt => Some(M::property(prop_name).gt(v)), - Ge => Some(M::property(prop_name).ge(v)), - IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + Eq => Some(M::default().property(prop_name).eq(v)), + Ne => Some(M::default().property(prop_name).ne(v)), + Lt => Some(M::default().property(prop_name).lt(v)), + Le => Some(M::default().property(prop_name).le(v)), + Gt => Some(M::default().property(prop_name).gt(v)), + Ge => Some(M::default().property(prop_name).ge(v)), + IsIn => sampled_values.map(|vals| M::default().property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::default().property(prop_name).is_not_in(vals)), _ => return None, }), PropType::Bool => prop_value.into_bool().and_then(|v| match filter_op { - Eq => Some(M::property(prop_name).eq(v)), - Ne => Some(M::property(prop_name).ne(v)), - IsIn => sampled_values.map(|vals| M::property(prop_name).is_in(vals)), - IsNotIn => sampled_values.map(|vals| M::property(prop_name).is_not_in(vals)), + Eq => Some(M::default().property(prop_name).eq(v)), + Ne => Some(M::default().property(prop_name).ne(v)), + IsIn => sampled_values.map(|vals| M::default().property(prop_name).is_in(vals)), + IsNotIn => sampled_values.map(|vals| M::default().property(prop_name).is_not_in(vals)), _ => return None, }), diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 60d16aa028..383fcd2a8e 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -291,6 +291,7 @@ type Edge { Returns: boolean """ isSelfLoop: Boolean! + filter(expr: EdgeFilter!): Edge! } input EdgeAddition { @@ -451,6 +452,10 @@ input EdgeViewCollection @oneOf { Set the window end to a specified time. """ shrinkEnd: Int + """ + Edge filter + """ + edgeFilter: EdgeFilter } type EdgeWindowSet { @@ -578,6 +583,14 @@ type Edges { Returns a list of all objects in the current selection of the collection. You should filter filter the collection first then call list. """ list: [Edge!]! + """ + Returns a filtered view that applies to list down the chain + """ + filter(expr: EdgeFilter!): Edges! + """ + Returns filtered list of edges + """ + select(expr: EdgeFilter!): Edges! } input EdgesViewCollection @oneOf { @@ -641,6 +654,10 @@ input EdgesViewCollection @oneOf { Set the window end to a specified time. """ shrinkEnd: Int + """ + Edge filter + """ + edgeFilter: EdgeFilter } type EdgesWindowSet { @@ -811,7 +828,7 @@ type Graph { """ Gets (optionally a subset of) the nodes in the graph. """ - nodes(ids: [String!]): Nodes! + nodes(select: NodeFilter): Nodes! """ Gets the edge with the specified source and destination nodes. """ @@ -819,7 +836,7 @@ type Graph { """ Gets the edges in the graph. """ - edges: Edges! + edges(select: EdgeFilter): Edges! """ Returns the properties of the graph. """ @@ -850,8 +867,8 @@ type Graph { Export all nodes and edges from this graph view to another existing graph """ exportTo(path: String!): Boolean! - nodeFilter(filter: NodeFilter!): Graph! - edgeFilter(filter: EdgeFilter!): Graph! + filterNodes(expr: NodeFilter!): Graph! + filterEdges(expr: EdgeFilter!): Graph! """ (Experimental) Get index specification. """ @@ -876,8 +893,8 @@ type Graph { } type GraphAlgorithmPlugin { - pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! shortest_path(source: String!, targets: [String!]!, direction: String): [ShortestPathOutput!]! + pagerank(iterCount: Int!, threads: Int, tol: Float): [PagerankOutput!]! } type GraphSchema { @@ -1445,28 +1462,28 @@ type Node { """ Returns all connected edges. """ - edges: Edges! + edges(select: EdgeFilter): Edges! """ Returns outgoing edges. """ - outEdges: Edges! + outEdges(select: EdgeFilter): Edges! """ Returns incoming edges. """ - inEdges: Edges! + inEdges(select: EdgeFilter): Edges! """ Returns neighbouring nodes. """ - neighbours: PathFromNode! + neighbours(select: NodeFilter): PathFromNode! """ Returns the number of neighbours that have at least one in-going edge to this node. """ - inNeighbours: PathFromNode! + inNeighbours(select: NodeFilter): PathFromNode! """ Returns the number of neighbours that have at least one out-going edge from this node. """ - outNeighbours: PathFromNode! - nodeFilter(filter: NodeFilter!): Node! + outNeighbours(select: NodeFilter): PathFromNode! + filter(expr: NodeFilter!): Node! } input NodeAddition { @@ -1735,10 +1752,6 @@ type Nodes { Filter nodes by node type. """ typeFilter(nodeTypes: [String!]!): Nodes! - """ - Returns a view of the node types. - """ - nodeFilter(filter: NodeFilter!): Nodes! applyViews(views: [NodesViewCollection!]!): Nodes! sorted(sortBys: [NodeSortBy!]!): Nodes! """ @@ -1763,6 +1776,14 @@ type Nodes { Returns a view of the node ids. """ ids: [String!]! + """ + Returns a filtered view that applies to list down the chain + """ + filter(expr: NodeFilter!): Nodes! + """ + Returns filtered list of nodes + """ + select(expr: NodeFilter!): Nodes! } input NodesViewCollection @oneOf { @@ -1963,6 +1984,14 @@ type PathFromNode { Takes a specified selection of views and applies them in given order. """ applyViews(views: [PathFromNodeViewCollection!]!): PathFromNode! + """ + Returns a filtered view that applies to list down the chain + """ + filter(expr: NodeFilter!): PathFromNode! + """ + Returns filtered list of neighbour nodes + """ + select(expr: NodeFilter!): PathFromNode! } input PathFromNodeViewCollection @oneOf { @@ -2094,6 +2123,7 @@ type Property { input PropertyFilterNew { name: String! + window: Window where: PropCondition! } diff --git a/raphtory-graphql/src/model/graph/edge.rs b/raphtory-graphql/src/model/graph/edge.rs index 5e592b70f6..5ae11ea44d 100644 --- a/raphtory-graphql/src/model/graph/edge.rs +++ b/raphtory-graphql/src/model/graph/edge.rs @@ -1,7 +1,7 @@ use crate::{ model::graph::{ edges::GqlEdges, - filtering::EdgeViewCollection, + filtering::{EdgeViewCollection, GqlEdgeFilter}, node::GqlNode, property::{GqlMetadata, GqlProperties}, windowset::GqlEdgeWindowSet, @@ -13,8 +13,8 @@ use crate::{ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{ db::{ - api::view::{DynamicGraph, EdgeViewOps, IntoDynamic, StaticGraphViewOps}, - graph::edge::EdgeView, + api::view::{BaseFilterOps, DynamicGraph, EdgeViewOps, IntoDynamic, StaticGraphViewOps}, + graph::{edge::EdgeView, views::filter::model::edge_filter::CompositeEdgeFilter}, }, errors::GraphError, prelude::{LayerOps, TimeOps}, @@ -232,6 +232,7 @@ impl GqlEdge { } EdgeViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, EdgeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + EdgeViewCollection::EdgeFilter(filter) => return_view.filter(filter).await?, } } Ok(return_view) @@ -368,4 +369,20 @@ impl GqlEdge { async fn is_self_loop(&self) -> bool { self.ee.is_self_loop() } + + async fn filter(&self, expr: GqlEdgeFilter) -> Result { + let self_clone = self.clone(); + blocking_compute(move || { + let filter: CompositeEdgeFilter = expr.try_into()?; + let filtered = self_clone.ee.filter(filter)?; + Ok(self_clone.update(filtered.into_dynamic())) + }) + .await + } +} + +impl GqlEdge { + fn update>>(&self, edge: E) -> Self { + Self { ee: edge.into() } + } } diff --git a/raphtory-graphql/src/model/graph/edges.rs b/raphtory-graphql/src/model/graph/edges.rs index 41c19811e6..84a2278d50 100644 --- a/raphtory-graphql/src/model/graph/edges.rs +++ b/raphtory-graphql/src/model/graph/edges.rs @@ -15,8 +15,8 @@ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use itertools::Itertools; use raphtory::{ db::{ - api::view::{internal::BaseFilter, DynamicGraph}, - graph::edges::Edges, + api::view::{internal::BaseFilter, DynamicGraph, IterFilterOps}, + graph::{edges::Edges, views::filter::model::edge_filter::CompositeEdgeFilter}, }, errors::GraphError, prelude::*, @@ -24,6 +24,9 @@ use raphtory::{ use raphtory_api::iter::IntoDynBoxed; use std::{cmp::Ordering, sync::Arc}; +use crate::model::graph::filtering::GqlEdgeFilter; +use raphtory::db::api::view::BaseFilterOps; + #[derive(ResolvedObject, Clone)] #[graphql(name = "Edges")] pub(crate) struct GqlEdges { @@ -220,6 +223,7 @@ impl GqlEdges { } EdgesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, EdgesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, + EdgesViewCollection::EdgeFilter(filter) => return_view.filter(filter).await?, } } @@ -348,4 +352,26 @@ impl GqlEdges { let self_clone = self.clone(); blocking_compute(move || self_clone.iter().collect()).await } + + /// Returns a filtered view that applies to list down the chain + async fn filter(&self, expr: GqlEdgeFilter) -> Result { + let self_clone = self.clone(); + blocking_compute(move || { + let filter: CompositeEdgeFilter = expr.try_into()?; + let filtered = self_clone.ee.filter(filter)?; + Ok(self_clone.update(filtered.into_dyn())) + }) + .await + } + + /// Returns filtered list of edges + async fn select(&self, expr: GqlEdgeFilter) -> Result { + let self_clone = self.clone(); + blocking_compute(move || { + let filter: CompositeEdgeFilter = expr.try_into()?; + let filtered = self_clone.ee.select(filter)?; + Ok(self_clone.update(filtered)) + }) + .await + } } diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 5c37eb34a2..6a95fc7aee 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -16,7 +16,10 @@ use raphtory::{ }, errors::GraphError, }; -use raphtory_api::core::entities::{properties::prop::Prop, GID}; +use raphtory_api::core::{ + entities::{properties::prop::Prop, GID}, + storage::timeindex::TimeIndexEntry, +}; use std::{ borrow::Cow, collections::HashSet, @@ -187,6 +190,8 @@ pub enum EdgesViewCollection { ShrinkStart(i64), /// Set the window end to a specified time. ShrinkEnd(i64), + /// Edge filter + EdgeFilter(GqlEdgeFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -221,6 +226,8 @@ pub enum EdgeViewCollection { ShrinkStart(i64), /// Set the window end to a specified time. ShrinkEnd(i64), + /// Edge filter + EdgeFilter(GqlEdgeFilter), } #[derive(OneOfInput, Clone, Debug)] @@ -282,6 +289,7 @@ impl Display for NodeField { #[derive(InputObject, Clone, Debug)] pub struct PropertyFilterNew { pub name: String, + pub window: Option, #[graphql(name = "where")] pub where_: PropCondition, } @@ -844,9 +852,10 @@ fn translate_prop_leaf_to_filter( }) } -fn build_property_filter_from_condition( +fn build_property_filter_from_condition_with_entity( prop_ref: PropertyRef, cond: &PropCondition, + entity: M, ) -> Result, GraphError> { let mut ops: Vec = Vec::new(); let mut cursor = cond; @@ -859,10 +868,25 @@ fn build_property_filter_from_condition( prop_value, operator, ops, - _phantom: PhantomData, + entity, }) } +fn build_windowed_property_filter_from_condition( + prop_ref: PropertyRef, + cond: &PropCondition, + start: i64, + end: i64, +) -> Result>, GraphError> { + build_property_filter_from_condition_with_entity::< + raphtory::db::graph::views::filter::model::Windowed, + >( + prop_ref, + cond, + raphtory::db::graph::views::filter::model::Windowed::from_times(start, end), + ) +} + fn build_node_filter_from_prop_condition( prop_ref: PropertyRef, cond: &PropCondition, @@ -899,7 +923,9 @@ fn build_node_filter_from_prop_condition( Ok(CompositeNodeFilter::Not(Box::new(nf))) } _ => { - let pf = build_property_filter_from_condition::(prop_ref, cond)?; + let pf = build_property_filter_from_condition_with_entity::( + prop_ref, cond, NodeFilter, + )?; Ok(CompositeNodeFilter::Property(pf)) } } @@ -928,7 +954,22 @@ impl TryFrom for CompositeNodeFilter { } GqlNodeFilter::TemporalProperty(prop) => { let prop_ref = PropertyRef::TemporalProperty(prop.name); - build_node_filter_from_prop_condition(prop_ref, &prop.where_) + if let Some(w) = prop.window { + let pf = build_windowed_property_filter_from_condition::( + prop_ref, + &prop.where_, + w.start, + w.end, + )?; + return Ok(CompositeNodeFilter::PropertyWindowed(pf)); + } + + let pf = build_property_filter_from_condition_with_entity::( + prop_ref, + &prop.where_, + NodeFilter, + )?; + Ok(CompositeNodeFilter::Property(pf)) } GqlNodeFilter::And(and_filters) => { let mut iter = and_filters.into_iter().map(TryInto::try_into); @@ -994,7 +1035,9 @@ fn build_edge_filter_from_prop_condition( Ok(CompositeEdgeFilter::Not(Box::new(ef))) } _ => { - let pf = build_property_filter_from_condition::(prop_ref, cond)?; + let pf = build_property_filter_from_condition_with_entity::( + prop_ref, cond, EdgeFilter, + )?; Ok(CompositeEdgeFilter::Property(pf)) } } @@ -1042,7 +1085,17 @@ impl TryFrom for CompositeEdgeFilter { } GqlEdgeFilter::TemporalProperty(p) => { let prop_ref = PropertyRef::TemporalProperty(p.name); - build_edge_filter_from_prop_condition(prop_ref, &p.where_) + if let Some(w) = p.window { + let pf = build_windowed_property_filter_from_condition::( + prop_ref, &p.where_, w.start, w.end, + )?; + return Ok(CompositeEdgeFilter::PropertyWindowed(pf)); + } + + let pf = build_property_filter_from_condition_with_entity::( + prop_ref, &p.where_, EdgeFilter, + )?; + Ok(CompositeEdgeFilter::Property(pf)) } GqlEdgeFilter::And(and_filters) => { let mut iter = and_filters.into_iter().map(TryInto::try_into); diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs index ed6293d256..4e49e1e309 100644 --- a/raphtory-graphql/src/model/graph/graph.rs +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -28,8 +28,8 @@ use raphtory::{ api::{ properties::dyn_props::DynProperties, view::{ - BaseFilterOps, DynamicGraph, IntoDynamic, NodeViewOps, SearchableGraphOps, - StaticGraphViewOps, TimeOps, + BaseFilterOps, DynamicGraph, IntoDynamic, IterFilterOps, NodeViewOps, + SearchableGraphOps, StaticGraphViewOps, TimeOps, }, }, graph::{ @@ -374,12 +374,20 @@ impl GqlGraph { } /// Gets (optionally a subset of) the nodes in the graph. - async fn nodes(&self, ids: Option>) -> GqlNodes { - let nodes = self.graph.nodes(); - match ids { - None => GqlNodes::new(nodes), - Some(ids) => GqlNodes::new(blocking_compute(move || nodes.id_filter(ids)).await), + async fn nodes(&self, select: Option) -> Result { + let nn = self.graph.nodes(); + + if let Some(sel) = select { + let nf: CompositeNodeFilter = sel.try_into()?; + let narrowed = blocking_compute({ + let nn_clone = nn.clone(); + move || nn_clone.select(nf) + }) + .await?; + return Ok(GqlNodes::new(narrowed.into_dyn())); } + + Ok(GqlNodes::new(nn)) } /// Gets the edge with the specified source and destination nodes. @@ -388,8 +396,16 @@ impl GqlGraph { } /// Gets the edges in the graph. - async fn edges<'a>(&self) -> GqlEdges { - GqlEdges::new(self.graph.edges()) + async fn edges<'a>(&self, select: Option) -> Result { + let base = self.graph.edges(); + + if let Some(sel) = select { + let ef: CompositeEdgeFilter = sel.try_into()?; + let narrowed = blocking_compute(move || base.select(ef)).await?; + return Ok(GqlEdges::new(narrowed)); + } + + Ok(GqlEdges::new(base)) } //////////////////////// @@ -502,10 +518,10 @@ impl GqlGraph { .await } - async fn node_filter(&self, filter: GqlNodeFilter) -> Result { + async fn filter_nodes(&self, expr: GqlNodeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = filter.try_into()?; + let filter: CompositeNodeFilter = expr.try_into()?; let filtered_graph = self_clone.graph.filter(filter)?; Ok(GqlGraph::new( self_clone.path.clone(), @@ -515,10 +531,10 @@ impl GqlGraph { .await } - async fn edge_filter(&self, filter: GqlEdgeFilter) -> Result { + async fn filter_edges(&self, expr: GqlEdgeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeEdgeFilter = filter.try_into()?; + let filter: CompositeEdgeFilter = expr.try_into()?; let filtered_graph = self_clone.graph.filter(filter)?; Ok(GqlGraph::new( self_clone.path.clone(), @@ -661,8 +677,8 @@ impl GqlGraph { } GraphViewCollection::ShrinkStart(start) => return_view.shrink_start(start).await, GraphViewCollection::ShrinkEnd(end) => return_view.shrink_end(end).await, - GraphViewCollection::NodeFilter(filter) => return_view.node_filter(filter).await?, - GraphViewCollection::EdgeFilter(filter) => return_view.edge_filter(filter).await?, + GraphViewCollection::NodeFilter(filter) => return_view.filter_nodes(filter).await?, + GraphViewCollection::EdgeFilter(filter) => return_view.filter_edges(filter).await?, }; } Ok(return_view) diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs index f470367a59..0203796cf8 100644 --- a/raphtory-graphql/src/model/graph/node.rs +++ b/raphtory-graphql/src/model/graph/node.rs @@ -1,7 +1,7 @@ use crate::{ model::graph::{ edges::GqlEdges, - filtering::{GqlNodeFilter, NodeViewCollection}, + filtering::{GqlEdgeFilter, GqlNodeFilter, NodeViewCollection}, nodes::GqlNodes, path_from_node::GqlPathFromNode, property::{GqlMetadata, GqlProperties}, @@ -15,8 +15,16 @@ use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{ algorithms::components::{in_component, out_component}, db::{ - api::{properties::dyn_props::DynProperties, view::*}, - graph::{node::NodeView, views::filter::model::node_filter::CompositeNodeFilter}, + api::{ + properties::dyn_props::DynProperties, + view::{BaseFilterOps, *}, + }, + graph::{ + node::NodeView, + views::filter::model::{ + edge_filter::CompositeEdgeFilter, node_filter::CompositeNodeFilter, + }, + }, }, errors::GraphError, prelude::NodeStateOps, @@ -220,7 +228,7 @@ impl GqlNode { } NodeViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, NodeViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, - NodeViewCollection::NodeFilter(filter) => return_view.node_filter(filter).await?, + NodeViewCollection::NodeFilter(filter) => return_view.filter(filter).await?, } } Ok(return_view) @@ -336,41 +344,86 @@ impl GqlNode { } /// Returns all connected edges. - async fn edges(&self) -> GqlEdges { - GqlEdges::new(self.vv.edges()) + async fn edges(&self, select: Option) -> Result { + let base = self.vv.edges(); + if let Some(sel) = select { + let ef: CompositeEdgeFilter = sel.try_into()?; + let narrowed = blocking_compute(move || base.select(ef)).await?; + return Ok(GqlEdges::new(narrowed)); + } + Ok(GqlEdges::new(base)) } /// Returns outgoing edges. - async fn out_edges(&self) -> GqlEdges { - GqlEdges::new(self.vv.out_edges()) + async fn out_edges(&self, select: Option) -> Result { + let base = self.vv.out_edges(); + if let Some(sel) = select { + let ef: CompositeEdgeFilter = sel.try_into()?; + let narrowed = blocking_compute(move || base.select(ef)).await?; + return Ok(GqlEdges::new(narrowed)); + } + Ok(GqlEdges::new(base)) } /// Returns incoming edges. - async fn in_edges(&self) -> GqlEdges { - GqlEdges::new(self.vv.in_edges()) + async fn in_edges(&self, select: Option) -> Result { + let base = self.vv.in_edges(); + if let Some(sel) = select { + let ef: CompositeEdgeFilter = sel.try_into()?; + let narrowed = blocking_compute(move || base.select(ef)).await?; + return Ok(GqlEdges::new(narrowed)); + } + Ok(GqlEdges::new(base)) } /// Returns neighbouring nodes. - async fn neighbours<'a>(&self) -> GqlPathFromNode { - GqlPathFromNode::new(self.vv.neighbours()) + async fn neighbours<'a>( + &self, + select: Option, + ) -> Result { + let base = self.vv.neighbours(); + if let Some(expr) = select { + let nf: CompositeNodeFilter = expr.try_into()?; + let narrowed = blocking_compute(move || base.select(nf)).await?; + return Ok(GqlPathFromNode::new(narrowed)); + } + Ok(GqlPathFromNode::new(base)) } /// Returns the number of neighbours that have at least one in-going edge to this node. - async fn in_neighbours<'a>(&self) -> GqlPathFromNode { - GqlPathFromNode::new(self.vv.in_neighbours()) + async fn in_neighbours<'a>( + &self, + select: Option, + ) -> Result { + let base = self.vv.in_neighbours(); + if let Some(expr) = select { + let nf: CompositeNodeFilter = expr.try_into()?; + let narrowed = blocking_compute(move || base.select(nf)).await?; + return Ok(GqlPathFromNode::new(narrowed)); + } + Ok(GqlPathFromNode::new(base)) } /// Returns the number of neighbours that have at least one out-going edge from this node. - async fn out_neighbours(&self) -> GqlPathFromNode { - GqlPathFromNode::new(self.vv.out_neighbours()) + async fn out_neighbours( + &self, + select: Option, + ) -> Result { + let base = self.vv.out_neighbours(); + if let Some(expr) = select { + let nf: CompositeNodeFilter = expr.try_into()?; + let narrowed = blocking_compute(move || base.select(nf)).await?; + return Ok(GqlPathFromNode::new(narrowed)); + } + Ok(GqlPathFromNode::new(base)) } - async fn node_filter(&self, filter: GqlNodeFilter) -> Result { + async fn filter(&self, expr: GqlNodeFilter) -> Result { let self_clone = self.clone(); blocking_compute(move || { - let filter: CompositeNodeFilter = filter.try_into()?; - let filtered_nodes_applied = self_clone.vv.filter(filter)?; - Ok(self_clone.update(filtered_nodes_applied.into_dynamic())) + let filter: CompositeNodeFilter = expr.try_into()?; + let filtered = self_clone.vv.filter(filter)?; + Ok(self_clone.update(filtered.into_dynamic())) }) .await } diff --git a/raphtory-graphql/src/model/graph/nodes.rs b/raphtory-graphql/src/model/graph/nodes.rs index 5a02b175a0..4efaf45bd3 100644 --- a/raphtory-graphql/src/model/graph/nodes.rs +++ b/raphtory-graphql/src/model/graph/nodes.rs @@ -17,7 +17,7 @@ use raphtory::{ db::{ api::{ state::Index, - view::{DynamicGraph, IterFilterOps}, + view::{BaseFilterOps, DynamicGraph, IterFilterOps}, }, graph::{nodes::Nodes, views::filter::model::node_filter::CompositeNodeFilter}, }, @@ -180,17 +180,6 @@ impl GqlNodes { blocking_compute(move || self_clone.update(self_clone.nn.type_filter(&node_types))).await } - /// Returns a view of the node types. - async fn node_filter(&self, filter: GqlNodeFilter) -> Result { - let self_clone = self.clone(); - blocking_compute(move || { - let filter: CompositeNodeFilter = filter.try_into()?; - let filtered_nodes = self_clone.nn.filter_iter(filter)?; - Ok(self_clone.update(filtered_nodes.into_dyn())) - }) - .await - } - async fn apply_views(&self, views: Vec) -> Result { let mut return_view: GqlNodes = GqlNodes::new(self.nn.clone()); for view in views { @@ -235,7 +224,7 @@ impl GqlNodes { NodesViewCollection::ShrinkStart(time) => return_view.shrink_start(time).await, NodesViewCollection::ShrinkEnd(time) => return_view.shrink_end(time).await, NodesViewCollection::NodeFilter(node_filter) => { - return_view.node_filter(node_filter).await? + return_view.filter(node_filter).await? } NodesViewCollection::TypeFilter(types) => return_view.type_filter(types).await, } @@ -352,4 +341,26 @@ impl GqlNodes { let self_clone = self.clone(); blocking_compute(move || self_clone.nn.name().collect()).await } + + /// Returns a filtered view that applies to list down the chain + async fn filter(&self, expr: GqlNodeFilter) -> Result { + let self_clone = self.clone(); + blocking_compute(move || { + let filter: CompositeNodeFilter = expr.try_into()?; + let filtered = self_clone.nn.filter(filter)?; + Ok(self_clone.update(filtered.into_dyn())) + }) + .await + } + + /// Returns filtered list of nodes + async fn select(&self, expr: GqlNodeFilter) -> Result { + let self_clone = self.clone(); + blocking_compute(move || { + let filter: CompositeNodeFilter = expr.try_into()?; + let filtered = self_clone.nn.select(filter)?; + Ok(self_clone.update(filtered.into_dyn())) + }) + .await + } } diff --git a/raphtory-graphql/src/model/graph/path_from_node.rs b/raphtory-graphql/src/model/graph/path_from_node.rs index d077a69c02..153ccf5484 100644 --- a/raphtory-graphql/src/model/graph/path_from_node.rs +++ b/raphtory-graphql/src/model/graph/path_from_node.rs @@ -1,6 +1,6 @@ use crate::{ model::graph::{ - filtering::PathFromNodeViewCollection, + filtering::{GqlNodeFilter, PathFromNodeViewCollection}, node::GqlNode, windowset::GqlPathFromNodeWindowSet, WindowDuration::{self, Duration, Epoch}, @@ -9,7 +9,10 @@ use crate::{ }; use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; use raphtory::{ - db::{api::view::DynamicGraph, graph::path::PathFromNode}, + db::{ + api::view::{BaseFilterOps, DynamicGraph, IterFilterOps}, + graph::{path::PathFromNode, views::filter::model::node_filter::CompositeNodeFilter}, + }, errors::GraphError, prelude::*, }; @@ -268,4 +271,26 @@ impl GqlPathFromNode { } Ok(return_view) } + + /// Returns a filtered view that applies to list down the chain + async fn filter(&self, expr: GqlNodeFilter) -> Result { + let self_clone = self.clone(); + blocking_compute(move || { + let filter: CompositeNodeFilter = expr.try_into()?; + let filtered = self_clone.nn.filter(filter)?; + Ok(self_clone.update(filtered.into_dyn())) + }) + .await + } + + /// Returns filtered list of neighbour nodes + async fn select(&self, expr: GqlNodeFilter) -> Result { + let self_clone = self.clone(); + blocking_compute(move || { + let filter: CompositeNodeFilter = expr.try_into()?; + let filtered = self_clone.nn.select(filter)?; + Ok(self_clone.update(filtered.into_dyn())) + }) + .await + } } diff --git a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs index d3be6c026d..6942454bc6 100644 --- a/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs +++ b/raphtory/src/db/api/storage/graph/storage_ops/time_semantics.rs @@ -212,7 +212,7 @@ mod test_graph_storage { let g = Graph::new(); let g = init_graph_for_nodes_tests(g); g.create_index().unwrap(); - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let mut results = g .search_nodes(filter, 10, 0) .expect("Failed to search for nodes") @@ -244,7 +244,7 @@ mod test_graph_storage { let g = Graph::new(); let g = init_graph_for_edges_tests(g); g.create_index().unwrap(); - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let mut results = g .search_edges(filter, 10, 0) .expect("Failed to search for nodes") diff --git a/raphtory/src/db/api/view/filter_ops.rs b/raphtory/src/db/api/view/filter_ops.rs index ac216e8c24..fa21ee35d9 100644 --- a/raphtory/src/db/api/view/filter_ops.rs +++ b/raphtory/src/db/api/view/filter_ops.rs @@ -16,7 +16,7 @@ pub trait BaseFilterOps<'graph>: BaseFilter<'graph> { } pub trait IterFilterOps<'graph>: IterFilter<'graph> { - fn filter_iter( + fn select( &self, filter: F, ) -> Result>, GraphError> { diff --git a/raphtory/src/db/graph/edges.rs b/raphtory/src/db/graph/edges.rs index 7d0c7cea43..72ee168453 100644 --- a/raphtory/src/db/graph/edges.rs +++ b/raphtory/src/db/graph/edges.rs @@ -29,6 +29,15 @@ pub struct Edges<'graph, G> { pub(crate) edges: Arc BoxedLIter<'graph, EdgeRef> + Send + Sync + 'graph>, } +impl<'graph, G: IntoDynamic> Edges<'graph, G> { + pub fn into_dyn(self) -> Edges<'graph, DynamicGraph> { + Edges { + base_graph: self.base_graph.into_dynamic(), + edges: self.edges, + } + } +} + impl<'graph, G: GraphViewOps<'graph>> Debug for Edges<'graph, G> { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_list().entries(self.iter()).finish() diff --git a/raphtory/src/db/graph/path.rs b/raphtory/src/db/graph/path.rs index 37b626e664..c92ac38107 100644 --- a/raphtory/src/db/graph/path.rs +++ b/raphtory/src/db/graph/path.rs @@ -275,6 +275,15 @@ pub struct PathFromNode<'graph, G> { pub(crate) op: Arc BoxedLIter<'graph, VID> + Send + Sync + 'graph>, } +impl<'graph, G: IntoDynamic> PathFromNode<'graph, G> { + pub fn into_dyn(self) -> PathFromNode<'graph, DynamicGraph> { + PathFromNode { + base_graph: self.base_graph.into_dynamic(), + op: self.op, + } + } +} + impl<'graph, G: GraphViewOps<'graph>> PathFromNode<'graph, G> { pub(crate) fn new BoxedLIter<'graph, VID> + Send + Sync + 'graph>( graph: G, diff --git a/raphtory/src/db/graph/views/cached_view.rs b/raphtory/src/db/graph/views/cached_view.rs index 2bbff60615..5526b73f7a 100644 --- a/raphtory/src/db/graph/views/cached_view.rs +++ b/raphtory/src/db/graph/views/cached_view.rs @@ -484,7 +484,7 @@ mod test { #[test] fn test_nodes_filters() { - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -505,7 +505,7 @@ mod test { #[test] fn test_nodes_filters_w() { // TODO: Enable event_disk_graph for filter_nodes once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -525,7 +525,7 @@ mod test { #[test] fn test_nodes_filters_pg_w() { - let filter = NodeFilter::property("p1").ge(2u64); + let filter = NodeFilter.property("p1").ge(2u64); let expected_results = vec!["N2", "N5", "N8"]; assert_filter_nodes_results( init_graph, @@ -597,7 +597,7 @@ mod test { #[test] fn test_edges_filters() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -617,7 +617,7 @@ mod test { #[test] fn test_edges_filter_w() { - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -638,7 +638,7 @@ mod test { #[test] fn test_edges_filters_pg_w() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").ge(2u64); + let filter = EdgeFilter.property("p1").ge(2u64); let expected_results = vec!["N2->N3", "N5->N6", "N8->N1"]; assert_filter_edges_results( init_graph, diff --git a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs b/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs index 429ac56023..fdf01921cf 100644 --- a/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/edge_property_filtered_graph.rs @@ -10,14 +10,18 @@ use crate::{ InheritTimeSemantics, InternalEdgeFilterOps, Static, }, }, - graph::views::filter::{ - internal::CreateFilter, model::edge_filter::EdgeFilter, PropertyFilter, + graph::views::{ + filter::{ + internal::CreateFilter, + model::{edge_filter::EdgeFilter, property_filter::PropertyFilter, Windowed}, + }, + window_graph::WindowedGraph, }, }, errors::GraphError, - prelude::{GraphViewOps, LayerOps}, + prelude::{GraphViewOps, LayerOps, TimeOps}, }; -use raphtory_api::inherit::Base; +use raphtory_api::{core::storage::timeindex::AsTime, inherit::Base}; use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::edges::edge_ref::EdgeStorageRef}; #[derive(Debug, Clone)] @@ -37,6 +41,30 @@ impl EdgePropertyFilteredGraph { } } +impl CreateFilter for PropertyFilter> { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + EdgePropertyFilteredGraph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let prop_id = self.resolve_prop_id(graph.edge_meta(), graph.num_layers() > 1)?; + let filter = PropertyFilter { + prop_ref: self.prop_ref, + prop_value: self.prop_value, + operator: self.operator, + ops: self.ops, + entity: EdgeFilter, + }; + Ok(EdgePropertyFilteredGraph::new( + graph.window(self.entity.start.t(), self.entity.end.t()), + prop_id, + filter, + )) + } +} + impl CreateFilter for PropertyFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = EdgePropertyFilteredGraph; @@ -138,7 +166,7 @@ mod test_edge_property_filtered_graph { let filter_expr = EdgeFilter::dst() .name() .eq("David") - .and(EdgeFilter::property("band").eq("Dead & Company")); + .and(EdgeFilter.property("band").eq("Dead & Company")); let filtered_edges = g.filter(filter_expr).unwrap(); assert_eq!( @@ -179,7 +207,7 @@ mod test_edge_property_filtered_graph { let filter_expr = EdgeFilter::dst() .name() .eq("David") - .and(EdgeFilter::property("band").eq("Dead & Company")); + .and(EdgeFilter.property("band").eq("Dead & Company")); let filtered_edges = g.filter(filter_expr).unwrap(); let g_expected = PersistentGraph::new(); @@ -206,7 +234,7 @@ mod test_edge_property_filtered_graph { g.add_edge(1, 2, 3, [("test", 2i64)], None).unwrap(); g.add_edge(1, 2, 4, [("test", 0i64)], None).unwrap(); - let filter = EdgeFilter::property("test").eq(1i64); + let filter = EdgeFilter.property("test").eq(1i64); let n1 = g.node(1).unwrap().filter(filter).unwrap(); assert_eq!( n1.edges().id().collect_vec(), @@ -215,7 +243,7 @@ mod test_edge_property_filtered_graph { let n2 = g .node(2) .unwrap() - .filter(EdgeFilter::property("test").gt(1i64)) + .filter(EdgeFilter.property("test").gt(1i64)) .unwrap(); assert_eq!( n2.edges().id().collect_vec(), @@ -229,13 +257,13 @@ mod test_edge_property_filtered_graph { g.add_edge(0, 1, 2, [("test", 1i64)], None).unwrap(); g.add_edge(1, 2, 3, [("test", 2i64)], None).unwrap(); - let filter = EdgeFilter::property("test").eq(1i64); + let filter = EdgeFilter.property("test").eq(1i64); let gf = g.filter(filter).unwrap(); assert_eq!( gf.edges().id().collect_vec(), vec![(GID::U64(1), GID::U64(2))] ); - let gf = g.filter(EdgeFilter::property("test").gt(1i64)).unwrap(); + let gf = g.filter(EdgeFilter.property("test").gt(1i64)).unwrap(); assert_eq!( gf.edges().id().collect_vec(), vec![(GID::U64(2), GID::U64(3))] @@ -248,7 +276,7 @@ mod test_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = EdgeFilter::property("int_prop").gt(v); + let filter = EdgeFilter.property("int_prop").gt(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { for e in g.edges().iter() { if e.properties().get("int_prop").unwrap_i64() > v { @@ -267,7 +295,7 @@ mod test_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = EdgeFilter::property("int_prop").ge(v); + let filter = EdgeFilter.property("int_prop").ge(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { for e in g.edges().iter() { if e.properties().get("int_prop").unwrap_i64() >= v { @@ -286,7 +314,7 @@ mod test_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = EdgeFilter::property("int_prop").lt(v); + let filter = EdgeFilter.property("int_prop").lt(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { for e in g.edges().iter() { if e.properties().get("int_prop").unwrap_i64() < v { @@ -305,7 +333,7 @@ mod test_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = EdgeFilter::property("int_prop").le(v); + let filter = EdgeFilter.property("int_prop").le(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { for e in g.edges().iter() { if e.properties().get("int_prop").unwrap_i64() <= v { @@ -324,7 +352,7 @@ mod test_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = EdgeFilter::property("int_prop").eq(v); + let filter = EdgeFilter.property("int_prop").eq(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { for e in g.edges().iter() { if e.properties().get("int_prop").unwrap_i64() == v { @@ -343,7 +371,7 @@ mod test_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = EdgeFilter::property("int_prop").ne(v); + let filter = EdgeFilter.property("int_prop").ne(v); assert_ok_or_missing_edges(&edges, g.filter(filter), |filtered| { for e in g.edges().iter() { if e.properties().get("int_prop").unwrap_i64() != v { @@ -363,7 +391,7 @@ mod test_edge_property_filtered_graph { for (src, dst, t) in edge_deletions { g.delete_edge(t, src, dst, None).unwrap(); } - let filter = EdgeFilter::property("int_prop").gt(v); + let filter = EdgeFilter.property("int_prop").gt(v); assert_ok_or_missing_edges(&edges, g.window(start, end).filter(filter.clone()), |filtered| { let gwfm = filtered.materialize().unwrap(); assert_graph_equal(&filtered, &gwfm); @@ -384,7 +412,7 @@ mod test_edge_property_filtered_graph { for (src, dst, t) in edge_deletions { g.delete_edge(t, src, dst, None).unwrap(); } - let filter = EdgeFilter::property("int_prop").gt(v); + let filter = EdgeFilter.property("int_prop").gt(v); assert_ok_or_missing_edges(&edges, g.window(start, end).filter(filter.clone()), |filtered| { let gwfm = filtered.materialize().unwrap(); assert_persistent_materialize_graph_equal(&filtered, &gwfm); @@ -403,7 +431,7 @@ mod test_edge_property_filtered_graph { g.add_edge(0, 0, 1, [("test", 1i64)], None).unwrap(); g.delete_edge(10, 0, 1, None).unwrap(); let gw = g - .filter(EdgeFilter::property("test").gt(0i64)) + .filter(EdgeFilter.property("test").gt(0i64)) .unwrap() .window(0, 0); @@ -422,7 +450,7 @@ mod test_edge_property_filtered_graph { g.add_edge(0, 0, 1, [("test", 1i64)], None).unwrap(); g.delete_edge(1, 0, 1, None).unwrap(); let gw = g - .filter(EdgeFilter::property("test").gt(0i64)) + .filter(EdgeFilter.property("test").gt(0i64)) .unwrap() .window(0, 2); let gm = gw.materialize().unwrap(); @@ -444,7 +472,7 @@ mod test_edge_property_filtered_graph { let gwf = g .window(-1, 2) - .filter(EdgeFilter::property("test").gt(0i64)) + .filter(EdgeFilter.property("test").gt(0i64)) .unwrap(); assert!(gwf.has_edge(0, 1)); assert!(!gwf.has_edge(0, 0)); @@ -452,7 +480,7 @@ mod test_edge_property_filtered_graph { assert_persistent_materialize_graph_equal(&gwf, &gwf.materialize().unwrap()); let gfw = g - .filter(EdgeFilter::property("test").gt(0i64)) + .filter(EdgeFilter.property("test").gt(0i64)) .unwrap() .window(-1, 2); let gm = gfw.materialize().unwrap(); diff --git a/raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs b/raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs index 3bcd1b2f51..6c7889fdcb 100644 --- a/raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs +++ b/raphtory/src/db/graph/views/filter/exploded_edge_property_filter.rs @@ -10,15 +10,23 @@ use crate::{ InheritTimeSemantics, InternalExplodedEdgeFilterOps, Static, }, }, - graph::views::filter::{internal::CreateFilter, model::edge_filter::ExplodedEdgeFilter}, + graph::views::{ + filter::{ + internal::CreateFilter, + model::{ + edge_filter::ExplodedEdgeFilter, property_filter::PropertyFilter, Windowed, + }, + }, + window_graph::WindowedGraph, + }, }, errors::GraphError, - prelude::{GraphViewOps, LayerOps, PropertyFilter}, + prelude::{GraphViewOps, LayerOps, TimeOps}, }; use raphtory_api::{ core::{ entities::{EID, ELID}, - storage::timeindex::TimeIndexEntry, + storage::timeindex::{AsTime, TimeIndexEntry}, }, inherit::Base, }; @@ -53,22 +61,42 @@ impl<'graph, G: GraphViewOps<'graph>> ExplodedEdgePropertyFilteredGraph { } } -impl CreateFilter for PropertyFilter { - type EntityFiltered<'graph, G: GraphViewOps<'graph>> = ExplodedEdgePropertyFilteredGraph; +impl CreateFilter for PropertyFilter> { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + ExplodedEdgePropertyFilteredGraph>; fn create_filter<'graph, G: GraphViewOps<'graph>>( self, graph: G, ) -> Result, GraphError> { let prop_id = self.resolve_prop_id(graph.edge_meta(), graph.num_layers() > 1)?; + let filter = PropertyFilter { + prop_ref: self.prop_ref, + prop_value: self.prop_value, + operator: self.operator, + ops: self.ops, + entity: ExplodedEdgeFilter, + }; Ok(ExplodedEdgePropertyFilteredGraph::new( - graph.clone(), + graph.window(self.entity.start.t(), self.entity.end.t()), prop_id, - self, + filter, )) } } +impl CreateFilter for PropertyFilter { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = ExplodedEdgePropertyFilteredGraph; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let prop_id = self.resolve_prop_id(graph.edge_meta(), graph.num_layers() > 1)?; + Ok(ExplodedEdgePropertyFilteredGraph::new(graph, prop_id, self)) + } +} + impl Base for ExplodedEdgePropertyFilteredGraph { type Base = G; @@ -154,13 +182,9 @@ mod test_exploded_edge_property_filtered_graph { }, views::{ deletion_graph::PersistentGraph, - filter::{ - exploded_edge_property_filter::ExplodedEdgePropertyFilteredGraph, - internal::CreateFilter, - model::{ - edge_filter::ExplodedEdgeFilter, property_filter::PropertyFilterOps, - PropertyFilterFactory, TryAsCompositeFilter, - }, + filter::model::{ + edge_filter::ExplodedEdgeFilter, property_filter::PropertyFilterOps, + PropertyFilterFactory, TryAsCompositeFilter, }, }, }, @@ -177,7 +201,7 @@ mod test_exploded_edge_property_filtered_graph { use raphtory_api::core::{entities::properties::prop::PropType, storage::arc_str::ArcStr}; use raphtory_core::entities::nodes::node_ref::AsNodeRef; use raphtory_storage::mutation::addition_ops::InternalAdditionOps; - use std::collections::{HashMap, HashSet}; + use std::collections::HashMap; fn build_filtered_graph( edges: &[(u64, u64, i64, String, i64)], @@ -313,7 +337,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").gt(v); + let filter = ExplodedEdgeFilter.property("int_prop").gt(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let expected_filtered_g = build_filtered_graph(&edges, |vv| vv > v); assert_graph_equal(&filtered, &expected_filtered_g); @@ -336,7 +360,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected_filtered_g) = build_filtered_persistent_graph(edges, |vv| vv > v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").gt(v) + ExplodedEdgeFilter.property("int_prop").gt(v) ).unwrap(); assert_graph_equal(&filtered, &expected_filtered_g); }) @@ -347,7 +371,7 @@ mod test_exploded_edge_property_filtered_graph { let g = Graph::new(); g.add_edge(0, 1, 2, [("int_prop", 0i64)], None).unwrap(); let filtered = g - .filter(ExplodedEdgeFilter::property("int_prop").gt(1i64)) + .filter(ExplodedEdgeFilter.property("int_prop").gt(1i64)) .unwrap(); let gf = Graph::new(); gf.resolve_layer(None).unwrap(); @@ -362,7 +386,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").ge(v); + let filter = ExplodedEdgeFilter.property("int_prop").ge(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let expected_filtered_g = build_filtered_graph(&edges, |vv| vv >= v); assert_graph_equal(&filtered, &expected_filtered_g); @@ -377,7 +401,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected_filtered_g) = build_filtered_persistent_graph(edges, |vv| vv >= v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").ge(v) + ExplodedEdgeFilter.property("int_prop").ge(v) ).unwrap(); assert_graph_equal(&filtered, &expected_filtered_g); }) @@ -388,7 +412,7 @@ mod test_exploded_edge_property_filtered_graph { let g = PersistentGraph::new(); g.add_edge(0, 0, 0, [("test", 1i64)], None).unwrap(); let gf = g - .filter(ExplodedEdgeFilter::property("test").gt(1i64)) + .filter(ExplodedEdgeFilter.property("test").gt(1i64)) .unwrap(); assert_eq!(gf.count_edges(), 1); @@ -410,7 +434,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").lt(v); + let filter = ExplodedEdgeFilter.property("int_prop").lt(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let expected_filtered_g = build_filtered_graph(&edges, |vv| vv < v); assert_graph_equal(&filtered, &expected_filtered_g); @@ -425,7 +449,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected_filtered_g) = build_filtered_persistent_graph(edges, |vv| vv < v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").lt(v) + ExplodedEdgeFilter.property("int_prop").lt(v) ).unwrap(); assert_graph_equal(&filtered, &expected_filtered_g); }) @@ -437,7 +461,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").le(v); + let filter = ExplodedEdgeFilter.property("int_prop").le(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let expected_filtered_g = build_filtered_graph(&edges, |vv| vv <= v); assert_graph_equal(&filtered, &expected_filtered_g); @@ -452,7 +476,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected_filtered_g) = build_filtered_persistent_graph(edges, |vv| vv <= v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").le(v) + ExplodedEdgeFilter.property("int_prop").le(v) ).unwrap(); assert_graph_equal(&filtered, &expected_filtered_g); }) @@ -464,7 +488,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").eq(v); + let filter = ExplodedEdgeFilter.property("int_prop").eq(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let expected_filtered_g = build_filtered_graph(&edges, |vv| vv == v); assert_graph_equal(&filtered, &expected_filtered_g); @@ -479,7 +503,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected_filtered_g) = build_filtered_persistent_graph(edges, |vv| vv == v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").eq(v) + ExplodedEdgeFilter.property("int_prop").eq(v) ).unwrap(); assert_graph_equal(&filtered, &expected_filtered_g); }) @@ -491,7 +515,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").ne(v); + let filter = ExplodedEdgeFilter.property("int_prop").ne(v); assert_ok_or_missing_edges(&edges, g.filter(filter), |filtered| { let expected_filtered_g = build_filtered_graph(&edges, |vv| vv != v); assert_graph_equal(&filtered, &expected_filtered_g); @@ -506,7 +530,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected_filtered_g) = build_filtered_persistent_graph(edges, |vv| vv != v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").ne(v) + ExplodedEdgeFilter.property("int_prop").ne(v) ).unwrap(); assert_graph_equal(&filtered, &expected_filtered_g); }) @@ -518,7 +542,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::(), (start, end) in build_window() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").eq(v); + let filter = ExplodedEdgeFilter.property("int_prop").eq(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let expected_filtered_g = build_filtered_graph(&edges, |vv| vv == v); assert_graph_equal(&filtered.window(start, end), &expected_filtered_g.window(start, end)); @@ -533,7 +557,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected_filtered_g) = build_filtered_persistent_graph(edges, |vv| vv >= v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").ge(v) + ExplodedEdgeFilter.property("int_prop").ge(v) ).unwrap(); assert_graph_equal(&filtered.window(start, end), &expected_filtered_g.window(start, end)); }) @@ -545,7 +569,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").eq(v); + let filter = ExplodedEdgeFilter.property("int_prop").eq(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let mat = filtered.materialize().unwrap(); assert_graph_equal(&filtered, &mat); @@ -560,7 +584,7 @@ mod test_exploded_edge_property_filtered_graph { )| { let (g, expected) = build_filtered_persistent_graph(edges, |vv| vv >= v); let filtered = g.filter( - ExplodedEdgeFilter::property("int_prop").ge(v) + ExplodedEdgeFilter.property("int_prop").ge(v) ).unwrap(); let mat = filtered.materialize().unwrap(); assert_graph_equal(&filtered, &mat); @@ -574,7 +598,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").eq(v); + let filter = ExplodedEdgeFilter.property("int_prop").eq(v); assert_ok_or_missing_edges(&edges, g.nodes().filter(filter.clone()), |filtered| { let expected_filtered_g = build_filtered_nodes_graph(&edges, |vv| vv == v); assert_nodes_equal(&filtered, &expected_filtered_g.nodes()); @@ -590,7 +614,7 @@ mod test_exploded_edge_property_filtered_graph { let g = build_graph_from_edge_list(&edges); if let Some(node) = g.node(0) { let filtered_node = node.filter( - ExplodedEdgeFilter::property("int_prop").eq(v) + ExplodedEdgeFilter.property("int_prop").eq(v) ).unwrap(); let expected_filtered_g = build_filtered_graph(&edges, |vv| vv == v); if filtered_node.degree() == 0 { @@ -609,7 +633,7 @@ mod test_exploded_edge_property_filtered_graph { edges in build_edge_list(100, 100), v in any::(), (start, end) in build_window() )| { let g = build_graph_from_edge_list(&edges); - let filter = ExplodedEdgeFilter::property("int_prop").eq(v); + let filter = ExplodedEdgeFilter.property("int_prop").eq(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let left = filtered.window(start, end); let right = filtered.window(start, end).materialize().unwrap(); @@ -629,7 +653,7 @@ mod test_exploded_edge_property_filtered_graph { let edges = g .node(1) .unwrap() - .filter(ExplodedEdgeFilter::property("int_prop").gt(1i64)) + .filter(ExplodedEdgeFilter.property("int_prop").gt(1i64)) .unwrap() .edges() .explode() @@ -638,7 +662,7 @@ mod test_exploded_edge_property_filtered_graph { assert_eq!(edges.len(), 1); let gf = g - .filter(ExplodedEdgeFilter::property("int_prop").gt(1i64)) + .filter(ExplodedEdgeFilter.property("int_prop").gt(1i64)) .unwrap(); let gfm = gf.materialize().unwrap(); @@ -654,7 +678,7 @@ mod test_exploded_edge_property_filtered_graph { g.delete_edge(10, 0, 1, None).unwrap(); let gf = g - .filter(ExplodedEdgeFilter::property("test").ne(2i64)) + .filter(ExplodedEdgeFilter.property("test").ne(2i64)) .unwrap(); assert_eq!( gf.edges().explode().earliest_time().collect_vec(), @@ -674,7 +698,7 @@ mod test_exploded_edge_property_filtered_graph { for (src, dst, t) in edge_deletions { g.delete_edge(t, src, dst, None).unwrap(); } - let filter = ExplodedEdgeFilter::property("int_prop").gt(v); + let filter = ExplodedEdgeFilter.property("int_prop").gt(v); assert_ok_or_missing_edges(&edges, g.filter(filter.clone()), |filtered| { let gfm = filtered.materialize().unwrap(); assert_graph_equal(&filtered, &gfm) @@ -687,7 +711,7 @@ mod test_exploded_edge_property_filtered_graph { let g = PersistentGraph::new(); g.add_edge(0, 0, 0, [("test", 0i64)], None).unwrap(); let gf = g - .filter(ExplodedEdgeFilter::property("test").gt(0i64)) + .filter(ExplodedEdgeFilter.property("test").gt(0i64)) .unwrap(); let gfm = gf.materialize().unwrap(); dbg!(&gfm); @@ -704,7 +728,7 @@ mod test_exploded_edge_property_filtered_graph { for (src, dst, t) in edge_deletions { g.delete_edge(t, src, dst, None).unwrap(); } - let filter = ExplodedEdgeFilter::property("int_prop").gt(v); + let filter = ExplodedEdgeFilter.property("int_prop").gt(v); assert_ok_or_missing_edges(&edges, g.window(start, end).filter(filter.clone()), |filtered| { let gwfm = filtered.materialize().unwrap(); assert_persistent_materialize_graph_equal(&filtered, &gwfm); @@ -725,7 +749,7 @@ mod test_exploded_edge_property_filtered_graph { g.add_edge(0, 0, 1, [("test", Prop::I32(1))], None).unwrap(); let gwf = g - .filter(ExplodedEdgeFilter::property("test").gt(0)) + .filter(ExplodedEdgeFilter.property("test").gt(0)) .unwrap() .window(-1, 0); assert_eq!(gwf.count_nodes(), 0); @@ -736,7 +760,7 @@ mod test_exploded_edge_property_filtered_graph { let gfw = g .window(-1, 0) - .filter(ExplodedEdgeFilter::property("test").gt(0)) + .filter(ExplodedEdgeFilter.property("test").gt(0)) .unwrap(); assert_eq!(gfw.count_edges(), 0); let gm = gfw.materialize().unwrap(); diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index d0ee6d4f5c..c31de6adfe 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -1,7 +1,6 @@ use crate::db::graph::views::filter::model::{ edge_filter::EdgeFieldFilter, node_filter::{NodeNameFilter, NodeTypeFilter}, - property_filter::PropertyFilter, }; pub mod and_filtered_graph; @@ -17,368 +16,17 @@ pub mod node_type_filtered_graph; pub mod not_filtered_graph; pub mod or_filtered_graph; -#[cfg(test)] -mod test_fluent_builder_apis { - use crate::db::graph::views::filter::model::{ - edge_filter::{CompositeEdgeFilter, EdgeFilter, EdgeFilterOps}, - node_filter::{CompositeNodeFilter, NodeFilter, NodeFilterBuilderOps}, - property_filter::{ElemQualifierOps, Op, PropertyFilter, PropertyFilterOps, PropertyRef}, - ComposableFilter, Filter, PropertyFilterFactory, TryAsCompositeFilter, - }; - - #[test] - fn test_node_property_filter_build() { - let filter_expr = NodeFilter::property("p").eq("raphtory"); - let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p".to_string()), - "raphtory", - )); - assert_eq!(node_property_filter, node_property_filter2); - } - - #[test] - fn test_node_metadata_filter_build() { - let filter_expr = NodeFilter::metadata("p").eq("raphtory"); - let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Metadata("p".to_string()), - "raphtory", - )); - assert_eq!(node_property_filter, node_property_filter2); - } - - #[test] - fn test_node_any_temporal_property_filter_build() { - let filter_expr = NodeFilter::property("p").temporal().any().eq("raphtory"); - let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Property( - PropertyFilter::eq(PropertyRef::TemporalProperty("p".to_string()), "raphtory") - .with_op(Op::Any), - ); - assert_eq!(node_property_filter, node_property_filter2); - } - - #[test] - fn test_node_latest_temporal_property_filter_build() { - let filter_expr = NodeFilter::property("p").temporal().last().eq("raphtory"); - let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Property( - PropertyFilter::eq(PropertyRef::TemporalProperty("p".to_string()), "raphtory") - .with_op(Op::Last), - ); - assert_eq!(node_property_filter, node_property_filter2); - } - - #[test] - fn test_node_name_filter_build() { - let filter_expr = NodeFilter::name().eq("raphtory"); - let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Node(Filter::eq("node_name", "raphtory")); - assert_eq!(node_property_filter, node_property_filter2); - } - - #[test] - fn test_node_type_filter_build() { - let filter_expr = NodeFilter::node_type().eq("raphtory"); - let node_property_filter = filter_expr.try_as_composite_node_filter().unwrap(); - let node_property_filter2 = CompositeNodeFilter::Node(Filter::eq("node_type", "raphtory")); - assert_eq!(node_property_filter, node_property_filter2); - } - - #[test] - fn test_node_filter_composition() { - let node_composite_filter = NodeFilter::name() - .eq("fire_nation") - .and(NodeFilter::metadata("p2").eq(2u64)) - .and(NodeFilter::property("p1").eq(1u64)) - .and( - NodeFilter::property("p3") - .temporal() - .any() - .eq(5u64) - .or(NodeFilter::property("p4").temporal().last().eq(7u64)), - ) - .or(NodeFilter::node_type().eq("raphtory")) - .or(NodeFilter::property("p5").eq(9u64)) - .try_as_composite_node_filter() - .unwrap(); - - let node_composite_filter2 = CompositeNodeFilter::Or( - Box::new(CompositeNodeFilter::Or( - Box::new(CompositeNodeFilter::And( - Box::new(CompositeNodeFilter::And( - Box::new(CompositeNodeFilter::And( - Box::new(CompositeNodeFilter::Node(Filter::eq( - "node_name", - "fire_nation", - ))), - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Metadata("p2".to_string()), - 2u64, - ))), - )), - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p1".to_string()), - 1u64, - ))), - )), - Box::new(CompositeNodeFilter::Or( - Box::new(CompositeNodeFilter::Property( - PropertyFilter::eq( - PropertyRef::TemporalProperty("p3".to_string()), - 5u64, - ) - .with_op(Op::Any), - )), - Box::new(CompositeNodeFilter::Property( - PropertyFilter::eq( - PropertyRef::TemporalProperty("p4".to_string()), - 7u64, - ) - .with_op(Op::Last), - )), - )), - )), - Box::new(CompositeNodeFilter::Node(Filter::eq( - "node_type", - "raphtory", - ))), - )), - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p5".to_string()), - 9u64, - ))), - ); - - assert_eq!(node_composite_filter, node_composite_filter2); - } - - #[test] - fn test_edge_src_filter_build() { - let filter_expr = EdgeFilter::src().name().eq("raphtory"); - let edge_property_filter = filter_expr.try_as_composite_edge_filter().unwrap(); - let edge_property_filter2 = CompositeEdgeFilter::Edge(Filter::eq("src", "raphtory")); - assert_eq!(edge_property_filter, edge_property_filter2); - } - - #[test] - fn test_edge_dst_filter_build() { - let filter_expr = EdgeFilter::dst().name().eq("raphtory"); - let edge_property_filter = filter_expr.try_as_composite_edge_filter().unwrap(); - let edge_property_filter2 = CompositeEdgeFilter::Edge(Filter::eq("dst", "raphtory")); - assert_eq!(edge_property_filter, edge_property_filter2); - } - - #[test] - fn test_edge_filter_composition() { - let edge_composite_filter = EdgeFilter::src() - .name() - .eq("fire_nation") - .and(EdgeFilter::metadata("p2").eq(2u64)) - .and(EdgeFilter::property("p1").eq(1u64)) - .and( - EdgeFilter::property("p3") - .temporal() - .any() - .eq(5u64) - .or(EdgeFilter::property("p4").temporal().last().eq(7u64)), - ) - .or(EdgeFilter::src().name().eq("raphtory")) - .or(EdgeFilter::property("p5").eq(9u64)) - .try_as_composite_edge_filter() - .unwrap(); - - let edge_composite_filter2 = CompositeEdgeFilter::Or( - Box::new(CompositeEdgeFilter::Or( - Box::new(CompositeEdgeFilter::And( - Box::new(CompositeEdgeFilter::And( - Box::new(CompositeEdgeFilter::And( - Box::new(CompositeEdgeFilter::Edge(Filter::eq("src", "fire_nation"))), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::Metadata("p2".into()), - 2u64, - ))), - )), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p1".into()), - 1u64, - ))), - )), - Box::new(CompositeEdgeFilter::Or( - Box::new(CompositeEdgeFilter::Property( - PropertyFilter::eq(PropertyRef::TemporalProperty("p3".into()), 5u64) - .with_op(Op::Any), - )), - Box::new(CompositeEdgeFilter::Property( - PropertyFilter::eq(PropertyRef::TemporalProperty("p4".into()), 7u64) - .with_op(Op::Last), - )), - )), - )), - Box::new(CompositeEdgeFilter::Edge(Filter::eq("src", "raphtory"))), - )), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p5".into()), - 9u64, - ))), - ); - - assert_eq!(edge_composite_filter, edge_composite_filter2); - } -} - #[cfg(test)] mod test_composite_filters { use crate::{ db::graph::views::filter::model::{ - edge_filter::{CompositeEdgeFilter, EdgeFilter}, - node_filter::{CompositeNodeFilter, NodeFilter}, - property_filter::{PropertyFilter, PropertyFilterOps, PropertyRef}, + edge_filter::EdgeFilter, node_filter::NodeFilter, property_filter::PropertyFilterOps, Filter, PropertyFilterFactory, }, prelude::IntoProp, }; use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; - #[test] - fn test_composite_node_filter() { - assert_eq!( - "p2 == 2", - CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p2".to_string()), - 2u64 - )) - .to_string() - ); - - assert_eq!( - "((((node_type IS_NOT_IN [fire_nation, water_tribe] AND p2 == 2) AND p1 == 1) AND (p3 <= 5 OR p4 IS_IN [2, 10])) OR (node_name == pometry OR p5 == 9))", - CompositeNodeFilter::Or(Box::new(CompositeNodeFilter::And( - Box::new(CompositeNodeFilter::And( - Box::new(CompositeNodeFilter::And( - Box::new(CompositeNodeFilter::Node(Filter::is_not_in( - "node_type", - vec!["fire_nation".into(), "water_tribe".into()], - ))), - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p2".to_string()), - 2u64, - ))), - )), - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p1".to_string()), - 1u64, - ))), - )), - Box::new(CompositeNodeFilter::Or( - Box::new(CompositeNodeFilter::Property(PropertyFilter::le( - PropertyRef::Property("p3".to_string()), - 5u64, - ))), - Box::new(CompositeNodeFilter::Property(PropertyFilter::is_in( - PropertyRef::Property("p4".to_string()), - vec![Prop::U64(10), Prop::U64(2)], - ))), - )), - )), - Box::new(CompositeNodeFilter::Or( - Box::new(CompositeNodeFilter::Node(Filter::eq("node_name", "pometry"))), - Box::new(CompositeNodeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p5".to_string()), - 9u64, - ))), - )), - ).to_string() - ); - - assert_eq!( - "(name FUZZY_SEARCH(1,true) shivam AND nation FUZZY_SEARCH(1,false) air_nomad)", - CompositeNodeFilter::And( - Box::from(CompositeNodeFilter::Node(Filter::fuzzy_search( - "name", "shivam", 1, true - ))), - Box::from(CompositeNodeFilter::Property(PropertyFilter::fuzzy_search( - PropertyRef::Property("nation".to_string()), - "air_nomad", - 1, - false, - ))), - ) - .to_string() - ); - } - - #[test] - fn test_composite_edge_filter() { - assert_eq!( - "p2 == 2", - CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p2".to_string()), - 2u64 - )) - .to_string() - ); - - assert_eq!( - "((((edge_type IS_NOT_IN [fire_nation, water_tribe] AND p2 == 2) AND p1 == 1) AND (p3 <= 5 OR p4 IS_IN [2, 10])) OR (src == pometry OR p5 == 9))", - CompositeEdgeFilter::Or( - Box::new(CompositeEdgeFilter::And( - Box::new(CompositeEdgeFilter::And( - Box::new(CompositeEdgeFilter::And( - Box::new(CompositeEdgeFilter::Edge(Filter::is_not_in( - "edge_type", - vec!["fire_nation".into(), "water_tribe".into()], - ))), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p2".to_string()), - 2u64, - ))), - )), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p1".to_string()), - 1u64, - ))), - )), - Box::new(CompositeEdgeFilter::Or( - Box::new(CompositeEdgeFilter::Property(PropertyFilter::le( - PropertyRef::Property("p3".to_string()), - 5u64, - ))), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::is_in( - PropertyRef::Property("p4".to_string()), - vec![Prop::U64(10), Prop::U64(2)], - ))), - )), - )), - Box::new(CompositeEdgeFilter::Or( - Box::new(CompositeEdgeFilter::Edge(Filter::eq("src", "pometry"))), - Box::new(CompositeEdgeFilter::Property(PropertyFilter::eq( - PropertyRef::Property("p5".to_string()), - 9u64, - ))), - )), - ) - .to_string() - ); - - assert_eq!( - "(name FUZZY_SEARCH(1,true) shivam AND nation FUZZY_SEARCH(1,false) air_nomad)", - CompositeEdgeFilter::And( - Box::from(CompositeEdgeFilter::Edge(Filter::fuzzy_search( - "name", "shivam", 1, true - ))), - Box::from(CompositeEdgeFilter::Property(PropertyFilter::fuzzy_search( - PropertyRef::Property("nation".to_string()), - "air_nomad", - 1, - false, - ))), - ) - .to_string() - ); - } - #[test] fn test_fuzzy_search() { let filter = Filter::fuzzy_search("name", "pomet", 2, false); @@ -411,35 +59,35 @@ mod test_composite_filters { #[test] fn test_fuzzy_search_property() { - let filter = NodeFilter::property("prop").fuzzy_search("pomet", 2, false); + let filter = NodeFilter.property("prop").fuzzy_search("pomet", 2, false); assert!(filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); } #[test] fn test_fuzzy_search_property_prefix_match() { - let filter = EdgeFilter::property("prop").fuzzy_search("pome", 2, false); + let filter = EdgeFilter.property("prop").fuzzy_search("pome", 2, false); assert!(!filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); - let filter = EdgeFilter::property("prop").fuzzy_search("pome", 2, true); + let filter = EdgeFilter.property("prop").fuzzy_search("pome", 2, true); assert!(filter.matches(Some(&Prop::Str(ArcStr::from("pometry"))))); } #[test] fn test_contains_match() { - let filter = EdgeFilter::property("prop").contains("shivam"); + let filter = EdgeFilter.property("prop").contains("shivam"); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); assert!(res); let res = filter.matches(None); assert!(!res); - let filter = EdgeFilter::property("prop").contains("am_ka"); + let filter = EdgeFilter.property("prop").contains("am_ka"); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); assert!(res); } #[test] fn test_contains_not_match() { - let filter = NodeFilter::property("prop").not_contains("shivam"); + let filter = NodeFilter.property("prop").not_contains("shivam"); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam_kapoor")))); assert!(!res); let res = filter.matches(None); @@ -448,7 +96,9 @@ mod test_composite_filters { #[test] fn test_is_in_match() { - let filter = NodeFilter::property("prop").is_in(vec!["shivam".into_prop()]); + let filter = NodeFilter + .property("prop") + .is_in(vec!["shivam".into_prop()]); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam")))); assert!(res); let res = filter.matches(None); @@ -457,7 +107,9 @@ mod test_composite_filters { #[test] fn test_is_not_in_match() { - let filter = EdgeFilter::property("prop").is_not_in(vec!["shivam".into_prop()]); + let filter = EdgeFilter + .property("prop") + .is_not_in(vec!["shivam".into_prop()]); let res = filter.matches(Some(&Prop::Str(ArcStr::from("shivam")))); assert!(!res); let res = filter.matches(None); @@ -595,7 +247,7 @@ pub(crate) mod test_filters { #[test] fn test_metadata_semantics() { - let filter = NodeFilter::metadata("p1").eq(1u64); + let filter = NodeFilter.metadata("p1").eq(1u64); let expected_results = vec!["N1", "N10", "N11", "N12", "N13", "N14", "N15", "N9"]; assert_filter_nodes_results( init_graph, @@ -615,7 +267,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_any_semantics() { - let filter = NodeFilter::property("p1").temporal().any().eq(1u64); + let filter = NodeFilter.property("p1").temporal().any().eq(1u64); let expected_results = vec!["N1", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -635,7 +287,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_any_semantics_for_secondary_indexes() { - let filter = NodeFilter::property("p1").temporal().any().eq(1u64); + let filter = NodeFilter.property("p1").temporal().any().eq(1u64); let expected_results = vec!["N1", "N16", "N17", "N2", "N3", "N4", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( @@ -656,7 +308,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics() { - let filter = NodeFilter::property("p1").temporal().last().eq(1u64); + let filter = NodeFilter.property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -676,7 +328,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics_for_secondary_indexes() { - let filter = NodeFilter::property("p1").temporal().last().eq(1u64); + let filter = NodeFilter.property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1", "N16", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph_for_secondary_indexes, @@ -697,7 +349,7 @@ pub(crate) mod test_filters { #[test] fn test_property_semantics() { // TODO: Const properties not supported for disk_graph. - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -717,7 +369,7 @@ pub(crate) mod test_filters { #[test] fn test_property_semantics_for_secondary_indexes() { - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N16", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph_for_secondary_indexes, @@ -769,7 +421,7 @@ pub(crate) mod test_filters { graph } - let filter = NodeFilter::property("p1").ge(1u64); + let filter = NodeFilter.property("p1").ge(1u64); let graph = init_graph(Graph::new()); assert!(matches!( graph.filter(filter.clone()).unwrap_err(), @@ -809,7 +461,7 @@ pub(crate) mod test_filters { graph } - let filter = NodeFilter::property("p1").le(1u64); + let filter = NodeFilter.property("p1").le(1u64); let expected_results = vec!["N1", "N3"]; assert_filter_nodes_results( init_graph, @@ -971,7 +623,7 @@ pub(crate) mod test_filters { graph } - let filter = EdgeFilter::property("p1").temporal().first().eq(2u64); + let filter = EdgeFilter.property("p1").temporal().first().eq(2u64); // No window; means the first update is at time 0 and the value of p1 is expected to be 1u64. let expected_empty = []; @@ -1077,7 +729,7 @@ pub(crate) mod test_filters { fn test_metadata_semantics() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::metadata("p1").eq(1u64); + let filter = EdgeFilter.metadata("p1").eq(1u64); let expected_results = vec![ "N1->N2", "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N14->N15", "N15->N1", "N9->N10", @@ -1114,7 +766,7 @@ pub(crate) mod test_filters { let graph = init_graph(Graph::new()); - let filter = EdgeFilter::metadata("p1").eq(1u64); + let filter = EdgeFilter.metadata("p1").eq(1u64); assert_eq!( filter_edges(&graph, filter.clone()), vec![ @@ -1131,10 +783,14 @@ pub(crate) mod test_filters { let prop = graph.edge("shivam", "kapoor").unwrap().metadata().get("z"); assert_eq!(prop, Some(Prop::map([("fire_nation", true)]))); - let filter2 = EdgeFilter::metadata("z").eq(Prop::map([("fire_nation", true)])); + let filter2 = EdgeFilter + .metadata("z") + .eq(Prop::map([("fire_nation", true)])); assert_eq!(filter_edges(&graph, filter2), vec!["shivam->kapoor"]); - let filter = EdgeFilter::metadata("p1").eq(Prop::map([("_default", 1u64)])); + let filter = EdgeFilter + .metadata("p1") + .eq(Prop::map([("_default", 1u64)])); assert_eq!( filter_edges(&graph, filter), vec![ @@ -1147,7 +803,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_any_semantics() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").temporal().any().eq(1u64); + let filter = EdgeFilter.property("p1").temporal().any().eq(1u64); let expected_results = vec![ "N1->N2", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N9", ]; @@ -1170,7 +826,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_any_semantics_for_secondary_indexes() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").temporal().any().lt(2u64); + let filter = EdgeFilter.property("p1").temporal().any().lt(2u64); let expected_results = vec![ "N1->N2", "N16->N15", "N17->N16", "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N9", @@ -1194,7 +850,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").temporal().last().eq(1u64); + let filter = EdgeFilter.property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -1215,7 +871,7 @@ pub(crate) mod test_filters { #[test] fn test_temporal_latest_semantics_for_secondary_indexes() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").temporal().last().eq(1u64); + let filter = EdgeFilter.property("p1").temporal().last().eq(1u64); let expected_results = vec!["N1->N2", "N16->N15", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( @@ -1237,7 +893,7 @@ pub(crate) mod test_filters { #[test] fn test_property_semantics() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p1").ge(2u64); + let filter = EdgeFilter.property("p1").ge(2u64); let expected_results = vec![ "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N8->N9", "N9->N10", @@ -1262,7 +918,7 @@ pub(crate) mod test_filters { fn test_property_semantics_for_secondary_indexes() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N16->N15", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( @@ -1318,7 +974,7 @@ pub(crate) mod test_filters { graph } - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let graph = init_graph(Graph::new()); assert!(matches!( graph.filter(filter.clone()).unwrap_err(), @@ -1359,7 +1015,7 @@ pub(crate) mod test_filters { graph } - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4"]; assert_filter_edges_results( init_graph, @@ -2612,10 +2268,6 @@ pub(crate) mod test_filters { #[cfg(test)] mod test_node_property_filter { - use crate::db::graph::views::filter::test_filters::init_nodes_graph; - use raphtory_api::core::entities::properties::prop::Prop; - use std::vec; - use crate::db::graph::{ assertions::{assert_filter_nodes_results, assert_search_nodes_results, TestVariants}, views::filter::{ @@ -2625,13 +2277,15 @@ pub(crate) mod test_filters { property_filter::{ListAggOps, PropertyFilterOps}, ComposableFilter, PropertyFilterFactory, }, - test_filters::IdentityGraphTransformer, + test_filters::{init_nodes_graph, IdentityGraphTransformer}, }, }; + use raphtory_api::core::entities::properties::prop::Prop; + use std::vec; #[test] fn test_exact_match() { - let filter = NodeFilter::property("p10").eq("Paper_airplane"); + let filter = NodeFilter.property("p10").eq("Paper_airplane"); let expected_results = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2648,7 +2302,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10").eq(""); + let filter = NodeFilter.property("p10").eq(""); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( init_nodes_graph, @@ -2668,7 +2322,7 @@ pub(crate) mod test_filters { #[test] fn test_not_exact_match() { - let filter = NodeFilter::property("p10").eq("Paper"); + let filter = NodeFilter.property("p10").eq("Paper"); let expected_results: Vec<&str> = vec![]; assert_filter_nodes_results( init_nodes_graph, @@ -2688,7 +2342,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_eq() { - let filter = NodeFilter::property("p2").eq(2u64); + let filter = NodeFilter.property("p2").eq(2u64); let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -2705,10 +2359,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p30") - .temporal() - .first() - .eq("Old_boat"); + let filter = NodeFilter.property("p30").temporal().first().eq("Old_boat"); let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -2725,7 +2376,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p20").temporal().all().eq("Gold_ship"); + let filter = NodeFilter.property("p20").temporal().all().eq("Gold_ship"); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -2745,7 +2396,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_ne() { - let filter = NodeFilter::property("p2").ne(2u64); + let filter = NodeFilter.property("p2").ne(2u64); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2762,10 +2413,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p30") - .temporal() - .first() - .ne("Old_boat"); + let filter = NodeFilter.property("p30").temporal().first().ne("Old_boat"); let expected_results = vec!["1", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -2782,7 +2430,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p1").temporal().all().ne("Gold_ship"); + let filter = NodeFilter.property("p1").temporal().all().ne("Gold_ship"); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -2802,7 +2450,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_lt() { - let filter = NodeFilter::property("p2").lt(10u64); + let filter = NodeFilter.property("p2").lt(10u64); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2819,7 +2467,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40").temporal().first().lt(10u64); + let filter = NodeFilter.property("p40").temporal().first().lt(10u64); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -2836,7 +2484,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p9").temporal().all().lt(10u64); + let filter = NodeFilter.property("p9").temporal().all().lt(10u64); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -2856,7 +2504,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_le() { - let filter = NodeFilter::property("p2").le(6u64); + let filter = NodeFilter.property("p2").le(6u64); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2873,7 +2521,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p9").temporal().first().le(10u64); + let filter = NodeFilter.property("p9").temporal().first().le(10u64); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -2890,7 +2538,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p2").temporal().all().le(10u64); + let filter = NodeFilter.property("p2").temporal().all().le(10u64); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2910,7 +2558,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_gt() { - let filter = NodeFilter::property("p2").gt(2u64); + let filter = NodeFilter.property("p2").gt(2u64); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2927,7 +2575,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40").temporal().first().gt(5u64); + let filter = NodeFilter.property("p40").temporal().first().gt(5u64); let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -2944,7 +2592,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p9").temporal().all().gt(1u64); + let filter = NodeFilter.property("p9").temporal().all().gt(1u64); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -2964,7 +2612,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_ge() { - let filter = NodeFilter::property("p2").ge(2u64); + let filter = NodeFilter.property("p2").ge(2u64); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -2981,7 +2629,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40").temporal().first().ge(5u64); + let filter = NodeFilter.property("p40").temporal().first().ge(5u64); let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, @@ -2998,7 +2646,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40").temporal().all().ge(5u64); + let filter = NodeFilter.property("p40").temporal().all().ge(5u64); let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, @@ -3018,7 +2666,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_in() { - let filter = NodeFilter::property("p2").is_in(vec![Prop::U64(6)]); + let filter = NodeFilter.property("p2").is_in(vec![Prop::U64(6)]); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3035,7 +2683,9 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p2").is_in(vec![Prop::U64(2), Prop::U64(6)]); + let filter = NodeFilter + .property("p2") + .is_in(vec![Prop::U64(2), Prop::U64(6)]); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3052,7 +2702,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40") + let filter = NodeFilter + .property("p40") .temporal() .first() .is_in(vec![Prop::U64(5)]); @@ -3072,7 +2723,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p2") + let filter = NodeFilter + .property("p2") .temporal() .any() .is_in(vec![Prop::U64(2)]); @@ -3095,7 +2747,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_not_in() { - let filter = NodeFilter::property("p2").is_not_in(vec![Prop::U64(6)]); + let filter = NodeFilter.property("p2").is_not_in(vec![Prop::U64(6)]); let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -3112,7 +2764,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40").is_not_in(vec![Prop::U64(6)]); + let filter = NodeFilter.property("p40").is_not_in(vec![Prop::U64(6)]); let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, @@ -3129,7 +2781,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p2") + let filter = NodeFilter + .property("p2") .temporal() .all() .is_not_in(vec![Prop::U64(2)]); @@ -3152,7 +2805,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_is_some() { - let filter = NodeFilter::property("p2").is_some(); + let filter = NodeFilter.property("p2").is_some(); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3169,7 +2822,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40").is_some(); + let filter = NodeFilter.property("p40").is_some(); let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, @@ -3189,7 +2842,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_is_none() { - let filter = NodeFilter::property("p2").is_none(); + let filter = NodeFilter.property("p2").is_none(); let expected_results = vec!["1", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -3206,7 +2859,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p40").is_none(); + let filter = NodeFilter.property("p40").is_none(); let expected_results = vec!["3", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -3226,7 +2879,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_starts_with() { - let filter = NodeFilter::property("p10").starts_with("Pa"); + let filter = NodeFilter.property("p10").starts_with("Pa"); let expected_results: Vec<&str> = vec!["1", "2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3243,7 +2896,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .any() .starts_with("Pap"); @@ -3263,7 +2917,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .last() .starts_with("Pape"); @@ -3283,7 +2938,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .last() .starts_with("Yohan"); @@ -3303,7 +2959,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p30") + let filter = NodeFilter + .property("p30") .temporal() .first() .starts_with("Gold"); @@ -3323,7 +2980,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p20") + let filter = NodeFilter + .property("p20") .temporal() .all() .starts_with("Gold"); @@ -3346,7 +3004,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_ends_with() { - let filter = NodeFilter::property("p10").ends_with("lane"); + let filter = NodeFilter.property("p10").ends_with("lane"); let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3363,7 +3021,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .any() .ends_with("ship"); @@ -3383,7 +3042,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .last() .ends_with("ane"); @@ -3403,7 +3063,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .last() .ends_with("Jerry"); @@ -3423,7 +3084,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p20") + let filter = NodeFilter + .property("p20") .temporal() .first() .ends_with("boat"); @@ -3443,7 +3105,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p20") + let filter = NodeFilter + .property("p20") .temporal() .all() .ends_with("ship"); @@ -3466,7 +3129,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_contains() { - let filter = NodeFilter::property("p10").contains("Paper"); + let filter = NodeFilter.property("p10").contains("Paper"); let expected_results: Vec<&str> = vec!["1", "2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3483,7 +3146,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .any() .contains("Paper"); @@ -3503,7 +3167,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .last() .contains("Paper"); @@ -3523,7 +3188,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p30") + let filter = NodeFilter + .property("p30") .temporal() .first() .contains("Old"); @@ -3543,10 +3209,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p30") - .temporal() - .all() - .contains("Gold"); + let filter = NodeFilter.property("p30").temporal().all().contains("Gold"); let expected_results: Vec<&str> = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -3566,7 +3229,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_property_contains_not() { - let filter = NodeFilter::property("p10").not_contains("ship"); + let filter = NodeFilter.property("p10").not_contains("ship"); let expected_results: Vec<&str> = vec!["1", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3583,7 +3246,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .any() .not_contains("ship"); @@ -3603,7 +3267,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10") + let filter = NodeFilter + .property("p10") .temporal() .last() .not_contains("ship"); @@ -3623,7 +3288,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p30") + let filter = NodeFilter + .property("p30") .temporal() .first() .not_contains("Old"); @@ -3643,7 +3309,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p30") + let filter = NodeFilter + .property("p30") .temporal() .all() .not_contains("boat"); @@ -3666,7 +3333,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_not_property() { - let filter = NotFilter(NodeFilter::property("p10").contains("Paper")); + let filter = NotFilter(NodeFilter.property("p10").contains("Paper")); let expected_results: Vec<&str> = vec!["4"]; assert_filter_nodes_results( init_nodes_graph, @@ -3683,7 +3350,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p10").contains("Paper").not(); + let filter = NodeFilter.property("p10").contains("Paper").not(); assert_filter_nodes_results( init_nodes_graph, IdentityGraphTransformer, @@ -3702,7 +3369,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_temporal_property_sum() { - let filter = NodeFilter::property("p9").temporal().sum().eq(15u64); + let filter = NodeFilter.property("p9").temporal().sum().eq(15u64); let expected_results: Vec<&str> = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -3722,7 +3389,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_temporal_property_avg() { - let filter = NodeFilter::property("p2").temporal().avg().le(10f64); + let filter = NodeFilter.property("p2").temporal().avg().le(10f64); let expected_results: Vec<&str> = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3742,7 +3409,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_temporal_property_min() { - let filter = NodeFilter::property("p40").temporal().min().is_in(vec![ + let filter = NodeFilter.property("p40").temporal().min().is_in(vec![ Prop::U64(5), Prop::U64(10), Prop::U64(20), @@ -3766,7 +3433,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_temporal_property_max() { - let filter = NodeFilter::property("p3").temporal().max().is_not_in(vec![ + let filter = NodeFilter.property("p3").temporal().max().is_not_in(vec![ Prop::U64(5), Prop::U64(10), Prop::U64(20), @@ -3790,7 +3457,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_for_temporal_property_len() { - let filter = NodeFilter::property("p2").temporal().len().le(5u64); + let filter = NodeFilter.property("p2").temporal().len().le(5u64); let expected_results: Vec<&str> = vec!["1", "2", "3", "4"]; assert_filter_nodes_results( init_nodes_graph, @@ -3807,6 +3474,196 @@ pub(crate) mod test_filters { TestVariants::All, ); } + + #[test] + fn test_nodes_window_filter() { + let filter = NodeFilter::window(1, 3) + .property("p2") + .temporal() + .sum() + .ge(2u64); + + let expected_results = vec!["2"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter, + &expected_results, + TestVariants::All, + ); + + // Wider window includes node 3 + let filter = NodeFilter::window(1, 5) + .property("p2") + .temporal() + .sum() + .ge(2u64); + + let expected_results = vec!["2", "3"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter, + &expected_results, + TestVariants::All, + ); + } + + #[test] + fn test_nodes_window_filter_on_non_temporal_property() { + let filter1 = NodeFilter::window(1, 2).property("p1").eq("shivam_kapoor"); + let filter2 = NodeFilter::window(100, 200) + .property("p1") + .eq("shivam_kapoor"); + + let expected_results = vec!["1"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter1.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter1.clone(), + &expected_results, + TestVariants::All, + ); + + let expected_results = vec![]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter2.clone(), + &expected_results, + TestVariants::EventOnly, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter2.clone(), + &expected_results, + TestVariants::EventOnly, + ); + + let filter = NodeFilter::window(100, 200) + .property("p1") + .eq("shivam_kapoor"); + + let expected_results = vec!["1"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::PersistentOnly, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::PersistentOnly, + ); + } + + #[test] + fn test_nodes_window_filter_any_all_over_window() { + let filter = NodeFilter::window(3, 5) + .property("p20") + .temporal() + .any() + .eq("Gold_boat"); + + let expected_results = vec!["4"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = NodeFilter::window(3, 5) + .property("p20") + .temporal() + .all() + .eq("Gold_boat"); + + let expected_results = vec![]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + } + + #[test] + fn test_nodes_window_filter_and() { + // Filters both node 1 and 3 + let filter1 = NodeFilter::window(1, 4) + .property("p10") + .temporal() + .any() + .eq("Paper_airplane"); + + // Filters only node 3 + let filter2 = NodeFilter::window(3, 6) + .property("p2") + .temporal() + .sum() + .eq(6u64); + + let filter = filter1.and(filter2); + + let expected_results = vec!["3"]; + assert_filter_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_nodes_results( + init_nodes_graph, + IdentityGraphTransformer, + filter, + &expected_results, + TestVariants::All, + ); + } } #[cfg(test)] @@ -3831,9 +3688,10 @@ pub(crate) mod test_filters { #[test] fn test_filter_nodes_by_props_added_at_different_times() { - let filter = NodeFilter::property("p4") + let filter = NodeFilter + .property("p4") .eq("pometry") - .and(NodeFilter::property("p5").eq(12u64)); + .and(NodeFilter.property("p5").eq(12u64)); let expected_results = vec!["4"]; assert_filter_nodes_results( init_nodes_graph, @@ -3853,9 +3711,10 @@ pub(crate) mod test_filters { #[test] fn test_unique_results_from_composite_filters() { - let filter = NodeFilter::property("p2") + let filter = NodeFilter + .property("p2") .ge(2u64) - .and(NodeFilter::property("p2").ge(1u64)); + .and(NodeFilter.property("p2").ge(1u64)); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3865,9 +3724,10 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p2") + let filter = NodeFilter + .property("p2") .ge(2u64) - .or(NodeFilter::property("p2").ge(5u64)); + .or(NodeFilter.property("p2").ge(5u64)); let expected_results = vec!["2", "3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3880,9 +3740,10 @@ pub(crate) mod test_filters { #[test] fn test_composite_filter_nodes() { - let filter = NodeFilter::property("p2") + let filter = NodeFilter + .property("p2") .eq(2u64) - .and(NodeFilter::property("p1").eq("kapoor")); + .and(NodeFilter.property("p1").eq("kapoor")); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( init_nodes_graph, @@ -3914,9 +3775,10 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p2") + let filter = NodeFilter + .property("p2") .eq(2u64) - .or(NodeFilter::property("p1").eq("shivam_kapoor")); + .or(NodeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, @@ -3948,11 +3810,10 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p1") - .eq("pometry") - .or(NodeFilter::property("p2") - .eq(6u64) - .and(NodeFilter::property("p3").eq(1u64))); + let filter = NodeFilter.property("p1").eq("pometry").or(NodeFilter + .property("p2") + .eq(6u64) + .and(NodeFilter.property("p3").eq(1u64))); let expected_results = vec!["3"]; assert_filter_nodes_results( init_nodes_graph, @@ -3986,7 +3847,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::node_type() .eq("fire_nation") - .and(NodeFilter::property("p1").eq("prop1")); + .and(NodeFilter.property("p1").eq("prop1")); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( init_nodes_graph, @@ -4018,9 +3879,10 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = NodeFilter::property("p9") + let filter = NodeFilter + .property("p9") .eq(5u64) - .and(NodeFilter::property("p1").eq("shivam_kapoor")); + .and(NodeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -4054,7 +3916,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::node_type() .eq("fire_nation") - .and(NodeFilter::property("p1").eq("shivam_kapoor")); + .and(NodeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -4088,7 +3950,7 @@ pub(crate) mod test_filters { let filter = NodeFilter::name() .eq("2") - .and(NodeFilter::property("p2").eq(2u64)); + .and(NodeFilter.property("p2").eq(2u64)); let expected_results = vec!["2"]; assert_filter_nodes_results( init_nodes_graph, @@ -4122,8 +3984,8 @@ pub(crate) mod test_filters { let filter = NodeFilter::name() .eq("2") - .and(NodeFilter::property("p2").eq(2u64)) - .or(NodeFilter::property("p9").eq(5u64)); + .and(NodeFilter.property("p2").eq(2u64)) + .or(NodeFilter.property("p9").eq(5u64)); let expected_results = vec!["1", "2"]; assert_filter_nodes_results( init_nodes_graph, @@ -4160,8 +4022,8 @@ pub(crate) mod test_filters { fn test_not_composite_filter_nodes() { let filter = NodeFilter::name() .eq("2") - .and(NodeFilter::property("p2").eq(2u64)) - .or(NodeFilter::property("p9").eq(5u64)) + .and(NodeFilter.property("p2").eq(2u64)) + .or(NodeFilter.property("p9").eq(5u64)) .not(); let expected_results = vec!["3", "4"]; assert_filter_nodes_results( @@ -4182,8 +4044,8 @@ pub(crate) mod test_filters { let filter = NodeFilter::name() .eq("2") .not() - .and(NodeFilter::property("p2").eq(2u64)) - .or(NodeFilter::property("p9").eq(5u64)); + .and(NodeFilter.property("p2").eq(2u64)) + .or(NodeFilter.property("p9").eq(5u64)); let expected_results = vec!["1"]; assert_filter_nodes_results( init_nodes_graph, @@ -4205,7 +4067,7 @@ pub(crate) mod test_filters { fn test_out_neighbours_filter() { let filter = NodeFilter::name() .eq("2") - .and(NodeFilter::property("p2").eq(2u64)); + .and(NodeFilter.property("p2").eq(2u64)); let expected_results = vec!["2"]; assert_filter_neighbours_results( |graph| init_edges_graph(init_nodes_graph(graph)), @@ -4220,7 +4082,7 @@ pub(crate) mod test_filters { #[test] fn test_in_neighbours_filter() { - let filter = NodeFilter::property("p9").ge(1u64); + let filter = NodeFilter.property("p9").ge(1u64); let expected_results = vec!["1"]; assert_filter_neighbours_results( |graph| init_edges_graph(init_nodes_graph(graph)), @@ -4235,7 +4097,7 @@ pub(crate) mod test_filters { #[test] fn test_neighbours_filter() { - let filter = NodeFilter::property("p10").contains("Paper"); + let filter = NodeFilter.property("p10").contains("Paper"); let expected_results = vec!["1", "3"]; assert_filter_neighbours_results( |graph| init_edges_graph(init_nodes_graph(graph)), @@ -4638,56 +4500,56 @@ pub(crate) mod test_filters { // ------ Property: SUM ---- #[test] fn test_node_property_sum_u8s() { - let filter = NodeFilter::property("p_u8s").sum().eq(Prop::U64(10)); + let filter = NodeFilter.property("p_u8s").sum().eq(Prop::U64(10)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_sum_u16s() { - let filter = NodeFilter::property("p_u16s").sum().eq(Prop::U64(6)); + let filter = NodeFilter.property("p_u16s").sum().eq(Prop::U64(6)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_sum_u32s() { - let filter = NodeFilter::property("p_u32s").sum().eq(Prop::U64(10)); + let filter = NodeFilter.property("p_u32s").sum().eq(Prop::U64(10)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_sum_u64s() { - let filter = NodeFilter::property("p_u64s").sum().eq(Prop::U64(6)); + let filter = NodeFilter.property("p_u64s").sum().eq(Prop::U64(6)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_sum_i32s() { - let filter = NodeFilter::property("p_i32s").sum().eq(Prop::I64(2)); + let filter = NodeFilter.property("p_i32s").sum().eq(Prop::I64(2)); let expected = vec!["n6"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_sum_i64s() { - let filter = NodeFilter::property("p_i64s").sum().eq(Prop::I64(0)); + let filter = NodeFilter.property("p_i64s").sum().eq(Prop::I64(0)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_sum_f32s() { - let filter = NodeFilter::property("p_f32s").sum().eq(Prop::F64(6.5)); + let filter = NodeFilter.property("p_f32s").sum().eq(Prop::F64(6.5)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_sum_f64s() { - let filter = NodeFilter::property("p_f64s").sum().eq(Prop::F64(120.0)); + let filter = NodeFilter.property("p_f64s").sum().eq(Prop::F64(120.0)); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } @@ -4695,35 +4557,36 @@ pub(crate) mod test_filters { // ------ Property: AVG ---- #[test] fn test_node_property_avg_u8s() { - let filter = NodeFilter::property("p_u8s").avg().eq(Prop::F64(2.5)); + let filter = NodeFilter.property("p_u8s").avg().eq(Prop::F64(2.5)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_avg_u16s() { - let filter = NodeFilter::property("p_u16s").avg().eq(Prop::F64(2.0)); + let filter = NodeFilter.property("p_u16s").avg().eq(Prop::F64(2.0)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_avg_u32s() { - let filter = NodeFilter::property("p_u32s").avg().eq(Prop::F64(2.5)); + let filter = NodeFilter.property("p_u32s").avg().eq(Prop::F64(2.5)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_avg_u64s() { - let filter = NodeFilter::property("p_u64s").avg().eq(Prop::F64(2.0)); + let filter = NodeFilter.property("p_u64s").avg().eq(Prop::F64(2.0)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_avg_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .avg() .eq(Prop::F64(0.6666666666666666)); let expected = vec!["n6"]; @@ -4732,14 +4595,15 @@ pub(crate) mod test_filters { #[test] fn test_node_property_avg_i64s() { - let filter = NodeFilter::property("p_i64s").avg().eq(Prop::F64(0.0)); + let filter = NodeFilter.property("p_i64s").avg().eq(Prop::F64(0.0)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_avg_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .avg() .eq(Prop::F64(2.1666666666666665)); let expected = vec!["n10", "n3"]; @@ -4748,7 +4612,7 @@ pub(crate) mod test_filters { #[test] fn test_node_property_avg_f64s() { - let filter = NodeFilter::property("p_f64s").avg().eq(Prop::F64(40.0)); + let filter = NodeFilter.property("p_f64s").avg().eq(Prop::F64(40.0)); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } @@ -4756,63 +4620,63 @@ pub(crate) mod test_filters { // ------ Property: LEN ------ #[test] fn test_node_property_len_u8s() { - let filter = NodeFilter::property("p_u8s").len().eq(Prop::U64(4)); + let filter = NodeFilter.property("p_u8s").len().eq(Prop::U64(4)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_u16s() { - let filter = NodeFilter::property("p_u16s").len().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_u16s").len().eq(Prop::U64(3)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_u32s() { - let filter = NodeFilter::property("p_u32s").len().eq(Prop::U64(4)); + let filter = NodeFilter.property("p_u32s").len().eq(Prop::U64(4)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_u64s() { - let filter = NodeFilter::property("p_u64s").len().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_u64s").len().eq(Prop::U64(3)); let expected = vec!["n10", "n3", "n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_i32s() { - let filter = NodeFilter::property("p_i32s").len().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_i32s").len().eq(Prop::U64(3)); let expected = vec!["n10", "n3", "n4", "n6"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_i64s() { - let filter = NodeFilter::property("p_i64s").len().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_i64s").len().eq(Prop::U64(3)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_f32s() { - let filter = NodeFilter::property("p_f32s").len().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_f32s").len().eq(Prop::U64(3)); let expected = vec!["n10", "n3", "n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_f64s() { - let filter = NodeFilter::property("p_f64s").len().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_f64s").len().eq(Prop::U64(3)); let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_len_strs() { - let filter = NodeFilter::property("p_strs").len().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_strs").len().eq(Prop::U64(3)); let expected = vec!["n10", "n3", "n4"]; apply_assertion(filter, &expected); } @@ -4820,56 +4684,56 @@ pub(crate) mod test_filters { // ------ Property: MIN ------ #[test] fn test_node_property_min_u8s() { - let filter = NodeFilter::property("p_u8s").min().eq(Prop::U8(1)); + let filter = NodeFilter.property("p_u8s").min().eq(Prop::U8(1)); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_min_u16s() { - let filter = NodeFilter::property("p_u16s").min().eq(Prop::U16(1)); + let filter = NodeFilter.property("p_u16s").min().eq(Prop::U16(1)); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_min_u32s() { - let filter = NodeFilter::property("p_u32s").min().eq(Prop::U32(1)); + let filter = NodeFilter.property("p_u32s").min().eq(Prop::U32(1)); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_min_u64s() { - let filter = NodeFilter::property("p_u64s").min().eq(Prop::U64(1)); + let filter = NodeFilter.property("p_u64s").min().eq(Prop::U64(1)); let expected = vec!["n1", "n10", "n2", "n3", "n5"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_min_i32s() { - let filter = NodeFilter::property("p_i32s").min().eq(Prop::I32(-2)); + let filter = NodeFilter.property("p_i32s").min().eq(Prop::I32(-2)); let expected = vec!["n6"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_min_i64s() { - let filter = NodeFilter::property("p_i64s").min().eq(Prop::I64(-3)); + let filter = NodeFilter.property("p_i64s").min().eq(Prop::I64(-3)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_min_f32s() { - let filter = NodeFilter::property("p_f32s").min().eq(Prop::F32(10.0)); + let filter = NodeFilter.property("p_f32s").min().eq(Prop::F32(10.0)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_min_f64s() { - let filter = NodeFilter::property("p_f64s").min().eq(Prop::F64(40.0)); + let filter = NodeFilter.property("p_f64s").min().eq(Prop::F64(40.0)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } @@ -4877,56 +4741,56 @@ pub(crate) mod test_filters { // ------ Property: MAX ------ #[test] fn test_node_property_max_u8s() { - let filter = NodeFilter::property("p_u8s").max().eq(Prop::U8(4)); + let filter = NodeFilter.property("p_u8s").max().eq(Prop::U8(4)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_max_u16s() { - let filter = NodeFilter::property("p_u16s").max().eq(Prop::U16(3)); + let filter = NodeFilter.property("p_u16s").max().eq(Prop::U16(3)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_max_u32s() { - let filter = NodeFilter::property("p_u32s").max().eq(Prop::U32(4)); + let filter = NodeFilter.property("p_u32s").max().eq(Prop::U32(4)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_max_u64s() { - let filter = NodeFilter::property("p_u64s").max().eq(Prop::U64(3)); + let filter = NodeFilter.property("p_u64s").max().eq(Prop::U64(3)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_max_i32s() { - let filter = NodeFilter::property("p_i32s").max().eq(Prop::I32(3)); + let filter = NodeFilter.property("p_i32s").max().eq(Prop::I32(3)); let expected = vec!["n10", "n3", "n6"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_max_i64s() { - let filter = NodeFilter::property("p_i64s").max().eq(Prop::I64(2)); + let filter = NodeFilter.property("p_i64s").max().eq(Prop::I64(2)); let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_max_f32s() { - let filter = NodeFilter::property("p_f32s").max().eq(Prop::F32(30.0)); + let filter = NodeFilter.property("p_f32s").max().eq(Prop::F32(30.0)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_max_f64s() { - let filter = NodeFilter::property("p_f64s").max().eq(Prop::F64(50.0)); + let filter = NodeFilter.property("p_f64s").max().eq(Prop::F64(50.0)); let expected = vec!["n1", "n10", "n2", "n3"]; apply_assertion(filter, &expected); } @@ -4934,56 +4798,56 @@ pub(crate) mod test_filters { // ------ Metadata: SUM ------ #[test] fn test_node_property_metadata_sum_u8s() { - let filter = NodeFilter::metadata("p_u8s").sum().eq(Prop::U64(11)); + let filter = NodeFilter.metadata("p_u8s").sum().eq(Prop::U64(11)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_sum_u16s() { - let filter = NodeFilter::metadata("p_u16s").sum().eq(Prop::U64(8)); + let filter = NodeFilter.metadata("p_u16s").sum().eq(Prop::U64(8)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_sum_u32s() { - let filter = NodeFilter::metadata("p_u32s").sum().eq(Prop::U64(13)); + let filter = NodeFilter.metadata("p_u32s").sum().eq(Prop::U64(13)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_sum_u64s() { - let filter = NodeFilter::metadata("p_u64s").sum().eq(Prop::U64(12)); + let filter = NodeFilter.metadata("p_u64s").sum().eq(Prop::U64(12)); let expected = vec!["n2"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_sum_i32s() { - let filter = NodeFilter::metadata("p_i32s").sum().eq(Prop::I64(9)); + let filter = NodeFilter.metadata("p_i32s").sum().eq(Prop::I64(9)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_sum_i64s() { - let filter = NodeFilter::metadata("p_i64s").sum().eq(Prop::I64(20)); + let filter = NodeFilter.metadata("p_i64s").sum().eq(Prop::I64(20)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_sum_f32s() { - let filter = NodeFilter::metadata("p_f32s").sum().eq(Prop::F64(4.0)); + let filter = NodeFilter.metadata("p_f32s").sum().eq(Prop::F64(4.0)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_sum_f64s() { - let filter = NodeFilter::metadata("p_f64s").sum().eq(Prop::F64(2.0)); + let filter = NodeFilter.metadata("p_f64s").sum().eq(Prop::F64(2.0)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -4991,56 +4855,56 @@ pub(crate) mod test_filters { // ------ Metadata: AVG ------ #[test] fn test_node_property_metadata_avg_u8s() { - let filter = NodeFilter::metadata("p_u8s").avg().eq(Prop::F64(5.5)); + let filter = NodeFilter.metadata("p_u8s").avg().eq(Prop::F64(5.5)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_avg_u16s() { - let filter = NodeFilter::metadata("p_u16s").avg().eq(Prop::F64(4.0)); + let filter = NodeFilter.metadata("p_u16s").avg().eq(Prop::F64(4.0)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_avg_u32s() { - let filter = NodeFilter::metadata("p_u32s").avg().eq(Prop::F64(6.5)); + let filter = NodeFilter.metadata("p_u32s").avg().eq(Prop::F64(6.5)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_avg_u64s() { - let filter = NodeFilter::metadata("p_u64s").avg().eq(Prop::F64(4.0)); + let filter = NodeFilter.metadata("p_u64s").avg().eq(Prop::F64(4.0)); let expected = vec!["n2"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_avg_i32s() { - let filter = NodeFilter::metadata("p_i32s").avg().eq(Prop::F64(3.0)); + let filter = NodeFilter.metadata("p_i32s").avg().eq(Prop::F64(3.0)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_avg_i64s() { - let filter = NodeFilter::metadata("p_i64s").avg().eq(Prop::F64(5.0)); + let filter = NodeFilter.metadata("p_i64s").avg().eq(Prop::F64(5.0)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_avg_f32s() { - let filter = NodeFilter::metadata("p_f32s").avg().eq(Prop::F64(2.0)); + let filter = NodeFilter.metadata("p_f32s").avg().eq(Prop::F64(2.0)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_avg_f64s() { - let filter = NodeFilter::metadata("p_f64s").avg().eq(Prop::F64(1.0)); + let filter = NodeFilter.metadata("p_f64s").avg().eq(Prop::F64(1.0)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -5048,56 +4912,56 @@ pub(crate) mod test_filters { // ------ Metadata: MIN ------ #[test] fn test_node_property_metadata_min_u8s() { - let filter = NodeFilter::metadata("p_u8s").min().eq(Prop::U8(2)); + let filter = NodeFilter.metadata("p_u8s").min().eq(Prop::U8(2)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_min_u16s() { - let filter = NodeFilter::metadata("p_u16s").min().eq(Prop::U16(3)); + let filter = NodeFilter.metadata("p_u16s").min().eq(Prop::U16(3)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_min_u32s() { - let filter = NodeFilter::metadata("p_u32s").min().eq(Prop::U32(4)); + let filter = NodeFilter.metadata("p_u32s").min().eq(Prop::U32(4)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_min_u64s() { - let filter = NodeFilter::metadata("p_u64s").min().eq(Prop::U64(2)); + let filter = NodeFilter.metadata("p_u64s").min().eq(Prop::U64(2)); let expected = vec!["n2"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_min_i32s() { - let filter = NodeFilter::metadata("p_i32s").min().eq(Prop::I32(-3)); + let filter = NodeFilter.metadata("p_i32s").min().eq(Prop::I32(-3)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_min_i64s() { - let filter = NodeFilter::metadata("p_i64s").min().eq(Prop::I64(1)); + let filter = NodeFilter.metadata("p_i64s").min().eq(Prop::I64(1)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_min_f32s() { - let filter = NodeFilter::metadata("p_f32s").min().eq(Prop::F32(1.5)); + let filter = NodeFilter.metadata("p_f32s").min().eq(Prop::F32(1.5)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_min_f64s() { - let filter = NodeFilter::metadata("p_f64s").min().eq(Prop::F64(0.5)); + let filter = NodeFilter.metadata("p_f64s").min().eq(Prop::F64(0.5)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -5105,56 +4969,56 @@ pub(crate) mod test_filters { // ------ Metadata: MAX ------ #[test] fn test_node_property_metadata_max_u8s() { - let filter = NodeFilter::metadata("p_u8s").max().eq(Prop::U8(9)); + let filter = NodeFilter.metadata("p_u8s").max().eq(Prop::U8(9)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_max_u16s() { - let filter = NodeFilter::metadata("p_u16s").max().eq(Prop::U16(5)); + let filter = NodeFilter.metadata("p_u16s").max().eq(Prop::U16(5)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_max_u32s() { - let filter = NodeFilter::metadata("p_u32s").max().eq(Prop::U32(9)); + let filter = NodeFilter.metadata("p_u32s").max().eq(Prop::U32(9)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_max_u64s() { - let filter = NodeFilter::metadata("p_u64s").max().eq(Prop::U64(7)); + let filter = NodeFilter.metadata("p_u64s").max().eq(Prop::U64(7)); let expected = vec!["n2"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_max_i32s() { - let filter = NodeFilter::metadata("p_i32s").max().eq(Prop::I32(10)); + let filter = NodeFilter.metadata("p_i32s").max().eq(Prop::I32(10)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_max_i64s() { - let filter = NodeFilter::metadata("p_i64s").max().eq(Prop::I64(12)); + let filter = NodeFilter.metadata("p_i64s").max().eq(Prop::I64(12)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_max_f32s() { - let filter = NodeFilter::metadata("p_f32s").max().eq(Prop::F32(2.5)); + let filter = NodeFilter.metadata("p_f32s").max().eq(Prop::F32(2.5)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_max_f64s() { - let filter = NodeFilter::metadata("p_f64s").max().eq(Prop::F64(1.5)); + let filter = NodeFilter.metadata("p_f64s").max().eq(Prop::F64(1.5)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } @@ -5162,63 +5026,63 @@ pub(crate) mod test_filters { // ------ Metadata: Len ------ #[test] fn test_node_property_metadata_len_u8s() { - let filter = NodeFilter::metadata("p_u8s").len().eq(Prop::U64(2)); + let filter = NodeFilter.metadata("p_u8s").len().eq(Prop::U64(2)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_u16s() { - let filter = NodeFilter::metadata("p_u16s").len().eq(Prop::U64(2)); + let filter = NodeFilter.metadata("p_u16s").len().eq(Prop::U64(2)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_u32s() { - let filter = NodeFilter::metadata("p_u32s").len().eq(Prop::U64(2)); + let filter = NodeFilter.metadata("p_u32s").len().eq(Prop::U64(2)); let expected = vec!["n1"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_u64s() { - let filter = NodeFilter::metadata("p_u64s").len().eq(Prop::U64(3)); + let filter = NodeFilter.metadata("p_u64s").len().eq(Prop::U64(3)); let expected = vec!["n10", "n2"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_i32s() { - let filter = NodeFilter::metadata("p_i32s").len().eq(Prop::U64(3)); + let filter = NodeFilter.metadata("p_i32s").len().eq(Prop::U64(3)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_i64s() { - let filter = NodeFilter::metadata("p_i64s").len().eq(Prop::U64(4)); + let filter = NodeFilter.metadata("p_i64s").len().eq(Prop::U64(4)); let expected = vec!["n3"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_f32s() { - let filter = NodeFilter::metadata("p_f32s").len().eq(Prop::U64(2)); + let filter = NodeFilter.metadata("p_f32s").len().eq(Prop::U64(2)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_f64s() { - let filter = NodeFilter::metadata("p_f64s").len().eq(Prop::U64(2)); + let filter = NodeFilter.metadata("p_f64s").len().eq(Prop::U64(2)); let expected = vec!["n4"]; apply_assertion(filter, &expected); } #[test] fn test_node_property_metadata_len_strs() { - let filter = NodeFilter::metadata("p_strs").len().eq(Prop::U64(3)); + let filter = NodeFilter.metadata("p_strs").len().eq(Prop::U64(3)); let expected = vec!["n10", "n5"]; apply_assertion(filter, &expected); } @@ -5226,7 +5090,8 @@ pub(crate) mod test_filters { // ------ Temporal last: SUM ------ #[test] fn test_node_property_temporal_last_sum_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .last() .sum() @@ -5237,7 +5102,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_sum_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .last() .sum() @@ -5248,7 +5114,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_sum_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .last() .sum() @@ -5259,7 +5126,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_sum_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .last() .sum() @@ -5270,7 +5138,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_sum_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .last() .sum() @@ -5281,7 +5150,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_sum_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .last() .sum() @@ -5292,7 +5162,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_sum_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .last() .sum() @@ -5303,7 +5174,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_sum_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .last() .sum() @@ -5315,7 +5187,8 @@ pub(crate) mod test_filters { // ------ Temporal last: AVG ------ #[test] fn test_node_property_temporal_last_avg_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .last() .avg() @@ -5326,7 +5199,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_avg_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .last() .avg() @@ -5337,7 +5211,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_avg_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .last() .avg() @@ -5348,7 +5223,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_avg_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .last() .avg() @@ -5359,7 +5235,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_avg_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .last() .avg() @@ -5370,7 +5247,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_avg_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .last() .avg() @@ -5381,7 +5259,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_avg_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .last() .avg() @@ -5392,7 +5271,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_avg_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .last() .avg() @@ -5404,7 +5284,8 @@ pub(crate) mod test_filters { // ------ Temporal last: MIN ------ #[test] fn test_node_property_temporal_last_min_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .last() .min() @@ -5415,7 +5296,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_min_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .last() .min() @@ -5426,7 +5308,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_min_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .last() .min() @@ -5437,7 +5320,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_min_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .last() .min() @@ -5448,7 +5332,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_min_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .last() .min() @@ -5459,7 +5344,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_min_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .last() .min() @@ -5470,7 +5356,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_min_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .last() .min() @@ -5481,7 +5368,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_min_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .last() .min() @@ -5493,7 +5381,8 @@ pub(crate) mod test_filters { // ------ Temporal last: MAX ------ #[test] fn test_node_property_temporal_last_max_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .last() .max() @@ -5504,7 +5393,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_max_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .last() .max() @@ -5515,7 +5405,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_max_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .last() .max() @@ -5526,7 +5417,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_max_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .last() .max() @@ -5537,7 +5429,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_max_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .last() .max() @@ -5548,7 +5441,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_max_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .last() .max() @@ -5559,7 +5453,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_max_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .last() .max() @@ -5570,7 +5465,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_max_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .last() .max() @@ -5582,7 +5478,8 @@ pub(crate) mod test_filters { // ------ Temporal last: LEN ------ #[test] fn test_node_property_temporal_last_len_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .last() .len() @@ -5593,7 +5490,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_len_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .last() .len() @@ -5604,7 +5502,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_len_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .last() .len() @@ -5615,7 +5514,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_len_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .last() .len() @@ -5626,7 +5526,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_len_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .last() .len() @@ -5637,7 +5538,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_len_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .last() .len() @@ -5648,7 +5550,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_len_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .last() .len() @@ -5659,7 +5562,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_last_len_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .last() .len() @@ -5671,7 +5575,8 @@ pub(crate) mod test_filters { // ------ Temporal all: SUM ------ #[test] fn test_node_property_temporal_all_sum_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .all() .sum() @@ -5682,7 +5587,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_sum_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .all() .sum() @@ -5693,7 +5599,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_sum_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .all() .sum() @@ -5704,7 +5611,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_sum_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .all() .sum() @@ -5715,7 +5623,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_sum_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .all() .sum() @@ -5726,7 +5635,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_sum_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .all() .sum() @@ -5737,7 +5647,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_sum_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .all() .sum() @@ -5748,7 +5659,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_sum_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .all() .sum() @@ -5760,7 +5672,8 @@ pub(crate) mod test_filters { // ------ Temporal all: AVG ------ #[test] fn test_node_property_temporal_all_avg_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .all() .avg() @@ -5771,7 +5684,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_avg_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .all() .avg() @@ -5782,7 +5696,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_avg_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .all() .avg() @@ -5793,7 +5708,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_avg_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .all() .avg() @@ -5804,7 +5720,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_avg_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .all() .avg() @@ -5815,7 +5732,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_avg_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .all() .avg() @@ -5826,7 +5744,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_avg_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .all() .avg() @@ -5837,7 +5756,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_avg_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .all() .avg() @@ -5849,7 +5769,8 @@ pub(crate) mod test_filters { // ------ Temporal all: MIN ------ #[test] fn test_node_property_temporal_all_min_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .all() .min() @@ -5860,7 +5781,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_min_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .all() .min() @@ -5871,7 +5793,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_min_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .all() .min() @@ -5882,7 +5805,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_min_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .all() .min() @@ -5893,7 +5817,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_min_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .all() .min() @@ -5904,7 +5829,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_min_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .all() .min() @@ -5915,7 +5841,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_min_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .all() .min() @@ -5926,7 +5853,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_min_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .all() .min() @@ -5938,7 +5866,8 @@ pub(crate) mod test_filters { // ------ Temporal all: MAX ------ #[test] fn test_node_property_temporal_all_max_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .all() .max() @@ -5949,7 +5878,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_max_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .all() .max() @@ -5960,7 +5890,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_max_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .all() .max() @@ -5971,7 +5902,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_max_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .all() .max() @@ -5982,7 +5914,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_max_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .all() .max() @@ -5993,7 +5926,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_max_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .all() .max() @@ -6004,7 +5938,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_max_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .all() .max() @@ -6015,7 +5950,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_max_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .all() .max() @@ -6027,7 +5963,8 @@ pub(crate) mod test_filters { // ------ Temporal all: LEN ------ #[test] fn test_node_property_temporal_all_len_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .all() .len() @@ -6038,7 +5975,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_len_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .all() .len() @@ -6049,7 +5987,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_len_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .all() .len() @@ -6060,7 +5999,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_len_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .all() .len() @@ -6071,7 +6011,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_len_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .all() .len() @@ -6082,7 +6023,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_len_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .all() .len() @@ -6093,7 +6035,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_len_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .all() .len() @@ -6104,7 +6047,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_all_len_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .all() .len() @@ -6116,7 +6060,8 @@ pub(crate) mod test_filters { // ------ Temporal first: SUM ------ #[test] fn test_node_property_temporal_first_sum_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .first() .sum() @@ -6127,7 +6072,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_sum_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .first() .sum() @@ -6138,7 +6084,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_sum_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .first() .sum() @@ -6149,7 +6096,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_sum_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .first() .sum() @@ -6160,7 +6108,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_sum_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .first() .sum() @@ -6171,7 +6120,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_sum_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .first() .sum() @@ -6182,7 +6132,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_sum_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .first() .sum() @@ -6193,7 +6144,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_sum_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .first() .sum() @@ -6205,7 +6157,8 @@ pub(crate) mod test_filters { // ------ Temporal first: AVG ------ #[test] fn test_node_property_temporal_first_avg_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .first() .avg() @@ -6216,7 +6169,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_avg_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .first() .avg() @@ -6227,7 +6181,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_avg_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .first() .avg() @@ -6238,7 +6193,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_avg_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .first() .avg() @@ -6249,7 +6205,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_avg_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .first() .avg() @@ -6260,7 +6217,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_avg_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .first() .avg() @@ -6271,7 +6229,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_avg_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .first() .avg() @@ -6282,7 +6241,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_avg_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .first() .avg() @@ -6294,7 +6254,8 @@ pub(crate) mod test_filters { // ------ Temporal first: MIN ------ #[test] fn test_node_property_temporal_first_min_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .first() .min() @@ -6305,7 +6266,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_min_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .first() .min() @@ -6316,7 +6278,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_min_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .first() .min() @@ -6327,7 +6290,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_min_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .first() .min() @@ -6338,7 +6302,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_min_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .first() .min() @@ -6349,7 +6314,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_min_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .first() .min() @@ -6360,7 +6326,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_min_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .first() .min() @@ -6371,7 +6338,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_min_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .first() .min() @@ -6383,7 +6351,8 @@ pub(crate) mod test_filters { // ------ Temporal first: MAX ------ #[test] fn test_node_property_temporal_first_max_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .first() .max() @@ -6394,7 +6363,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_max_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .first() .max() @@ -6405,7 +6375,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_max_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .first() .max() @@ -6416,7 +6387,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_max_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .first() .max() @@ -6427,7 +6399,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_max_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .first() .max() @@ -6438,7 +6411,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_max_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .first() .max() @@ -6449,7 +6423,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_max_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .first() .max() @@ -6460,7 +6435,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_max_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .first() .max() @@ -6472,7 +6448,8 @@ pub(crate) mod test_filters { // ------ Temporal first: LEN ------ #[test] fn test_node_property_temporal_first_len_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .first() .len() @@ -6483,7 +6460,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_len_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .first() .len() @@ -6494,7 +6472,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_len_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .first() .len() @@ -6505,7 +6484,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_len_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .first() .len() @@ -6516,7 +6496,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_len_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .first() .len() @@ -6527,7 +6508,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_len_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .first() .len() @@ -6538,7 +6520,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_len_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .first() .len() @@ -6549,7 +6532,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_first_len_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .first() .len() @@ -6561,7 +6545,8 @@ pub(crate) mod test_filters { // ------ Temporal any: SUM ------ #[test] fn test_node_property_temporal_any_sum_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .sum() @@ -6569,7 +6554,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .sum() @@ -6580,7 +6566,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_sum_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .sum() @@ -6588,7 +6575,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .sum() @@ -6599,7 +6587,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_sum_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .sum() @@ -6607,7 +6596,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .sum() @@ -6618,7 +6608,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_sum_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .sum() @@ -6626,7 +6617,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .sum() @@ -6637,7 +6629,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_sum_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .sum() @@ -6645,7 +6638,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .sum() @@ -6656,7 +6650,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_sum_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .sum() @@ -6664,7 +6659,8 @@ pub(crate) mod test_filters { let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .sum() @@ -6675,7 +6671,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_sum_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .sum() @@ -6683,7 +6680,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .sum() @@ -6694,7 +6692,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_sum_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .sum() @@ -6702,7 +6701,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .sum() @@ -6714,7 +6714,8 @@ pub(crate) mod test_filters { // ------ Temporal any: AVG ------ #[test] fn test_node_property_temporal_any_avg_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .avg() @@ -6722,7 +6723,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .avg() @@ -6733,7 +6735,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_avg_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .avg() @@ -6741,7 +6744,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .avg() @@ -6752,7 +6756,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_avg_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .avg() @@ -6760,7 +6765,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .avg() @@ -6771,7 +6777,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_avg_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .avg() @@ -6779,7 +6786,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .avg() @@ -6790,7 +6798,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_avg_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .avg() @@ -6798,7 +6807,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .avg() @@ -6809,7 +6819,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_avg_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .avg() @@ -6817,7 +6828,8 @@ pub(crate) mod test_filters { let expected = vec!["n3", "n10"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .avg() @@ -6828,7 +6840,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_avg_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .avg() @@ -6836,7 +6849,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .avg() @@ -6847,7 +6861,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_avg_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .avg() @@ -6855,7 +6870,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .avg() @@ -6867,7 +6883,8 @@ pub(crate) mod test_filters { // ------ Temporal any: MIN ------ #[test] fn test_node_property_temporal_any_min_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .min() @@ -6878,7 +6895,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_min_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .min() @@ -6889,7 +6907,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_min_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .min() @@ -6900,7 +6919,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_min_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .min() @@ -6911,7 +6931,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_min_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .min() @@ -6919,7 +6940,8 @@ pub(crate) mod test_filters { let expected = vec!["n6"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .min() @@ -6930,7 +6952,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_min_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .min() @@ -6938,7 +6961,8 @@ pub(crate) mod test_filters { let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .min() @@ -6949,7 +6973,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_min_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .min() @@ -6957,7 +6982,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .min() @@ -6968,7 +6994,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_min_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .min() @@ -6976,7 +7003,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n2", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .min() @@ -6988,7 +7016,8 @@ pub(crate) mod test_filters { // ------ Temporal any: MAX ------ #[test] fn test_node_property_temporal_any_max_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .max() @@ -6996,7 +7025,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .max() @@ -7007,7 +7037,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_max_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .max() @@ -7015,7 +7046,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .max() @@ -7026,7 +7058,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_max_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .max() @@ -7034,7 +7067,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .max() @@ -7045,7 +7079,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_max_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .max() @@ -7053,7 +7088,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .max() @@ -7064,7 +7100,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_max_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .max() @@ -7072,7 +7109,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4", "n6"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .max() @@ -7083,7 +7121,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_max_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .max() @@ -7091,7 +7130,8 @@ pub(crate) mod test_filters { let expected = vec!["n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .max() @@ -7102,7 +7142,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_max_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .max() @@ -7110,7 +7151,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .max() @@ -7121,7 +7163,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_max_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .max() @@ -7133,7 +7176,8 @@ pub(crate) mod test_filters { // ------ Temporal any: LEN ------ #[test] fn test_node_property_temporal_any_len_u8s() { - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .len() @@ -7141,7 +7185,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u8s") + let filter = NodeFilter + .property("p_u8s") .temporal() .any() .len() @@ -7152,7 +7197,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_len_u16s() { - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .len() @@ -7160,7 +7206,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u16s") + let filter = NodeFilter + .property("p_u16s") .temporal() .any() .len() @@ -7171,7 +7218,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_len_u32s() { - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .len() @@ -7179,7 +7227,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u32s") + let filter = NodeFilter + .property("p_u32s") .temporal() .any() .len() @@ -7190,7 +7239,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_len_u64s() { - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .len() @@ -7198,7 +7248,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n2"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .any() .len() @@ -7209,7 +7260,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_len_i32s() { - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .len() @@ -7217,7 +7269,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4", "n6"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i32s") + let filter = NodeFilter + .property("p_i32s") .temporal() .any() .len() @@ -7228,7 +7281,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_len_i64s() { - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .len() @@ -7236,7 +7290,8 @@ pub(crate) mod test_filters { let expected = vec!["n5"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i64s") + let filter = NodeFilter + .property("p_i64s") .temporal() .any() .len() @@ -7247,7 +7302,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_len_f32s() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .len() @@ -7255,7 +7311,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .len() @@ -7266,7 +7323,8 @@ pub(crate) mod test_filters { #[test] fn test_node_property_temporal_any_len_f64s() { - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .len() @@ -7274,7 +7332,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f64s") + let filter = NodeFilter + .property("p_f64s") .temporal() .any() .len() @@ -7286,15 +7345,16 @@ pub(crate) mod test_filters { // ------ EMPTY LISTS ------ #[test] fn test_empty_list_agg() { - let filter = NodeFilter::property("p_u64s").sum().eq(Prop::U64(0)); + let filter = NodeFilter.property("p_u64s").sum().eq(Prop::U64(0)); let expected: Vec<&str> = vec![]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s").avg().eq(Prop::F64(0.0)); + let filter = NodeFilter.property("p_u64s").avg().eq(Prop::F64(0.0)); let expected: Vec<&str> = vec![]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .last() .min() @@ -7302,7 +7362,8 @@ pub(crate) mod test_filters { let expected: Vec<&str> = vec![]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s") + let filter = NodeFilter + .property("p_u64s") .temporal() .first() .max() @@ -7310,11 +7371,11 @@ pub(crate) mod test_filters { let expected: Vec<&str> = vec![]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s").len().eq(Prop::U64(0)); + let filter = NodeFilter.property("p_u64s").len().eq(Prop::U64(0)); let expected: Vec<&str> = vec!["n7"]; apply_assertion(filter, &expected); - let filter = NodeFilter::metadata("p_u64s").len().eq(Prop::U64(0)); + let filter = NodeFilter.metadata("p_u64s").len().eq(Prop::U64(0)); let expected: Vec<&str> = vec!["n6"]; apply_assertion(filter, &expected); } @@ -7322,27 +7383,27 @@ pub(crate) mod test_filters { // ------ Unsupported filter operations ------ #[test] fn test_unsupported_filter_ops_agg() { - let filter = NodeFilter::property("p_u64s").sum().starts_with("abc"); + let filter = NodeFilter.property("p_u64s").sum().starts_with("abc"); let expected: &str = "Operator STARTS_WITH is not supported with list aggregation"; apply_assertion_err(filter, expected); - let filter = NodeFilter::property("p_u64s").avg().ends_with("abc"); + let filter = NodeFilter.property("p_u64s").avg().ends_with("abc"); let expected: &str = "Operator ENDS_WITH is not supported with list aggregation"; apply_assertion_err(filter, expected); - let filter = NodeFilter::property("p_u64s").min().is_none(); + let filter = NodeFilter.property("p_u64s").min().is_none(); let expected: &str = "Operator IS_NONE is not supported with list aggregation"; apply_assertion_err(filter, expected); - let filter = NodeFilter::property("p_u64s").max().is_some(); + let filter = NodeFilter.property("p_u64s").max().is_some(); let expected: &str = "Operator IS_SOME is not supported with list aggregation"; apply_assertion_err(filter, expected); - let filter = NodeFilter::property("p_u64s").len().contains("abc"); + let filter = NodeFilter.property("p_u64s").len().contains("abc"); let expected: &str = "Operator CONTAINS is not supported with list aggregation"; apply_assertion_err(filter, expected); - let filter = NodeFilter::property("p_u64s").sum().not_contains("abc"); + let filter = NodeFilter.property("p_u64s").sum().not_contains("abc"); let expected: &str = "Operator NOT_CONTAINS is not supported with list aggregation"; apply_assertion_err(filter, expected); } @@ -7350,51 +7411,55 @@ pub(crate) mod test_filters { // --------------- OVERFLOW --------------- #[test] fn test_max_value_agg() { - let filter = NodeFilter::property("p_u64s_max") + let filter = NodeFilter + .property("p_u64s_max") .max() .eq(Prop::U64(u64::MAX)); let expected: Vec<&str> = vec!["n5", "n1"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s_min") + let filter = NodeFilter + .property("p_u64s_min") .min() .eq(Prop::U64(u64::MIN)); let expected: Vec<&str> = vec!["n5"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u8s_max").sum().eq(Prop::U64(510)); + let filter = NodeFilter.property("p_u8s_max").sum().eq(Prop::U64(510)); let expected: Vec<&str> = vec!["n1"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u16s_max") + let filter = NodeFilter + .property("p_u16s_max") .sum() .eq(Prop::U64(131070)); let expected: Vec<&str> = vec!["n1"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u32s_max") + let filter = NodeFilter + .property("p_u32s_max") .sum() .eq(Prop::U64(8589934590)); let expected: Vec<&str> = vec!["n1"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_u64s_max").sum().gt(Prop::U64(0)); + let filter = NodeFilter.property("p_u64s_max").sum().gt(Prop::U64(0)); let expected: Vec<&str> = vec![]; apply_assertion(filter, &expected); // AVG is computed in f64 even if SUM overflowed. let avg = (u64::MAX as f64 + 1.0) / 2.0; - let filter = NodeFilter::property("p_u64s_max").avg().eq(avg); + let filter = NodeFilter.property("p_u64s_max").avg().eq(avg); let expected = vec!["n5"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_i64s_max").sum().gt(Prop::I64(0)); + let filter = NodeFilter.property("p_i64s_max").sum().gt(Prop::I64(0)); let expected: Vec<&str> = vec![]; apply_assertion(filter, &expected); // AVG is computed in f64 even if SUM overflowed. let avg = (i64::MAX as f64 + 1.0) / 2.0; - let filter = NodeFilter::property("p_i64s_max").avg().eq(avg); + let filter = NodeFilter.property("p_i64s_max").avg().eq(avg); let expected = vec!["n5"]; apply_assertion(filter, &expected); } @@ -7402,7 +7467,7 @@ pub(crate) mod test_filters { // ------ Property: any ------ #[test] fn test_node_property_any() { - let filter = NodeFilter::property("p_u8s").any().eq(Prop::U8(3)); + let filter = NodeFilter.property("p_u8s").any().eq(Prop::U8(3)); let expected = vec!["n1", "n10", "n3"]; apply_assertion(filter, &expected); } @@ -7410,7 +7475,8 @@ pub(crate) mod test_filters { // ------ Property: all ------ #[test] fn test_node_property_all() { - let filter = NodeFilter::property("p_bools_all") + let filter = NodeFilter + .property("p_bools_all") .all() .eq(Prop::Bool(true)); let expected = vec!["n10", "n4"]; @@ -7420,7 +7486,7 @@ pub(crate) mod test_filters { // ------ Metadata: any ------ #[test] fn test_node_metadata_any() { - let filter = NodeFilter::metadata("p_u64s").any().eq(Prop::U64(1)); + let filter = NodeFilter.metadata("p_u64s").any().eq(Prop::U64(1)); let expected = vec!["n10", "n7"]; apply_assertion(filter, &expected); } @@ -7428,7 +7494,7 @@ pub(crate) mod test_filters { // ------ Metadata: all ------ #[test] fn test_node_metadata_all() { - let filter = NodeFilter::metadata("p_strs").all().eq("a"); + let filter = NodeFilter.metadata("p_strs").all().eq("a"); let expected = vec!["n6", "n7"]; apply_assertion(filter, &expected); } @@ -7436,7 +7502,8 @@ pub(crate) mod test_filters { // ------ Temporal First: any ------ #[test] fn test_node_temporal_property_first_any() { - let filter = NodeFilter::property("p_bools") + let filter = NodeFilter + .property("p_bools") .temporal() .first() .any() @@ -7448,7 +7515,8 @@ pub(crate) mod test_filters { // ------ Temporal First: all ------ #[test] fn test_node_temporal_property_first_all() { - let filter = NodeFilter::property("p_bools_all") + let filter = NodeFilter + .property("p_bools_all") .temporal() .first() .all() @@ -7460,7 +7528,8 @@ pub(crate) mod test_filters { // ------ Temporal last: any ------ #[test] fn test_node_temporal_property_last_any() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .last() .any() @@ -7472,7 +7541,8 @@ pub(crate) mod test_filters { // ------ Temporal last: all ------ #[test] fn test_node_temporal_property_last_all() { - let filter = NodeFilter::property("p_bools_all") + let filter = NodeFilter + .property("p_bools_all") .temporal() .last() .all() @@ -7484,7 +7554,8 @@ pub(crate) mod test_filters { // ------ Temporal Any: any ------ #[test] fn test_node_temporal_property_any_any() { - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .any() @@ -7492,7 +7563,8 @@ pub(crate) mod test_filters { let expected = vec!["n1", "n10", "n3", "n4"]; apply_assertion(filter, &expected); - let filter = NodeFilter::property("p_f32s") + let filter = NodeFilter + .property("p_f32s") .temporal() .any() .any() @@ -7504,7 +7576,8 @@ pub(crate) mod test_filters { // ------ Temporal Any: all ------ #[test] fn test_node_temporal_property_any_all() { - let filter = NodeFilter::property("p_bools") + let filter = NodeFilter + .property("p_bools") .temporal() .any() .all() @@ -7515,7 +7588,8 @@ pub(crate) mod test_filters { #[test] fn test_node_nested_list_property_all_all_all_any() { - let filter = NodeFilter::property("nested_list") + let filter = NodeFilter + .property("nested_list") .all() .all() .all() @@ -7528,7 +7602,8 @@ pub(crate) mod test_filters { #[test] fn test_node_nested_list_temporal_property_all_all_all_all_any() { - let filter = NodeFilter::property("nested_list") + let filter = NodeFilter + .property("nested_list") .temporal() .all() .all() @@ -7544,7 +7619,8 @@ pub(crate) mod test_filters { // ------ Temporal All: any ------ #[test] fn test_node_temporal_property_all_any() { - let filter = NodeFilter::property("p_bools") + let filter = NodeFilter + .property("p_bools") .temporal() .all() .any() @@ -7556,7 +7632,8 @@ pub(crate) mod test_filters { // ------ Temporal All: all ------ #[test] fn test_node_temporal_property_all_all() { - let filter = NodeFilter::property("p_bools_all") + let filter = NodeFilter + .property("p_bools_all") .temporal() .all() .all() @@ -8673,7 +8750,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_eq() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").eq(2u64); + let filter = EdgeFilter.property("p2").eq(2u64); let expected_results = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, @@ -8690,10 +8767,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p30") - .temporal() - .first() - .eq("Old_boat"); + let filter = EdgeFilter.property("p30").temporal().first().eq("Old_boat"); let expected_results = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, @@ -8710,7 +8784,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p20").temporal().all().eq("Gold_ship"); + let filter = EdgeFilter.property("p20").temporal().all().eq("Gold_ship"); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -8731,7 +8805,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_ne() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").ne(2u64); + let filter = EdgeFilter.property("p2").ne(2u64); let expected_results = vec![ "1->2", "2->1", @@ -8754,10 +8828,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p30") - .temporal() - .first() - .ne("Old_boat"); + let filter = EdgeFilter.property("p30").temporal().first().ne("Old_boat"); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -8774,7 +8845,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p30").temporal().all().ne("Classic"); + let filter = EdgeFilter.property("p30").temporal().all().ne("Classic"); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -8795,7 +8866,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_lt() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").lt(10u64); + let filter = EdgeFilter.property("p2").lt(10u64); let expected_results = vec![ "1->2", "2->1", @@ -8819,7 +8890,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().first().lt(5u64); + let filter = EdgeFilter.property("p2").temporal().first().lt(5u64); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -8836,7 +8907,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().all().lt(10u64); + let filter = EdgeFilter.property("p2").temporal().all().lt(10u64); let expected_results = vec![ "1->2", "2->1", @@ -8864,7 +8935,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_le() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").le(6u64); + let filter = EdgeFilter.property("p2").le(6u64); let expected_results = vec![ "1->2", "2->1", @@ -8888,7 +8959,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().first().le(3u64); + let filter = EdgeFilter.property("p2").temporal().first().le(3u64); let expected_results = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, @@ -8905,7 +8976,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().all().le(5u64); + let filter = EdgeFilter.property("p2").temporal().all().le(5u64); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -8926,7 +8997,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_gt() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").gt(2u64); + let filter = EdgeFilter.property("p2").gt(2u64); let expected_results = vec![ "1->2", "2->1", @@ -8949,7 +9020,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().first().gt(5u64); + let filter = EdgeFilter.property("p2").temporal().first().gt(5u64); let expected_results = vec![ "2->1", "3->1", @@ -8971,7 +9042,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().all().gt(5u64); + let filter = EdgeFilter.property("p2").temporal().all().gt(5u64); let expected_results = vec![ "2->1", "3->1", @@ -8997,7 +9068,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_ge() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").ge(2u64); + let filter = EdgeFilter.property("p2").ge(2u64); let expected_results = vec![ "1->2", "2->1", @@ -9021,7 +9092,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().first().ge(6u64); + let filter = EdgeFilter.property("p2").temporal().first().ge(6u64); let expected_results = vec![ "2->1", "3->1", @@ -9043,7 +9114,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().all().ge(6u64); + let filter = EdgeFilter.property("p2").temporal().all().ge(6u64); let expected_results = vec![ "2->1", "3->1", @@ -9069,7 +9140,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_in() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").is_in(vec![Prop::U64(6)]); + let filter = EdgeFilter.property("p2").is_in(vec![Prop::U64(6)]); let expected_results = vec![ "2->1", "3->1", @@ -9091,7 +9162,9 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").is_in(vec![Prop::U64(2), Prop::U64(6)]); + let filter = EdgeFilter + .property("p2") + .is_in(vec![Prop::U64(2), Prop::U64(6)]); let expected_results = vec![ "2->1", "2->3", @@ -9114,7 +9187,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .temporal() .first() .is_in(vec![Prop::U64(6)]); @@ -9139,7 +9213,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .temporal() .all() .is_in(vec![Prop::U64(6)]); @@ -9168,7 +9243,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_not_in() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").is_not_in(vec![Prop::U64(6)]); + let filter = EdgeFilter.property("p2").is_not_in(vec![Prop::U64(6)]); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -9185,7 +9260,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .temporal() .first() .is_not_in(vec![Prop::U64(6)]); @@ -9205,7 +9281,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .temporal() .all() .is_not_in(vec![Prop::U64(6)]); @@ -9229,7 +9306,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_is_some() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2").is_some(); + let filter = EdgeFilter.property("p2").is_some(); let expected_results = vec![ "1->2", "2->1", @@ -9253,7 +9330,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p2").temporal().first().is_some(); + let filter = EdgeFilter.property("p2").temporal().first().is_some(); let expected_results = vec![ "1->2", "2->1", @@ -9281,7 +9358,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_is_none() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. - let filter = EdgeFilter::property("p2").is_none(); + let filter = EdgeFilter.property("p2").is_none(); let expected_results = Vec::<&str>::new(); assert_filter_edges_results( init_edges_graph, @@ -9298,7 +9375,7 @@ pub(crate) mod test_filters { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("p2").temporal().first().is_none(); + let filter = EdgeFilter.property("p2").temporal().first().is_none(); let expected_results = vec![]; assert_filter_edges_results( init_edges_graph, @@ -9319,7 +9396,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_starts_with() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p10").starts_with("Pa"); + let filter = EdgeFilter.property("p10").starts_with("Pa"); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -9336,7 +9413,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .any() .starts_with("Pape"); @@ -9356,7 +9434,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .last() .starts_with("Paper"); @@ -9376,7 +9455,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .last() .starts_with("Traffic"); @@ -9396,7 +9476,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p30") + let filter = EdgeFilter + .property("p30") .temporal() .first() .starts_with("Old"); @@ -9416,7 +9497,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p20") + let filter = EdgeFilter + .property("p20") .temporal() .all() .starts_with("Gold"); @@ -9440,7 +9522,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_ends_with() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p10").ends_with("lane"); + let filter = EdgeFilter.property("p10").ends_with("lane"); let expected_results: Vec<&str> = vec!["1->2", "2->1"]; assert_filter_edges_results( init_edges_graph, @@ -9457,7 +9539,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .any() .ends_with("ship"); @@ -9477,7 +9560,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .last() .ends_with("ane"); @@ -9497,7 +9581,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .last() .ends_with("marcus"); @@ -9517,7 +9602,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p20") + let filter = EdgeFilter + .property("p20") .temporal() .first() .ends_with("boat"); @@ -9537,7 +9623,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p20") + let filter = EdgeFilter + .property("p20") .temporal() .all() .ends_with("ship"); @@ -9561,7 +9648,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_contains() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p10").contains("Paper"); + let filter = EdgeFilter.property("p10").contains("Paper"); let expected_results: Vec<&str> = vec!["1->2", "2->1", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -9578,7 +9665,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .any() .contains("Paper"); @@ -9598,7 +9686,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .last() .contains("Paper"); @@ -9618,7 +9707,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p20") + let filter = EdgeFilter + .property("p20") .temporal() .first() .contains("boat"); @@ -9638,10 +9728,7 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p20") - .temporal() - .all() - .contains("ship"); + let filter = EdgeFilter.property("p20").temporal().all().contains("ship"); let expected_results: Vec<&str> = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -9662,7 +9749,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_property_contains_not() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p10").not_contains("ship"); + let filter = EdgeFilter.property("p10").not_contains("ship"); let expected_results: Vec<&str> = vec!["1->2", "2->1"]; assert_filter_edges_results( init_edges_graph, @@ -9679,7 +9766,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .any() .not_contains("ship"); @@ -9699,7 +9787,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p10") + let filter = EdgeFilter + .property("p10") .temporal() .last() .not_contains("ship"); @@ -9719,7 +9808,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p20") + let filter = EdgeFilter + .property("p20") .temporal() .first() .not_contains("boat"); @@ -9739,7 +9829,8 @@ pub(crate) mod test_filters { TestVariants::All, ); - let filter = EdgeFilter::property("p30") + let filter = EdgeFilter + .property("p30") .temporal() .all() .not_contains("ship"); @@ -9764,7 +9855,7 @@ pub(crate) mod test_filters { fn test_filter_edges_by_fuzzy_search() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. // TODO: Enable these test for event_disk_graph, persistent_disk_graph once string property is fixed. - let filter = EdgeFilter::property("p1").fuzzy_search("shiv", 2, true); + let filter = EdgeFilter.property("p1").fuzzy_search("shiv", 2, true); let expected_results: Vec<&str> = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -9774,7 +9865,7 @@ pub(crate) mod test_filters { vec![TestGraphVariants::Graph], ); - let filter = EdgeFilter::property("p1").fuzzy_search("ShiV", 2, true); + let filter = EdgeFilter.property("p1").fuzzy_search("ShiV", 2, true); let expected_results: Vec<&str> = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -9784,7 +9875,7 @@ pub(crate) mod test_filters { vec![TestGraphVariants::Graph], ); - let filter = EdgeFilter::property("p1").fuzzy_search("shiv", 2, false); + let filter = EdgeFilter.property("p1").fuzzy_search("shiv", 2, false); let expected_results: Vec<&str> = vec![]; assert_filter_edges_results( init_edges_graph, @@ -9798,7 +9889,7 @@ pub(crate) mod test_filters { #[test] fn test_filter_edges_for_not_property() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. Search API uses filter API internally for this filter. - let filter = EdgeFilter::property("p2").ne(2u64).not(); + let filter = EdgeFilter.property("p2").ne(2u64).not(); let expected_results = vec!["2->3"]; assert_filter_edges_results( init_edges_graph, @@ -9815,6 +9906,194 @@ pub(crate) mod test_filters { TestVariants::EventOnly, ); } + + #[test] + fn test_edges_window_filter() { + let filter = EdgeFilter::window(1, 3) + .property("p2") + .temporal() + .sum() + .ge(2u64); + + let expected_results = vec!["1->2", "2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + + let filter = EdgeFilter::window(1, 5) + .property("p2") + .temporal() + .sum() + .ge(2u64); + + let expected_results = vec![ + "1->2", + "2->3", + "3->1", + "2->1", + "David Gilmour->John Mayer", + "John Mayer->Jimmy Page", + ]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + } + + #[test] + fn test_edges_window_filter_on_non_temporal_property() { + let filter = EdgeFilter::window(1, 2).property("p1").eq("shivam_kapoor"); + let expected_results = vec!["1->2"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter, + &expected_results, + TestVariants::All, + ); + + let filter2 = EdgeFilter::window(4, 5).property("p1").eq("shivam_kapoor"); + let expected_results = vec![]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter2.clone(), + &expected_results, + TestVariants::EventOnly, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter2, + &expected_results, + TestVariants::EventOnly, + ); + + let filter = EdgeFilter::window(4, 5).property("p1").eq("shivam_kapoor"); + let expected_results = vec!["1->2"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::PersistentOnly, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter, + &expected_results, + TestVariants::PersistentOnly, + ); + } + + #[test] + fn test_edges_window_filter_any_all_over_window() { + let filter_any = EdgeFilter::window(2, 4) + .property("p20") + .temporal() + .any() + .eq("Gold_boat"); + + let expected_any = vec!["2->3"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter_any.clone(), + &expected_any, + TestVariants::All, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter_any.clone(), + &expected_any, + TestVariants::All, + ); + + let filter_all = EdgeFilter::window(2, 4) + .property("p20") + .temporal() + .all() + .eq("Gold_boat"); + + let expected_all: Vec<&str> = vec![]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter_all.clone(), + &expected_all, + TestVariants::All, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter_all.clone(), + &expected_all, + TestVariants::All, + ); + } + + #[test] + fn test_edges_window_filter_and() { + let filter1 = EdgeFilter::window(3, 6) + .property("p10") + .temporal() + .any() + .eq("Paper_airplane"); + + let filter2 = EdgeFilter::window(3, 6) + .property("p2") + .temporal() + .sum() + .eq(6u64); + + let filter = filter1.and(filter2); + + let expected_results = vec!["2->1"]; + assert_filter_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter.clone(), + &expected_results, + TestVariants::All, + ); + assert_search_edges_results( + init_edges_graph, + IdentityGraphTransformer, + filter, + &expected_results, + TestVariants::All, + ); + } } #[cfg(test)] @@ -9862,9 +10141,10 @@ pub(crate) mod test_filters { #[test] fn test_unique_results_from_composite_filters() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .ge(2u64) - .and(EdgeFilter::property("p2").ge(1u64)); + .and(EdgeFilter.property("p2").ge(1u64)); let expected_results = vec![ "1->2", "2->1", @@ -9881,9 +10161,10 @@ pub(crate) mod test_filters { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .ge(2u64) - .or(EdgeFilter::property("p2").ge(5u64)); + .or(EdgeFilter.property("p2").ge(5u64)); let expected_results = vec![ "1->2", "2->1", @@ -9905,9 +10186,10 @@ pub(crate) mod test_filters { fn test_composite_filter_edges() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for both filter_edges and search_edges. // TODO: Enable these test for event_disk_graph, persistent_disk_graph once string property is fixed. - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .eq(2u64) - .and(EdgeFilter::property("p1").eq("kapoor")); + .and(EdgeFilter.property("p1").eq("kapoor")); let expected_results = Vec::<&str>::new(); assert_filter_edges_results( init_edges_graph, @@ -9939,9 +10221,10 @@ pub(crate) mod test_filters { TestVariants::NonDiskOnly, ); - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .eq(2u64) - .or(EdgeFilter::property("p1").eq("shivam_kapoor")); + .or(EdgeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1->2", "2->3"]; assert_filter_edges_results( init_edges_graph, @@ -9973,11 +10256,10 @@ pub(crate) mod test_filters { TestVariants::NonDiskOnly, ); - let filter = EdgeFilter::property("p1") - .eq("pometry") - .or(EdgeFilter::property("p2") - .eq(6u64) - .and(EdgeFilter::property("p3").eq(1u64))); + let filter = EdgeFilter.property("p1").eq("pometry").or(EdgeFilter + .property("p2") + .eq(6u64) + .and(EdgeFilter.property("p3").eq(1u64))); let expected_results = vec![ "2->1", "3->1", @@ -10017,7 +10299,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::src() .name() .eq("13") - .and(EdgeFilter::property("p1").eq("prop1")); + .and(EdgeFilter.property("p1").eq("prop1")); let expected_results = Vec::<&str>::new(); assert_filter_edges_results( init_edges_graph, @@ -10049,9 +10331,10 @@ pub(crate) mod test_filters { TestVariants::NonDiskOnly, ); - let filter = EdgeFilter::property("p2") + let filter = EdgeFilter + .property("p2") .eq(4u64) - .and(EdgeFilter::property("p1").eq("shivam_kapoor")); + .and(EdgeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -10086,7 +10369,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::src() .name() .eq("1") - .and(EdgeFilter::property("p1").eq("shivam_kapoor")); + .and(EdgeFilter.property("p1").eq("shivam_kapoor")); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -10121,7 +10404,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::dst() .name() .eq("1") - .and(EdgeFilter::property("p2").eq(6u64)); + .and(EdgeFilter.property("p2").eq(6u64)); let expected_results = vec!["2->1", "3->1"]; assert_filter_edges_results( init_edges_graph, @@ -10156,8 +10439,8 @@ pub(crate) mod test_filters { let filter = EdgeFilter::src() .name() .eq("1") - .and(EdgeFilter::property("p1").eq("shivam_kapoor")) - .or(EdgeFilter::property("p3").eq(5u64)); + .and(EdgeFilter.property("p1").eq("shivam_kapoor")) + .or(EdgeFilter.property("p3").eq(5u64)); let expected_results = vec!["1->2"]; assert_filter_edges_results( init_edges_graph, @@ -10197,7 +10480,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::src() .name() .eq("13") - .and(EdgeFilter::property("p1").eq("prop1")) + .and(EdgeFilter.property("p1").eq("prop1")) .not(); let expected_results = vec![ "1->2", @@ -10226,7 +10509,7 @@ pub(crate) mod test_filters { let filter = EdgeFilter::src() .name() .eq("13") - .and(EdgeFilter::property("p1").eq("prop1").not()) + .and(EdgeFilter.property("p1").eq("prop1").not()) .not(); let expected_results = vec![ "1->2", diff --git a/raphtory/src/db/graph/views/filter/model/edge_filter.rs b/raphtory/src/db/graph/views/filter/model/edge_filter.rs index 01a494e5ec..19a0ab8b74 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -5,7 +5,7 @@ use crate::{ internal::CreateFilter, model::{ node_filter::CompositeNodeFilter, property_filter::PropertyFilter, AndFilter, - Filter, NotFilter, OrFilter, PropertyFilterFactory, TryAsCompositeFilter, + Filter, NotFilter, OrFilter, TryAsCompositeFilter, Windowed, }, }, }, @@ -13,6 +13,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::entities::GID; +use raphtory_core::utils::time::IntoTime; use std::{fmt, fmt::Display, ops::Deref, sync::Arc}; #[derive(Debug, Clone)] @@ -28,6 +29,7 @@ impl Display for EdgeFieldFilter { pub enum CompositeEdgeFilter { Edge(Filter), Property(PropertyFilter), + PropertyWindowed(PropertyFilter>), And(Box, Box), Or(Box, Box), Not(Box), @@ -38,6 +40,7 @@ impl Display for CompositeEdgeFilter { match self { CompositeEdgeFilter::Edge(filter) => write!(f, "{}", filter), CompositeEdgeFilter::Property(filter) => write!(f, "{}", filter), + CompositeEdgeFilter::PropertyWindowed(filter) => write!(f, "{}", filter), CompositeEdgeFilter::And(left, right) => write!(f, "({} AND {})", left, right), CompositeEdgeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), CompositeEdgeFilter::Not(filter) => write!(f, "(NOT {})", filter), @@ -55,6 +58,7 @@ impl CreateFilter for CompositeEdgeFilter { match self { CompositeEdgeFilter::Edge(i) => Ok(Arc::new(EdgeFieldFilter(i).create_filter(graph)?)), CompositeEdgeFilter::Property(i) => Ok(Arc::new(i.create_filter(graph)?)), + CompositeEdgeFilter::PropertyWindowed(i) => Ok(Arc::new(i.create_filter(graph)?)), CompositeEdgeFilter::And(l, r) => Ok(Arc::new( AndFilter { left: l.deref().clone(), @@ -96,6 +100,7 @@ impl TryAsCompositeFilter for CompositeEdgeFilter { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CompositeExplodedEdgeFilter { Property(PropertyFilter), + PropertyWindowed(PropertyFilter>), And( Box, Box, @@ -111,6 +116,7 @@ impl Display for CompositeExplodedEdgeFilter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CompositeExplodedEdgeFilter::Property(filter) => write!(f, "{}", filter), + CompositeExplodedEdgeFilter::PropertyWindowed(filter) => write!(f, "{}", filter), CompositeExplodedEdgeFilter::And(left, right) => write!(f, "({} AND {})", left, right), CompositeExplodedEdgeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), CompositeExplodedEdgeFilter::Not(filter) => write!(f, "(NOT {})", filter), @@ -127,6 +133,9 @@ impl CreateFilter for CompositeExplodedEdgeFilter { ) -> Result, GraphError> { match self { CompositeExplodedEdgeFilter::Property(i) => Ok(Arc::new(i.create_filter(graph)?)), + CompositeExplodedEdgeFilter::PropertyWindowed(i) => { + Ok(Arc::new(i.create_filter(graph)?)) + } CompositeExplodedEdgeFilter::And(l, r) => Ok(Arc::new( AndFilter { left: l.deref().clone(), @@ -343,7 +352,7 @@ impl InternalEdgeFilterBuilderOps for EdgeDestinationFilterBuilder { } } -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] pub struct EdgeFilter; #[derive(Clone)] @@ -373,17 +382,24 @@ impl EdgeFilter { pub fn src() -> EdgeEndpointFilter { EdgeEndpointFilter::Src } + pub fn dst() -> EdgeEndpointFilter { EdgeEndpointFilter::Dst } -} -impl PropertyFilterFactory for EdgeFilter {} + pub fn window(start: S, end: E) -> Windowed { + Windowed::from_times(start, end) + } +} -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Copy, Default, PartialEq, Eq)] pub struct ExplodedEdgeFilter; -impl PropertyFilterFactory for ExplodedEdgeFilter {} +impl ExplodedEdgeFilter { + pub fn window(start: S, end: E) -> Windowed { + Windowed::from_times(start, end) + } +} impl TryAsCompositeFilter for EdgeFieldFilter { fn try_as_composite_node_filter(&self) -> Result { diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 3d934244c8..909da10aea 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -1,9 +1,12 @@ pub(crate) use crate::db::graph::views::filter::model::and_filter::AndFilter; use crate::{ db::graph::views::filter::model::{ - edge_filter::{CompositeEdgeFilter, CompositeExplodedEdgeFilter, EdgeFieldFilter}, + edge_filter::{ + CompositeEdgeFilter, CompositeExplodedEdgeFilter, EdgeFieldFilter, EdgeFilter, + ExplodedEdgeFilter, + }, filter_operator::FilterOperator, - node_filter::{CompositeNodeFilter, NodeNameFilter, NodeTypeFilter}, + node_filter::{CompositeNodeFilter, NodeFilter, NodeNameFilter, NodeTypeFilter}, not_filter::NotFilter, or_filter::OrFilter, property_filter::{MetadataFilterBuilder, PropertyFilter, PropertyFilterBuilder}, @@ -11,9 +14,13 @@ use crate::{ errors::GraphError, prelude::{GraphViewOps, NodeViewOps}, }; -use raphtory_api::core::entities::{GidRef, GID}; +use raphtory_api::core::{ + entities::{GidRef, GID}, + storage::timeindex::TimeIndexEntry, +}; +use raphtory_core::utils::time::IntoTime; use raphtory_storage::graph::edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}; -use std::{collections::HashSet, fmt, fmt::Display, ops::Deref, sync::Arc}; +use std::{collections::HashSet, fmt, fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; pub mod and_filter; pub mod edge_filter; @@ -23,6 +30,31 @@ pub mod not_filter; pub mod or_filter; pub mod property_filter; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Windowed { + pub start: TimeIndexEntry, + pub end: TimeIndexEntry, + pub _marker: PhantomData, +} + +impl Windowed { + #[inline] + pub fn new(start: TimeIndexEntry, end: TimeIndexEntry) -> Self { + Self { + start, + end, + _marker: PhantomData, + } + } + + #[inline] + pub fn from_times(start: S, end: E) -> Self { + let s = TimeIndexEntry::start(start.into_time()); + let e = TimeIndexEntry::end(end.into_time()); + Self::new(s, e) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum FilterValue { Single(String), @@ -330,12 +362,27 @@ impl ComposableFilter for AndFilter {} impl ComposableFilter for OrFilter {} impl ComposableFilter for NotFilter {} -pub trait PropertyFilterFactory { - fn property(name: impl Into) -> PropertyFilterBuilder { - PropertyFilterBuilder::new(name) - } +trait EntityMarker: Clone + Send + Sync {} + +impl EntityMarker for NodeFilter {} + +impl EntityMarker for EdgeFilter {} + +impl EntityMarker for ExplodedEdgeFilter {} + +impl EntityMarker for Windowed {} - fn metadata(name: impl Into) -> MetadataFilterBuilder { - MetadataFilterBuilder::new(name) +pub trait PropertyFilterFactory: Sized { + fn property(&self, name: impl Into) -> PropertyFilterBuilder; + + fn metadata(&self, name: impl Into) -> MetadataFilterBuilder; +} + +impl PropertyFilterFactory for M { + fn property(&self, name: impl Into) -> PropertyFilterBuilder { + PropertyFilterBuilder::new(name, self.clone()) + } + fn metadata(&self, name: impl Into) -> MetadataFilterBuilder { + MetadataFilterBuilder::new(name, self.clone()) } } diff --git a/raphtory/src/db/graph/views/filter/model/node_filter.rs b/raphtory/src/db/graph/views/filter/model/node_filter.rs index 2c2326d9db..5f6284ada4 100644 --- a/raphtory/src/db/graph/views/filter/model/node_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/node_filter.rs @@ -7,8 +7,8 @@ use crate::{ edge_filter::{CompositeEdgeFilter, CompositeExplodedEdgeFilter}, filter_operator::FilterOperator, property_filter::PropertyFilter, - AndFilter, Filter, FilterValue, NotFilter, OrFilter, PropertyFilterFactory, - TryAsCompositeFilter, + AndFilter, Filter, FilterValue, NotFilter, OrFilter, TryAsCompositeFilter, + Windowed, }, }, }, @@ -16,6 +16,7 @@ use crate::{ prelude::GraphViewOps, }; use raphtory_api::core::entities::{GidType, GID}; +use raphtory_core::utils::time::IntoTime; use std::{fmt, fmt::Display, ops::Deref, sync::Arc}; #[derive(Debug, Clone)] @@ -67,6 +68,7 @@ impl From for NodeTypeFilter { pub enum CompositeNodeFilter { Node(Filter), Property(PropertyFilter), + PropertyWindowed(PropertyFilter>), And(Box, Box), Or(Box, Box), Not(Box), @@ -76,6 +78,7 @@ impl Display for CompositeNodeFilter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CompositeNodeFilter::Property(filter) => write!(f, "{}", filter), + CompositeNodeFilter::PropertyWindowed(filter) => write!(f, "{}", filter), CompositeNodeFilter::Node(filter) => write!(f, "{}", filter), CompositeNodeFilter::And(left, right) => write!(f, "({} AND {})", left, right), CompositeNodeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), @@ -101,6 +104,7 @@ impl CreateFilter for CompositeNodeFilter { } }, CompositeNodeFilter::Property(i) => Ok(Arc::new(i.create_filter(graph)?)), + CompositeNodeFilter::PropertyWindowed(i) => Ok(Arc::new(i.create_filter(graph)?)), CompositeNodeFilter::And(l, r) => Ok(Arc::new( AndFilter { left: l.deref().clone(), @@ -292,7 +296,7 @@ impl InternalNodeFilterBuilderOps for NodeTypeFilterBuilder { } } -#[derive(Clone, Debug, Copy, PartialEq, Eq)] +#[derive(Clone, Debug, Default, Copy, PartialEq, Eq)] pub struct NodeFilter; impl NodeFilter { @@ -308,6 +312,10 @@ impl NodeFilter { NodeTypeFilterBuilder } + pub fn window(start: S, end: E) -> Windowed { + Windowed::from_times(start, end) + } + pub fn validate(id_dtype: Option, filter: &Filter) -> Result<(), GraphError> { use FilterOperator::*; use GidType::*; @@ -423,8 +431,6 @@ impl NodeFilter { } } -impl PropertyFilterFactory for NodeFilter {} - impl TryAsCompositeFilter for NodeIdFilter { fn try_as_composite_node_filter(&self) -> Result { Ok(CompositeNodeFilter::Node(self.0.clone())) diff --git a/raphtory/src/db/graph/views/filter/model/property_filter.rs b/raphtory/src/db/graph/views/filter/model/property_filter.rs index 43e4c5e292..8e2b413396 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter.rs @@ -18,12 +18,12 @@ use crate::{ }, filter_operator::FilterOperator, node_filter::{CompositeNodeFilter, NodeFilter}, - TryAsCompositeFilter, + TryAsCompositeFilter, Windowed, }, }, }, errors::GraphError, - prelude::{GraphViewOps, PropertiesOps, TimeOps}, + prelude::{GraphViewOps, PropertiesOps}, }; use itertools::Itertools; use raphtory_api::core::{ @@ -40,7 +40,7 @@ use raphtory_storage::graph::{ edges::{edge_ref::EdgeStorageRef, edge_storage_ops::EdgeStorageOps}, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; -use std::{collections::HashSet, fmt, fmt::Display, marker::PhantomData, ops::Deref, sync::Arc}; +use std::{collections::HashSet, fmt, fmt::Display, ops::Deref, sync::Arc}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Op { @@ -168,7 +168,7 @@ pub struct PropertyFilter { pub prop_value: PropertyFilterValue, pub operator: FilterOperator, pub ops: Vec, // validated by validate_chain_and_infer_effective_dtype - pub _phantom: PhantomData, + pub entity: M, } impl Display for PropertyFilter { @@ -223,164 +223,6 @@ impl PropertyFilter { self } - pub fn eq(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::Eq, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn ne(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::Ne, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn le(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::Le, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn ge(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::Ge, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn lt(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::Lt, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn gt(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::Gt, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn is_in(prop_ref: PropertyRef, prop_values: impl IntoIterator) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Set(Arc::new(prop_values.into_iter().collect())), - operator: FilterOperator::IsIn, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn is_not_in(prop_ref: PropertyRef, prop_values: impl IntoIterator) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Set(Arc::new(prop_values.into_iter().collect())), - operator: FilterOperator::IsNotIn, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn is_none(prop_ref: PropertyRef) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::None, - operator: FilterOperator::IsNone, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn is_some(prop_ref: PropertyRef) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::None, - operator: FilterOperator::IsSome, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn starts_with(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::StartsWith, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn ends_with(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::EndsWith, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn contains(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::Contains, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn not_contains(prop_ref: PropertyRef, prop_value: impl Into) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(prop_value.into()), - operator: FilterOperator::NotContains, - ops: vec![], - _phantom: PhantomData, - } - } - - pub fn fuzzy_search( - prop_ref: PropertyRef, - prop_value: impl Into, - levenshtein_distance: usize, - prefix_match: bool, - ) -> Self { - Self { - prop_ref, - prop_value: PropertyFilterValue::Single(Prop::Str(ArcStr::from(prop_value.into()))), - operator: FilterOperator::FuzzySearch { - levenshtein_distance, - prefix_match, - }, - ops: vec![], - _phantom: PhantomData, - } - } - #[inline] fn has_aggregator(&self) -> bool { self.ops.iter().copied().any(Op::is_aggregator) @@ -1258,7 +1100,6 @@ impl PropertyFilter { } Op::Any => { for p in &seq { - saw = true; let ok = if elem_quals.is_empty() { pred(p) } else { @@ -1328,8 +1169,8 @@ impl PropertyFilter { let Some(tview) = props.temporal().get_by_id(t_prop_id) else { return false; }; - let props: Vec = tview.values().collect(); - self.eval_temporal_and_apply(props) + let seq = tview.values().collect(); + self.eval_temporal_and_apply(seq) } } } @@ -1370,10 +1211,10 @@ impl PropertyFilter { PropertyRef::TemporalProperty(_) => { let props = node_view.properties(); let Some(tview) = props.temporal().get_by_id(prop_id) else { - return false; // no temporal values at all for this node/property + return false; }; - let seq: Vec = tview.values().collect(); + let seq = tview.values().collect(); if self.ops.is_empty() { return self.eval_temporal_and_apply(seq); @@ -1418,7 +1259,16 @@ impl PropertyFilter { let props = edge.metadata(); self.is_metadata_matched(prop_id, props) } - PropertyRef::TemporalProperty(_) | PropertyRef::Property(_) => { + PropertyRef::TemporalProperty(_) => { + let seq: Vec = edge + .properties() + .temporal() + .get_by_id(prop_id) + .map(|tv| tv.values().collect()) + .unwrap_or_default(); + self.eval_temporal_and_apply(seq) + } + PropertyRef::Property(_) => { let props = edge.properties(); self.is_property_matched(prop_id, props) } @@ -1503,16 +1353,24 @@ pub trait InternalPropertyFilterOps: Send + Sync { fn ops(&self) -> &[Op] { &[] } + + fn entity(&self) -> Self::Marker; } impl InternalPropertyFilterOps for Arc { type Marker = T::Marker; + fn property_ref(&self) -> PropertyRef { self.deref().property_ref() } + fn ops(&self) -> &[Op] { self.deref().ops() } + + fn entity(&self) -> Self::Marker { + self.deref().entity() + } } pub trait PropertyFilterOps: InternalPropertyFilterOps { @@ -1540,63 +1398,143 @@ pub trait PropertyFilterOps: InternalPropertyFilterOps { impl PropertyFilterOps for T { fn eq(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::eq(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::Eq, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn ne(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::ne(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::Ne, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn le(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::le(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::Le, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn ge(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::ge(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::Ge, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn lt(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::lt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::Lt, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn gt(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::gt(self.property_ref(), value.into()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::Gt, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn is_in(&self, values: impl IntoIterator) -> PropertyFilter { - PropertyFilter::is_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Set(Arc::new(values.into_iter().collect())), + operator: FilterOperator::IsIn, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn is_not_in(&self, values: impl IntoIterator) -> PropertyFilter { - PropertyFilter::is_not_in(self.property_ref(), values).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Set(Arc::new(values.into_iter().collect())), + operator: FilterOperator::IsNotIn, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn is_none(&self) -> PropertyFilter { - PropertyFilter::is_none(self.property_ref()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::None, + operator: FilterOperator::IsNone, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn is_some(&self) -> PropertyFilter { - PropertyFilter::is_some(self.property_ref()).with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::None, + operator: FilterOperator::IsSome, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn starts_with(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::starts_with(self.property_ref(), value.into()) - .with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::StartsWith, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn ends_with(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::ends_with(self.property_ref(), value.into()) - .with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::EndsWith, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn contains(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::contains(self.property_ref(), value.into()) - .with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::Contains, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn not_contains(&self, value: impl Into) -> PropertyFilter { - PropertyFilter::not_contains(self.property_ref(), value.into()) - .with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(value.into()), + operator: FilterOperator::NotContains, + ops: self.ops().to_vec(), + entity: self.entity(), + } } fn fuzzy_search( @@ -1605,22 +1543,25 @@ impl PropertyFilterOps for T { levenshtein_distance: usize, prefix_match: bool, ) -> PropertyFilter { - PropertyFilter::fuzzy_search( - self.property_ref(), - prop_value.into(), - levenshtein_distance, - prefix_match, - ) - .with_ops(self.ops().iter().copied()) + PropertyFilter { + prop_ref: self.property_ref(), + prop_value: PropertyFilterValue::Single(Prop::Str(ArcStr::from(prop_value.into()))), + operator: FilterOperator::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + ops: self.ops().to_vec(), + entity: self.entity(), + } } } #[derive(Clone)] -pub struct PropertyFilterBuilder(pub String, PhantomData); +pub struct PropertyFilterBuilder(pub String, pub M); impl PropertyFilterBuilder { - pub fn new(prop: impl Into) -> Self { - Self(prop.into(), PhantomData) + pub fn new(prop: impl Into, entity: M) -> Self { + Self(prop.into(), entity) } } @@ -1629,14 +1570,18 @@ impl InternalPropertyFilterOps for PropertyFil fn property_ref(&self) -> PropertyRef { PropertyRef::Property(self.0.clone()) } + + fn entity(&self) -> Self::Marker { + self.1.clone() + } } #[derive(Clone)] -pub struct MetadataFilterBuilder(pub String, PhantomData); +pub struct MetadataFilterBuilder(pub String, pub M); impl MetadataFilterBuilder { - pub fn new(prop: impl Into) -> Self { - Self(prop.into(), PhantomData) + pub fn new(prop: impl Into, entity: M) -> Self { + Self(prop.into(), entity) } } @@ -1645,13 +1590,17 @@ impl InternalPropertyFilterOps for MetadataFil fn property_ref(&self) -> PropertyRef { PropertyRef::Metadata(self.0.clone()) } + + fn entity(&self) -> Self::Marker { + self.1.clone() + } } #[derive(Clone)] pub struct OpChainBuilder { pub prop_ref: PropertyRef, pub ops: Vec, - pub _phantom: PhantomData, + pub entity: M, } impl OpChainBuilder { @@ -1668,27 +1617,35 @@ impl OpChainBuilder { pub fn first(self) -> Self { self.with_op(Op::First) } + pub fn last(self) -> Self { self.with_op(Op::Last) } + pub fn any(self) -> Self { self.with_op(Op::Any) } + pub fn all(self) -> Self { self.with_op(Op::All) } + pub fn len(self) -> Self { self.with_op(Op::Len) } + pub fn sum(self) -> Self { self.with_op(Op::Sum) } + pub fn avg(self) -> Self { self.with_op(Op::Avg) } + pub fn min(self) -> Self { self.with_op(Op::Min) } + pub fn max(self) -> Self { self.with_op(Op::Max) } @@ -1696,12 +1653,18 @@ impl OpChainBuilder { impl InternalPropertyFilterOps for OpChainBuilder { type Marker = M; + fn property_ref(&self) -> PropertyRef { self.prop_ref.clone() } + fn ops(&self) -> &[Op] { &self.ops } + + fn entity(&self) -> Self::Marker { + self.entity.clone() + } } pub trait ElemQualifierOps: InternalPropertyFilterOps { @@ -1712,7 +1675,7 @@ pub trait ElemQualifierOps: InternalPropertyFilterOps { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Any]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } @@ -1723,7 +1686,7 @@ pub trait ElemQualifierOps: InternalPropertyFilterOps { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::All]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } } @@ -1735,7 +1698,7 @@ impl PropertyFilterBuilder { OpChainBuilder { prop_ref: PropertyRef::TemporalProperty(self.0), ops: vec![], - _phantom: PhantomData, + entity: self.1, } } } @@ -1745,7 +1708,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Len]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } @@ -1753,7 +1716,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Sum]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } @@ -1761,7 +1724,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Avg]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } @@ -1769,7 +1732,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Min]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } @@ -1777,7 +1740,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Max]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } @@ -1785,7 +1748,7 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::First]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } @@ -1793,9 +1756,51 @@ pub trait ListAggOps: InternalPropertyFilterOps + Sized { OpChainBuilder { prop_ref: self.property_ref(), ops: self.ops().iter().copied().chain([Op::Last]).collect(), - _phantom: PhantomData, + entity: self.entity(), } } } impl ListAggOps for T {} + +impl TryAsCompositeFilter for PropertyFilter> { + fn try_as_composite_node_filter(&self) -> Result { + Ok(CompositeNodeFilter::PropertyWindowed(self.clone())) + } + fn try_as_composite_edge_filter(&self) -> Result { + Err(GraphError::NotSupported) + } + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Err(GraphError::NotSupported) + } +} + +impl TryAsCompositeFilter for PropertyFilter> { + fn try_as_composite_node_filter(&self) -> Result { + Err(GraphError::NotSupported) + } + fn try_as_composite_edge_filter(&self) -> Result { + Ok(CompositeEdgeFilter::PropertyWindowed(self.clone())) + } + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Err(GraphError::NotSupported) + } +} + +impl TryAsCompositeFilter for PropertyFilter> { + fn try_as_composite_node_filter(&self) -> Result { + Err(GraphError::NotSupported) + } + fn try_as_composite_edge_filter(&self) -> Result { + Err(GraphError::NotSupported) + } + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + Ok(CompositeExplodedEdgeFilter::PropertyWindowed(self.clone())) + } +} diff --git a/raphtory/src/db/graph/views/filter/node_property_filtered_graph.rs b/raphtory/src/db/graph/views/filter/node_property_filtered_graph.rs index 0294ff2aaa..e5a901bb08 100644 --- a/raphtory/src/db/graph/views/filter/node_property_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/node_property_filtered_graph.rs @@ -8,15 +8,21 @@ use crate::{ InheritTimeSemantics, InternalNodeFilterOps, Static, }, }, - graph::views::filter::{ - internal::CreateFilter, - model::{node_filter::NodeFilter, property_filter::PropertyFilter}, + graph::views::{ + filter::{ + internal::CreateFilter, + model::{node_filter::NodeFilter, property_filter::PropertyFilter, Windowed}, + }, + window_graph::WindowedGraph, }, }, errors::GraphError, - prelude::GraphViewOps, + prelude::{GraphViewOps, TimeOps}, +}; +use raphtory_api::{ + core::{entities::LayerIds, storage::timeindex::AsTime}, + inherit::Base, }; -use raphtory_api::{core::entities::LayerIds, inherit::Base}; use raphtory_storage::{core_ops::InheritCoreGraphOps, graph::nodes::node_ref::NodeStorageRef}; #[derive(Debug, Clone)] @@ -36,6 +42,30 @@ impl NodePropertyFilteredGraph { } } +impl CreateFilter for PropertyFilter> { + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodePropertyFilteredGraph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let prop_id = self.resolve_prop_id(graph.node_meta(), false)?; + let filter = PropertyFilter { + prop_ref: self.prop_ref, + prop_value: self.prop_value, + operator: self.operator, + ops: self.ops, + entity: NodeFilter, + }; + Ok(NodePropertyFilteredGraph::new( + graph.window(self.entity.start.t(), self.entity.end.t()), + prop_id, + filter, + )) + } +} + impl CreateFilter for PropertyFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodePropertyFilteredGraph; @@ -120,7 +150,7 @@ mod test_node_property_filtered_graph { let filter_expr = NodeFilter::name() .eq("John") - .and(NodeFilter::property("band").eq("Dead & Company")); + .and(NodeFilter.property("band").eq("Dead & Company")); let filtered_nodes = g.nodes().filter(filter_expr).unwrap(); // filter_nodes doesn't filter the iterator, it only filters the view of the nodes which includes history, edges, etc. @@ -169,7 +199,7 @@ mod test_node_property_filtered_graph { let n1 = g.node(1).unwrap(); assert_eq!( - n1.filter(NodeFilter::property("test").eq(1i64)) + n1.filter(NodeFilter.property("test").eq(1i64)) .unwrap() .edges() .id() @@ -177,7 +207,7 @@ mod test_node_property_filtered_graph { vec![] ); assert_eq!( - n1.filter(NodeFilter::property("test").eq(2i64)) + n1.filter(NodeFilter.property("test").eq(2i64)) .unwrap() .out_neighbours() .id() @@ -188,7 +218,7 @@ mod test_node_property_filtered_graph { let n2 = g.node(2).unwrap(); assert_eq!( - n2.filter(NodeFilter::property("test").gt(1i64)) + n2.filter(NodeFilter.property("test").gt(1i64)) .unwrap() .neighbours() .id() @@ -197,7 +227,7 @@ mod test_node_property_filtered_graph { ); assert_eq!( - n2.filter(NodeFilter::property("test").gt(0i64)) + n2.filter(NodeFilter.property("test").gt(0i64)) .unwrap() .neighbours() .id() @@ -209,7 +239,7 @@ mod test_node_property_filtered_graph { let n1p = gp.node(1).unwrap(); assert_eq!( - n1p.filter(NodeFilter::property("test").eq(1i64)) + n1p.filter(NodeFilter.property("test").eq(1i64)) .unwrap() .edges() .id() @@ -217,7 +247,7 @@ mod test_node_property_filtered_graph { vec![] ); assert_eq!( - n1p.filter(NodeFilter::property("test").eq(2i64)) + n1p.filter(NodeFilter.property("test").eq(2i64)) .unwrap() .out_neighbours() .id() @@ -228,7 +258,7 @@ mod test_node_property_filtered_graph { let n2p = gp.node(2).unwrap(); assert_eq!( - n2p.filter(NodeFilter::property("test").gt(1i64)) + n2p.filter(NodeFilter.property("test").gt(1i64)) .unwrap() .neighbours() .id() @@ -237,7 +267,7 @@ mod test_node_property_filtered_graph { ); assert_eq!( - n2p.filter(NodeFilter::property("test").gt(0i64)) + n2p.filter(NodeFilter.property("test").gt(0i64)) .unwrap() .neighbours() .id() @@ -259,7 +289,7 @@ mod test_node_property_filtered_graph { let filtered_nodes = g .nodes() - .filter_iter(NodeFilter::property("test").gt(1i64)) + .select(NodeFilter.property("test").gt(1i64)) .unwrap(); assert_eq!( filtered_nodes @@ -282,7 +312,7 @@ mod test_node_property_filtered_graph { let filtered_nodes_p = g .persistent_graph() .nodes() - .filter_iter(NodeFilter::property("test").gt(1i64)) + .select(NodeFilter.property("test").gt(1i64)) .unwrap(); assert_eq!( filtered_nodes_p @@ -305,32 +335,32 @@ mod test_node_property_filtered_graph { g.add_edge(1, 2, 1, NO_PROPS, None).unwrap(); g.add_edge(1, 1, 3, NO_PROPS, None).unwrap(); - let gf = g.filter(NodeFilter::property("test").eq(1i64)).unwrap(); + let gf = g.filter(NodeFilter.property("test").eq(1i64)).unwrap(); assert_eq!(gf.edges().id().collect_vec(), vec![]); - let gf = g.filter(NodeFilter::property("test").gt(1i64)).unwrap(); + let gf = g.filter(NodeFilter.property("test").gt(1i64)).unwrap(); assert_eq!( gf.edges().id().collect_vec(), vec![(GID::U64(2), GID::U64(3))] ); - let gf = g.filter(NodeFilter::property("test").lt(3i64)).unwrap(); + let gf = g.filter(NodeFilter.property("test").lt(3i64)).unwrap(); assert_eq!( gf.edges().id().collect_vec(), vec![(GID::U64(1), GID::U64(2)), (GID::U64(2), GID::U64(1))] ); let gp = g.persistent_graph(); - let gf = gp.filter(NodeFilter::property("test").eq(1i64)).unwrap(); + let gf = gp.filter(NodeFilter.property("test").eq(1i64)).unwrap(); assert_eq!(gf.edges().id().collect_vec(), vec![]); - let gf = gp.filter(NodeFilter::property("test").gt(1i64)).unwrap(); + let gf = gp.filter(NodeFilter.property("test").gt(1i64)).unwrap(); assert_eq!( gf.edges().id().collect_vec(), vec![(GID::U64(2), GID::U64(3))] ); - let gf = gp.filter(NodeFilter::property("test").lt(3i64)).unwrap(); + let gf = gp.filter(NodeFilter.property("test").lt(3i64)).unwrap(); assert_eq!( gf.edges().id().collect_vec(), vec![(GID::U64(1), GID::U64(2)), (GID::U64(2), GID::U64(1))] @@ -344,7 +374,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").gt(v); + let filter = NodeFilter.property("int_prop").gt(v); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.filter(|&vv| *vv > v).is_some() }); @@ -367,7 +397,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").ge(v); + let filter = NodeFilter.property("int_prop").ge(v); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.filter(|&vv| *vv >= v ).is_some() }); @@ -389,7 +419,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").lt(v); + let filter = NodeFilter.property("int_prop").lt(v); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.filter(|&vv| *vv < v ).is_some() }); @@ -411,7 +441,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").le(v); + let filter = NodeFilter.property("int_prop").le(v); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.filter(|&vv| *vv <= v ).is_some() }); @@ -433,7 +463,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").eq(v); + let filter = NodeFilter.property("int_prop").eq(v); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.filter(|&vv| *vv == v ).is_some() }); @@ -455,7 +485,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").ne(v); + let filter = NodeFilter.property("int_prop").ne(v); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.filter(|&vv| *vv != v ).is_some() }); @@ -477,7 +507,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").is_some(); + let filter = NodeFilter.property("int_prop").is_some(); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.is_some() }); @@ -499,7 +529,7 @@ mod test_node_property_filtered_graph { )| { let g = build_graph_from_edge_list(&edges); add_node_props(&g, &nodes); - let filter = NodeFilter::property("int_prop").is_none(); + let filter = NodeFilter.property("int_prop").is_none(); let expected_g = node_filtered_graph(&edges, &nodes, |_, int_v| { int_v.is_none() }); @@ -532,7 +562,7 @@ mod test_node_property_filtered_graph { assert_eq!(graph.count_nodes(), 3); - let filtered = graph.filter(NodeFilter::property("p2").is_none()).unwrap(); + let filtered = graph.filter(NodeFilter.property("p2").is_none()).unwrap(); let ids = filtered.nodes().name().collect_vec(); assert_eq!(ids, vec!["2"]); diff --git a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs index e6778235d5..09d78fcd00 100644 --- a/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs +++ b/raphtory/src/db/graph/views/filter/node_type_filtered_graph.rs @@ -157,14 +157,14 @@ mod tests_node_type_filtered_subgraph { assert_eq!( type_filtered_subgraph .clone() - .filter(NodeFilter::property("p1").eq(1u64)) + .filter(NodeFilter.property("p1").eq(1u64)) .unwrap() .nodes(), vec!["C", "D"] ); assert!(type_filtered_subgraph - .filter(EdgeFilter::property("p1").eq(1u64)) + .filter(EdgeFilter.property("p1").eq(1u64)) .unwrap() .edges() .is_empty()) @@ -348,7 +348,7 @@ mod tests_node_type_filtered_subgraph { #[test] fn test_nodes_filters() { - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); // NodePropertyFilter let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( @@ -368,7 +368,7 @@ mod tests_node_type_filtered_subgraph { let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N4", "N7"]; assert_filter_nodes_results( init_graph, @@ -389,7 +389,7 @@ mod tests_node_type_filtered_subgraph { #[test] fn test_nodes_filters_w() { // TODO: Enable event_disk_graph for filter_nodes once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -408,7 +408,7 @@ mod tests_node_type_filtered_subgraph { let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3"]; assert_filter_nodes_results( init_graph, @@ -428,7 +428,7 @@ mod tests_node_type_filtered_subgraph { #[test] fn test_nodes_filters_pg_w() { - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -447,7 +447,7 @@ mod tests_node_type_filtered_subgraph { let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N7"]; assert_filter_nodes_results( init_graph, @@ -587,7 +587,7 @@ mod tests_node_type_filtered_subgraph { #[test] fn test_edges_filters() { - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -606,7 +606,7 @@ mod tests_node_type_filtered_subgraph { let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5"]; assert_filter_edges_results( init_graph, @@ -643,7 +643,7 @@ mod tests_node_type_filtered_subgraph { #[test] fn test_edges_filters_w() { - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -662,7 +662,7 @@ mod tests_node_type_filtered_subgraph { let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4"]; assert_filter_edges_results( init_graph, @@ -703,7 +703,7 @@ mod tests_node_type_filtered_subgraph { #[test] fn test_edges_filters_pg_w() { - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7", "N7->N8"]; let graph = init_graph(Graph::new()).persistent_graph(); let edge = graph.edge("N8", "N1").unwrap(); @@ -737,7 +737,7 @@ mod tests_node_type_filtered_subgraph { let node_types: Option> = Some(vec!["air_nomad".into(), "water_tribe".into()]); - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4"]; assert_filter_edges_results( init_graph, diff --git a/raphtory/src/db/graph/views/layer_graph.rs b/raphtory/src/db/graph/views/layer_graph.rs index e96dd4af1b..bbea21ac96 100644 --- a/raphtory/src/db/graph/views/layer_graph.rs +++ b/raphtory/src/db/graph/views/layer_graph.rs @@ -338,7 +338,7 @@ mod test_layers { #[test] fn test_nodes_filters() { let layers: Vec = vec!["layer1".into(), "layer2".into()]; - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -356,7 +356,7 @@ mod test_layers { ); let layers: Vec = vec!["layer1".into()]; - let filter = NodeFilter::property("p1").ge(2u64); + let filter = NodeFilter.property("p1").ge(2u64); let expected_results = vec!["N2", "N5", "N8"]; assert_filter_nodes_results( init_graph, @@ -374,7 +374,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = NodeFilter::property("p1").le(1u64); + let filter = NodeFilter.property("p1").le(1u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -392,7 +392,7 @@ mod test_layers { ); let layers: Vec = vec!["layer1".into()]; - let filter = NodeFilter::property("p1").lt(2u64); + let filter = NodeFilter.property("p1").lt(2u64); let expected_results = vec!["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -410,7 +410,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = NodeFilter::property("p1").gt(1u64); + let filter = NodeFilter.property("p1").gt(1u64); let expected_results = vec!["N2", "N5", "N8"]; assert_filter_nodes_results( init_graph, @@ -432,7 +432,7 @@ mod test_layers { fn test_nodes_filters_w() { // TODO: Enable event_disk_graph for filter_nodes once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 let layers: Vec = vec!["layer1".into(), "layer2".into()]; - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -450,7 +450,7 @@ mod test_layers { ); let layers: Vec = vec!["layer1".into()]; - let filter = NodeFilter::property("p1").ge(2u64); + let filter = NodeFilter.property("p1").ge(2u64); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -468,7 +468,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = NodeFilter::property("p1").lt(2u64); + let filter = NodeFilter.property("p1").lt(2u64); let expected_results = vec!["N1", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -489,7 +489,7 @@ mod test_layers { #[test] fn test_nodes_filters_pg_w() { let layers: Vec = vec!["layer1".into(), "layer2".into()]; - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -507,7 +507,7 @@ mod test_layers { ); let layers: Vec = vec!["layer1".into()]; - let filter = NodeFilter::property("p1").lt(2u64); + let filter = NodeFilter.property("p1").lt(2u64); let expected_results = vec!["N1", "N3", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -525,7 +525,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = NodeFilter::property("p1").gt(1u64); + let filter = NodeFilter.property("p1").gt(1u64); let expected_results = vec!["N2", "N5", "N8"]; assert_filter_nodes_results( init_graph, @@ -599,7 +599,7 @@ mod test_layers { fn test_edges_filters() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph. let layers: Vec = vec!["layer1".into(), "layer2".into()]; - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -617,7 +617,7 @@ mod test_layers { ); let layers: Vec = vec!["layer1".into()]; - let filter = EdgeFilter::property("p1").le(1u64); + let filter = EdgeFilter.property("p1").le(1u64); let expected_results = vec![ "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N1", ]; @@ -637,7 +637,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = EdgeFilter::property("p1").ge(2u64); + let filter = EdgeFilter.property("p1").ge(2u64); let expected_results = vec!["N2->N3", "N5->N6", "N8->N1"]; assert_filter_edges_results( init_graph, @@ -655,7 +655,7 @@ mod test_layers { ); let layers: Vec = vec!["layer1".into()]; - let filter = EdgeFilter::property("p1").lt(2u64); + let filter = EdgeFilter.property("p1").lt(2u64); let expected_results = vec![ "N2->N3", "N3->N4", "N4->N5", "N5->N6", "N6->N7", "N7->N8", "N8->N1", ]; @@ -675,7 +675,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = EdgeFilter::property("p1").gt(1u64); + let filter = EdgeFilter.property("p1").gt(1u64); let expected_results = vec!["N2->N3", "N5->N6", "N8->N1"]; assert_filter_edges_results( init_graph, @@ -700,7 +700,7 @@ mod test_layers { // 2. However, when asked for a value of a particular property for an edge, the latest update // across all specified layers (or all layers if no layers specified) is returned! let layers: Vec = vec!["layer1".into(), "layer2".into()]; - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -721,7 +721,7 @@ mod test_layers { // When filtering by specific layer, filter criteria (p1==1) and latest semantics is applicable // only to that specific layer. let layers: Vec = vec!["layer1".into()]; - let filter = EdgeFilter::property("p1").lt(2u64); + let filter = EdgeFilter.property("p1").lt(2u64); let expected_results = vec!["N2->N3", "N3->N4"]; assert_filter_edges_results( init_graph, @@ -739,7 +739,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = EdgeFilter::property("p1").gt(1u64); + let filter = EdgeFilter.property("p1").gt(1u64); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -761,7 +761,7 @@ mod test_layers { fn test_edges_filters_pg_w() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph. let layers: Vec = vec!["layer1".into(), "layer2".into()]; - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); // Why is the edge N8 -> N1 included in the results? // The reason edge N8 -> N1 is included as part of the results because of following two semantic reasons: @@ -790,7 +790,7 @@ mod test_layers { ); let layers: Vec = vec!["layer1".into()]; - let filter = EdgeFilter::property("p1").le(1u64); + let filter = EdgeFilter.property("p1").le(1u64); let expected_results = vec!["N2->N3", "N3->N4", "N5->N6", "N6->N7", "N7->N8", "N8->N1"]; assert_filter_edges_results( @@ -809,7 +809,7 @@ mod test_layers { ); let layers: Vec = vec!["layer2".into()]; - let filter = EdgeFilter::property("p1").ge(2u64); + let filter = EdgeFilter.property("p1").ge(2u64); let expected_results = vec!["N2->N3", "N5->N6", "N8->N1"]; assert_filter_edges_results( init_graph, diff --git a/raphtory/src/db/graph/views/node_subgraph.rs b/raphtory/src/db/graph/views/node_subgraph.rs index 3c0a454bbb..af4685d747 100644 --- a/raphtory/src/db/graph/views/node_subgraph.rs +++ b/raphtory/src/db/graph/views/node_subgraph.rs @@ -422,7 +422,7 @@ mod subgraph_tests { #[test] fn test_search_nodes_subgraph() { - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = ["N1", "N3", "N4", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -441,7 +441,7 @@ mod subgraph_tests { let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); - let filter = NodeFilter::property("p1").le(1u64); + let filter = NodeFilter.property("p1").le(1u64); let expected_results = vec!["N3", "N4"]; assert_filter_nodes_results( init_graph, @@ -462,7 +462,7 @@ mod subgraph_tests { #[test] fn test_search_nodes_subgraph_w() { // TODO: Enable event_disk_graph for filter_nodes once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -480,7 +480,7 @@ mod subgraph_tests { ); let node_names: Option> = Some(vec!["N3".into()]); - let filter = NodeFilter::property("p1").gt(0u64); + let filter = NodeFilter.property("p1").gt(0u64); let expected_results = vec!["N3"]; assert_filter_nodes_results( init_graph, @@ -500,7 +500,7 @@ mod subgraph_tests { #[test] fn test_search_nodes_pg_w() { - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -519,7 +519,7 @@ mod subgraph_tests { let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); - let filter = NodeFilter::property("p1").ge(1u64); + let filter = NodeFilter.property("p1").ge(1u64); let expected_results = vec!["N2", "N3", "N5"]; assert_filter_nodes_results( init_graph, @@ -589,7 +589,7 @@ mod subgraph_tests { #[test] fn test_edges_filters() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph. - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N4->N5", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -608,7 +608,7 @@ mod subgraph_tests { let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); - let filter = EdgeFilter::property("p1").le(1u64); + let filter = EdgeFilter.property("p1").le(1u64); let expected_results = vec!["N3->N4", "N4->N5"]; assert_filter_edges_results( init_graph, @@ -628,7 +628,7 @@ mod subgraph_tests { #[test] fn test_edges_filters_w() { - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -647,7 +647,7 @@ mod subgraph_tests { let node_names: Option> = Some(vec!["N2".into(), "N3".into(), "N4".into(), "N5".into()]); - let filter = EdgeFilter::property("p1").ge(1u64); + let filter = EdgeFilter.property("p1").ge(1u64); let expected_results = vec!["N2->N3", "N3->N4"]; assert_filter_edges_results( init_graph, @@ -668,7 +668,7 @@ mod subgraph_tests { #[test] fn test_edges_filters_pg_w() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph. - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -692,7 +692,7 @@ mod subgraph_tests { "N5".into(), "N6".into(), ]); - let filter = EdgeFilter::property("p1").lt(2u64); + let filter = EdgeFilter.property("p1").lt(2u64); let expected_results = vec!["N3->N4"]; assert_filter_edges_results( init_graph, diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs index 3f7c083687..3ddc227fc8 100644 --- a/raphtory/src/db/graph/views/window_graph.rs +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -1785,7 +1785,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_eq() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -1802,7 +1802,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").eq(2i64); + let filter = NodeFilter.property("k1").eq(2i64); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -1819,7 +1819,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k2").eq("Paper_Airplane"); + let filter = NodeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1836,7 +1836,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = NodeFilter::property("k3").eq(true); + let filter = NodeFilter.property("k3").eq(true); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -1853,7 +1853,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = NodeFilter::property("k4").eq(6.0f64); + let filter = NodeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1870,7 +1870,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = NodeFilter::property("x").eq(Prop::List(Arc::new(vec![ + let filter = NodeFilter.property("x").eq(Prop::List(Arc::new(vec![ Prop::U64(1), Prop::U64(6), Prop::U64(9), @@ -1903,7 +1903,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_eq() { - let filter = NodeFilter::property("p1").eq(1u64); + let filter = NodeFilter.property("p1").eq(1u64); let expected_results = vec!["N1", "N3", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -1920,7 +1920,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").eq(2i64); + let filter = NodeFilter.property("k1").eq(2i64); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1937,7 +1937,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k2").eq("Paper_Airplane"); + let filter = NodeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1955,7 +1955,7 @@ mod views_test { ); // TODO: Const properties not supported for disk_graph. - let filter = NodeFilter::property("k3").eq(true); + let filter = NodeFilter.property("k3").eq(true); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -1972,7 +1972,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").eq(6.0f64); + let filter = NodeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -1989,7 +1989,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("x").eq(Prop::List(Arc::new(vec![ + let filter = NodeFilter.property("x").eq(Prop::List(Arc::new(vec![ Prop::U64(1), Prop::U64(6), Prop::U64(9), @@ -2023,7 +2023,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_ne() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").ne(1u64); + let filter = NodeFilter.property("p1").ne(1u64); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -2040,7 +2040,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").ne(2i64); + let filter = NodeFilter.property("k1").ne(2i64); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2057,7 +2057,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k2").ne("Paper_Airplane"); + let filter = NodeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -2074,7 +2074,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k3").ne(true); + let filter = NodeFilter.property("k3").ne(true); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2091,7 +2091,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k4").ne(6.0f64); + let filter = NodeFilter.property("k4").ne(6.0f64); let expected_results = vec!["N2", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2111,7 +2111,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_ne() { - let filter = NodeFilter::property("p1").ne(1u64); + let filter = NodeFilter.property("p1").ne(1u64); let expected_results = vec!["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"]; assert_filter_nodes_results( init_graph, @@ -2128,7 +2128,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").ne(2i64); + let filter = NodeFilter.property("k1").ne(2i64); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2145,7 +2145,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k2").ne("Paper_Airplane"); + let filter = NodeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2162,7 +2162,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k3").ne(true); + let filter = NodeFilter.property("k3").ne(true); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2179,7 +2179,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").ne(6.0f64); + let filter = NodeFilter.property("k4").ne(6.0f64); let expected_results = vec!["N12", "N2", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2200,7 +2200,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_lt() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").lt(3u64); + let filter = NodeFilter.property("p1").lt(3u64); let expected_results = vec!["N1", "N2", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2217,7 +2217,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").lt(3i64); + let filter = NodeFilter.property("k1").lt(3i64); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -2234,7 +2234,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k4").lt(10.0f64); + let filter = NodeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2254,7 +2254,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_lt() { - let filter = NodeFilter::property("p1").lt(3u64); + let filter = NodeFilter.property("p1").lt(3u64); let expected_results = vec!["N1", "N2", "N3", "N5", "N6", "N7", "N8", "N9"]; assert_filter_nodes_results( init_graph, @@ -2271,7 +2271,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").lt(3i64); + let filter = NodeFilter.property("k1").lt(3i64); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2288,7 +2288,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").lt(10.0f64); + let filter = NodeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2309,7 +2309,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_le() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").le(1u64); + let filter = NodeFilter.property("p1").le(1u64); let expected_results = vec!["N1", "N3", "N6"]; assert_filter_nodes_results( init_graph, @@ -2326,7 +2326,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").le(2i64); + let filter = NodeFilter.property("k1").le(2i64); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -2343,7 +2343,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k4").le(6.0f64); + let filter = NodeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2363,7 +2363,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_le() { - let filter = NodeFilter::property("p1").le(1u64); + let filter = NodeFilter.property("p1").le(1u64); let expected_results = vec!["N1", "N3", "N6", "N7"]; assert_filter_nodes_results( init_graph, @@ -2380,7 +2380,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").le(2i64); + let filter = NodeFilter.property("k1").le(2i64); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2397,7 +2397,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").le(6.0f64); + let filter = NodeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2418,7 +2418,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_gt() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").gt(1u64); + let filter = NodeFilter.property("p1").gt(1u64); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -2435,7 +2435,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").gt(2i64); + let filter = NodeFilter.property("k1").gt(2i64); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2452,7 +2452,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k4").gt(6.0f64); + let filter = NodeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -2469,7 +2469,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("x").gt(Prop::List(Arc::new(vec![ + let filter = NodeFilter.property("x").gt(Prop::List(Arc::new(vec![ Prop::U64(1), Prop::U64(6), Prop::U64(9), @@ -2487,7 +2487,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_gt() { - let filter = NodeFilter::property("p1").gt(1u64); + let filter = NodeFilter.property("p1").gt(1u64); let expected_results = vec!["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"]; assert_filter_nodes_results( init_graph, @@ -2504,7 +2504,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").gt(2i64); + let filter = NodeFilter.property("k1").gt(2i64); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2521,7 +2521,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").gt(6.0f64); + let filter = NodeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N12", "N2", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2542,7 +2542,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_ge() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").ge(1u64); + let filter = NodeFilter.property("p1").ge(1u64); let expected_results = vec!["N1", "N2", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2559,7 +2559,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").ge(2i64); + let filter = NodeFilter.property("k1").ge(2i64); let expected_results = vec!["N1", "N2"]; assert_filter_nodes_results( init_graph, @@ -2576,7 +2576,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k4").ge(6.0f64); + let filter = NodeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1", "N2"]; assert_filter_nodes_results( init_graph, @@ -2596,7 +2596,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_ge() { - let filter = NodeFilter::property("p1").ge(1u64); + let filter = NodeFilter.property("p1").ge(1u64); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N2", "N3", "N5", "N6", "N7", "N8", "N9", ]; @@ -2615,7 +2615,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").ge(2i64); + let filter = NodeFilter.property("k1").ge(2i64); let expected_results = vec!["N1", "N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2632,7 +2632,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").ge(6.0f64); + let filter = NodeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1", "N12", "N2", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2653,7 +2653,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_in() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").is_in(vec![2u64.into()]); + let filter = NodeFilter.property("p1").is_in(vec![2u64.into()]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -2670,7 +2670,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").is_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -2687,7 +2687,9 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k2").is_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter + .property("k2") + .is_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2704,7 +2706,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k3").is_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_in(vec![true.into()]); let expected_results = vec!["N2"]; assert_filter_nodes_results( init_graph, @@ -2721,7 +2723,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k4").is_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2741,7 +2743,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_in() { - let filter = NodeFilter::property("p1").is_in(vec![2u64.into()]); + let filter = NodeFilter.property("p1").is_in(vec![2u64.into()]); let expected_results = vec!["N2", "N5", "N8", "N9"]; assert_filter_nodes_results( init_graph, @@ -2758,7 +2760,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").is_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2775,7 +2777,9 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k2").is_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter + .property("k2") + .is_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2793,7 +2797,7 @@ mod views_test { ); // TODO: Const properties not supported for disk_graph. - let filter = NodeFilter::property("k3").is_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_in(vec![true.into()]); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2810,7 +2814,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").is_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2831,7 +2835,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_not_in() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").is_not_in(vec![1u64.into()]); + let filter = NodeFilter.property("p1").is_not_in(vec![1u64.into()]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -2848,7 +2852,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k1").is_not_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2865,7 +2869,9 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k2").is_not_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter + .property("k2") + .is_not_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N2", "N5"]; assert_filter_nodes_results( init_graph, @@ -2882,7 +2888,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k3").is_not_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2899,7 +2905,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k4").is_not_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = vec!["N2", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -2919,7 +2925,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_not_in() { - let filter = NodeFilter::property("p1").is_not_in(vec![1u64.into()]); + let filter = NodeFilter.property("p1").is_not_in(vec![1u64.into()]); let expected_results = vec!["N10", "N11", "N12", "N13", "N2", "N5", "N8", "N9"]; assert_filter_nodes_results( init_graph, @@ -2936,7 +2942,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k1").is_not_in(vec![2i64.into()]); + let filter = NodeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2953,7 +2959,9 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k2").is_not_in(vec!["Paper_Airplane".into()]); + let filter = NodeFilter + .property("k2") + .is_not_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N12", "N2", "N5", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -2970,7 +2978,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k3").is_not_in(vec![true.into()]); + let filter = NodeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -2987,7 +2995,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k4").is_not_in(vec![6.0f64.into()]); + let filter = NodeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = vec!["N12", "N2", "N5", "N6", "N7", "N8"]; assert_filter_nodes_results( init_graph, @@ -3008,7 +3016,7 @@ mod views_test { #[test] fn test_nodes_filters_for_property_is_some() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("p1").is_some(); + let filter = NodeFilter.property("p1").is_some(); let expected_results = vec!["N1", "N2", "N3", "N5", "N6"]; assert_filter_nodes_results( init_graph, @@ -3059,7 +3067,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_property_is_some() { - let filter = NodeFilter::property("p1").is_some(); + let filter = NodeFilter.property("p1").is_some(); let expected_results = vec![ "N1", "N10", "N11", "N12", "N13", "N2", "N3", "N5", "N6", "N7", "N8", "N9", ]; @@ -3116,9 +3124,10 @@ mod views_test { #[test] fn test_nodes_filters_for_props_added_at_different_times() { - let filter = NodeFilter::property("q1") + let filter = NodeFilter + .property("q1") .eq(0u64) - .and(NodeFilter::property("p1").eq(3u64)); + .and(NodeFilter.property("p1").eq(3u64)); let expected_results = vec!["N10", "N11", "N12", "N13"]; assert_filter_nodes_results( init_graph, @@ -3138,9 +3147,10 @@ mod views_test { #[test] fn test_nodes_filters_pg_for_props_added_at_different_times() { - let filter = NodeFilter::property("q1") + let filter = NodeFilter + .property("q1") .eq(0u64) - .and(NodeFilter::property("p1").eq(3u64)); + .and(NodeFilter.property("p1").eq(3u64)); let expected_results = vec!["N10", "N11", "N12", "N13"]; assert_filter_nodes_results( init_graph, @@ -3161,7 +3171,9 @@ mod views_test { #[test] fn test_nodes_filters_fuzzy_search() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("k2").fuzzy_search("Paper_Airpla", 2, false); + let filter = NodeFilter + .property("k2") + .fuzzy_search("Paper_Airpla", 2, false); let expected_results = vec!["N1"]; assert_filter_nodes_results( init_graph, @@ -3181,7 +3193,9 @@ mod views_test { #[test] fn test_nodes_filters_pg_fuzzy_search() { - let filter = NodeFilter::property("k2").fuzzy_search("Paper_Air", 5, false); + let filter = NodeFilter + .property("k2") + .fuzzy_search("Paper_Air", 5, false); let expected_results = vec!["N1", "N2", "N7"]; assert_filter_nodes_results( init_graph, @@ -3202,7 +3216,7 @@ mod views_test { #[test] fn test_nodes_filters_fuzzy_search_prefix_match() { // TODO: Enable event_disk_graph once bug fixed: https://github.com/Pometry/Raphtory/issues/2098 - let filter = NodeFilter::property("k2").fuzzy_search("Pa", 2, true); + let filter = NodeFilter.property("k2").fuzzy_search("Pa", 2, true); let expected_results = vec!["N1", "N2"]; assert_filter_nodes_results( init_graph, @@ -3219,7 +3233,7 @@ mod views_test { vec![TestGraphVariants::Graph], ); - let filter = NodeFilter::property("k2").fuzzy_search("Pa", 2, false); + let filter = NodeFilter.property("k2").fuzzy_search("Pa", 2, false); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( init_graph, @@ -3239,7 +3253,7 @@ mod views_test { #[test] fn test_nodes_filters_pg_fuzzy_search_prefix_match() { - let filter = NodeFilter::property("k2").fuzzy_search("Pa", 2, true); + let filter = NodeFilter.property("k2").fuzzy_search("Pa", 2, true); let expected_results = vec!["N1", "N2", "N7"]; assert_filter_nodes_results( init_graph, @@ -3256,7 +3270,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = NodeFilter::property("k2").fuzzy_search("Pa", 2, false); + let filter = NodeFilter.property("k2").fuzzy_search("Pa", 2, false); let expected_results = Vec::<&str>::new(); assert_filter_nodes_results( init_graph, @@ -3751,7 +3765,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_eq() { - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -3768,7 +3782,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").eq(2i64); + let filter = EdgeFilter.property("k1").eq(2i64); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -3785,7 +3799,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k2").eq("Paper_Airplane"); + let filter = EdgeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -3802,7 +3816,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k3").eq(true); + let filter = EdgeFilter.property("k3").eq(true); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -3819,7 +3833,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").eq(6.0f64); + let filter = EdgeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -3836,7 +3850,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("x").eq(Prop::List(Arc::new(vec![ + let filter = EdgeFilter.property("x").eq(Prop::List(Arc::new(vec![ Prop::U64(1), Prop::U64(6), Prop::U64(9), @@ -3871,7 +3885,7 @@ mod views_test { fn test_edges_filters_pg_for_property_eq() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").eq(1u64); + let filter = EdgeFilter.property("p1").eq(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -3888,7 +3902,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").eq(2i64); + let filter = EdgeFilter.property("k1").eq(2i64); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( @@ -3906,7 +3920,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k2").eq("Paper_Airplane"); + let filter = EdgeFilter.property("k2").eq("Paper_Airplane"); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -3923,7 +3937,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k3").eq(true); + let filter = EdgeFilter.property("k3").eq(true); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -3940,7 +3954,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").eq(6.0f64); + let filter = EdgeFilter.property("k4").eq(6.0f64); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -3957,7 +3971,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("x").eq(Prop::List(Arc::new(vec![ + let filter = EdgeFilter.property("x").eq(Prop::List(Arc::new(vec![ Prop::U64(1), Prop::U64(6), Prop::U64(9), @@ -3990,7 +4004,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_ne() { - let filter = EdgeFilter::property("p1").ne(1u64); + let filter = EdgeFilter.property("p1").ne(1u64); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -4007,7 +4021,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").ne(2i64); + let filter = EdgeFilter.property("k1").ne(2i64); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4024,7 +4038,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k2").ne("Paper_Airplane"); + let filter = EdgeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -4041,7 +4055,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k3").ne(true); + let filter = EdgeFilter.property("k3").ne(true); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4058,7 +4072,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").ne(6.0f64); + let filter = EdgeFilter.property("k4").ne(6.0f64); let expected_results = vec!["N2->N3", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4079,7 +4093,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_ne() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").ne(1u64); + let filter = EdgeFilter.property("p1").ne(1u64); let expected_results = vec![ "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N8->N9", "N9->N10", @@ -4099,7 +4113,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").ne(2i64); + let filter = EdgeFilter.property("k1").ne(2i64); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4116,7 +4130,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k2").ne("Paper_Airplane"); + let filter = EdgeFilter.property("k2").ne("Paper_Airplane"); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4133,7 +4147,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k3").ne(true); + let filter = EdgeFilter.property("k3").ne(true); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4150,7 +4164,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").ne(6.0f64); + let filter = EdgeFilter.property("k4").ne(6.0f64); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N6->N7", "N7->N8", "N8->N9"]; assert_filter_edges_results( @@ -4168,7 +4182,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("x").ne(Prop::List(Arc::new(vec![ + let filter = EdgeFilter.property("x").ne(Prop::List(Arc::new(vec![ Prop::U64(1), Prop::U64(6), Prop::U64(9), @@ -4193,7 +4207,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_lt() { - let filter = EdgeFilter::property("p1").lt(3u64); + let filter = EdgeFilter.property("p1").lt(3u64); let expected_results = vec!["N1->N2", "N2->N3", "N3->N4", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4210,7 +4224,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").lt(3i64); + let filter = EdgeFilter.property("k1").lt(3i64); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -4227,7 +4241,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").lt(10.0f64); + let filter = EdgeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4248,7 +4262,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_lt() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").lt(3u64); + let filter = EdgeFilter.property("p1").lt(3u64); let expected_results = vec![ "N1->N2", "N2->N3", "N3->N4", "N5->N6", "N6->N7", "N7->N8", "N8->N9", "N9->N10", ]; @@ -4267,7 +4281,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").lt(3i64); + let filter = EdgeFilter.property("k1").lt(3i64); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4284,7 +4298,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").lt(10.0f64); + let filter = EdgeFilter.property("k4").lt(10.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4304,7 +4318,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_le() { - let filter = EdgeFilter::property("p1").le(1u64); + let filter = EdgeFilter.property("p1").le(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4321,7 +4335,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").le(2i64); + let filter = EdgeFilter.property("k1").le(2i64); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -4338,7 +4352,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").le(6.0f64); + let filter = EdgeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4359,7 +4373,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_le() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").le(1u64); + let filter = EdgeFilter.property("p1").le(1u64); let expected_results = vec!["N1->N2", "N3->N4", "N6->N7", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -4376,7 +4390,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").le(2i64); + let filter = EdgeFilter.property("k1").le(2i64); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4393,7 +4407,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").le(6.0f64); + let filter = EdgeFilter.property("k4").le(6.0f64); let expected_results = vec!["N1->N2", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4413,7 +4427,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_gt() { - let filter = EdgeFilter::property("p1").gt(1u64); + let filter = EdgeFilter.property("p1").gt(1u64); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -4430,7 +4444,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").gt(2i64); + let filter = EdgeFilter.property("k1").gt(2i64); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4447,7 +4461,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").gt(6.0f64); + let filter = EdgeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -4464,7 +4478,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("x").gt(Prop::List(Arc::new(vec![ + let filter = EdgeFilter.property("x").gt(Prop::List(Arc::new(vec![ Prop::U64(1), Prop::U64(6), Prop::U64(9), @@ -4483,7 +4497,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_gt() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").gt(1u64); + let filter = EdgeFilter.property("p1").gt(1u64); let expected_results = vec![ "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N8->N9", "N9->N10", @@ -4503,7 +4517,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").gt(2i64); + let filter = EdgeFilter.property("k1").gt(2i64); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4520,7 +4534,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").gt(6.0f64); + let filter = EdgeFilter.property("k4").gt(6.0f64); let expected_results = vec!["N12->N13", "N2->N3", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4540,7 +4554,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_ge() { - let filter = EdgeFilter::property("p1").ge(1u64); + let filter = EdgeFilter.property("p1").ge(1u64); let expected_results = vec!["N1->N2", "N2->N3", "N3->N4", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4557,7 +4571,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").ge(2i64); + let filter = EdgeFilter.property("k1").ge(2i64); let expected_results = vec!["N1->N2", "N2->N3"]; assert_filter_edges_results( init_graph, @@ -4574,7 +4588,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").ge(6.0f64); + let filter = EdgeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1->N2", "N2->N3"]; assert_filter_edges_results( init_graph, @@ -4595,7 +4609,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_ge() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").ge(1u64); + let filter = EdgeFilter.property("p1").ge(1u64); let expected_results = vec![ "N1->N2", "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N3->N4", "N5->N6", "N6->N7", "N7->N8", "N8->N9", "N9->N10", @@ -4615,7 +4629,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").ge(2i64); + let filter = EdgeFilter.property("k1").ge(2i64); let expected_results = vec!["N1->N2", "N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( @@ -4633,7 +4647,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").ge(6.0f64); + let filter = EdgeFilter.property("k4").ge(6.0f64); let expected_results = vec!["N1->N2", "N12->N13", "N2->N3", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4653,7 +4667,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_in() { - let filter = EdgeFilter::property("p1").is_in(vec![2u64.into()]); + let filter = EdgeFilter.property("p1").is_in(vec![2u64.into()]); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -4670,7 +4684,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").is_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -4687,7 +4701,9 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k2").is_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter + .property("k2") + .is_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4704,7 +4720,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k3").is_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_in(vec![true.into()]); let expected_results = vec!["N2->N3"]; assert_filter_edges_results( init_graph, @@ -4721,7 +4737,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").is_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4742,7 +4758,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_in() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").is_in(vec![2u64.into()]); + let filter = EdgeFilter.property("p1").is_in(vec![2u64.into()]); let expected_results = vec!["N2->N3", "N5->N6", "N8->N9", "N9->N10"]; assert_filter_edges_results( init_graph, @@ -4759,7 +4775,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").is_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_in(vec![2i64.into()]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4776,7 +4792,9 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k2").is_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter + .property("k2") + .is_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4793,7 +4811,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k3").is_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_in(vec![true.into()]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4810,7 +4828,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").is_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_in(vec![6.0f64.into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4830,7 +4848,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_not_in() { - let filter = EdgeFilter::property("p1").is_not_in(vec![1u64.into()]); + let filter = EdgeFilter.property("p1").is_not_in(vec![1u64.into()]); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -4847,7 +4865,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k1").is_not_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4864,7 +4882,9 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k2").is_not_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter + .property("k2") + .is_not_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N2->N3", "N5->N6"]; assert_filter_edges_results( init_graph, @@ -4881,7 +4901,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k3").is_not_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4898,7 +4918,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k4").is_not_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = vec!["N2->N3", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -4919,7 +4939,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_not_in() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").is_not_in(vec![1u64.into()]); + let filter = EdgeFilter.property("p1").is_not_in(vec![1u64.into()]); let expected_results = vec![ "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N8->N9", "N9->N10", @@ -4939,7 +4959,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k1").is_not_in(vec![2i64.into()]); + let filter = EdgeFilter.property("k1").is_not_in(vec![2i64.into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4956,7 +4976,9 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k2").is_not_in(vec!["Paper_Airplane".into()]); + let filter = EdgeFilter + .property("k2") + .is_not_in(vec!["Paper_Airplane".into()]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N7->N8", "N8->N9"]; assert_filter_edges_results( init_graph, @@ -4973,7 +4995,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k3").is_not_in(vec![true.into()]); + let filter = EdgeFilter.property("k3").is_not_in(vec![true.into()]); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -4990,7 +5012,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k4").is_not_in(vec![6.0f64.into()]); + let filter = EdgeFilter.property("k4").is_not_in(vec![6.0f64.into()]); let expected_results = vec!["N12->N13", "N2->N3", "N5->N6", "N6->N7", "N7->N8", "N8->N9"]; assert_filter_edges_results( @@ -5011,7 +5033,7 @@ mod views_test { #[test] fn test_edges_filters_for_property_is_some() { - let filter = EdgeFilter::property("p1").is_some(); + let filter = EdgeFilter.property("p1").is_some(); let expected_results = vec!["N1->N2", "N2->N3", "N3->N4", "N5->N6", "N6->N7"]; assert_filter_edges_results( init_graph, @@ -5032,7 +5054,7 @@ mod views_test { #[test] fn test_edges_filters_pg_for_property_is_some() { // TODO: Const properties not supported for disk_graph. - let filter = EdgeFilter::property("p1").is_some(); + let filter = EdgeFilter.property("p1").is_some(); let expected_results = vec![ "N1->N2", "N10->N11", "N11->N12", "N12->N13", "N13->N14", "N2->N3", "N3->N4", "N5->N6", "N6->N7", "N7->N8", "N8->N9", "N9->N10", @@ -5078,7 +5100,9 @@ mod views_test { #[test] fn test_edges_filters_fuzzy_search() { - let filter = EdgeFilter::property("k2").fuzzy_search("Paper_Airpla", 2, false); + let filter = EdgeFilter + .property("k2") + .fuzzy_search("Paper_Airpla", 2, false); let expected_results = vec!["N1->N2"]; assert_filter_edges_results( init_graph, @@ -5100,7 +5124,7 @@ mod views_test { #[ignore] fn test_edges_filters_pg_fuzzy_search() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("k2").fuzzy_search("Paper_", 2, false); + let filter = EdgeFilter.property("k2").fuzzy_search("Paper_", 2, false); let expected_results = vec!["N1->N2", "N2->N3", "N7->N8"]; assert_filter_edges_results( init_graph, @@ -5121,7 +5145,7 @@ mod views_test { #[test] fn test_edges_filters_fuzzy_search_prefix_match() { - let filter = EdgeFilter::property("k2").fuzzy_search("Pa", 2, true); + let filter = EdgeFilter.property("k2").fuzzy_search("Pa", 2, true); let expected_results = vec!["N1->N2", "N2->N3"]; assert_filter_edges_results( init_graph, @@ -5138,7 +5162,7 @@ mod views_test { TestVariants::EventOnly, ); - let filter = EdgeFilter::property("k2").fuzzy_search("Pa", 2, true); + let filter = EdgeFilter.property("k2").fuzzy_search("Pa", 2, true); let expected_results = vec!["N1->N2", "N2->N3"]; assert_filter_edges_results( init_graph, @@ -5160,7 +5184,7 @@ mod views_test { #[ignore] fn test_edges_filters_pg_fuzzy_search_prefix_match() { // TODO: PropertyFilteringNotImplemented for variants persistent_graph, persistent_disk_graph for filter_edges. - let filter = EdgeFilter::property("k2").fuzzy_search("Pa", 2, true); + let filter = EdgeFilter.property("k2").fuzzy_search("Pa", 2, true); let expected_results = vec![ "N1->N2", "N12->N13", "N13->N14", "N2->N3", "N5->N6", "N7->N8", "N8->N9", ]; @@ -5172,7 +5196,7 @@ mod views_test { TestVariants::PersistentOnly, ); - let filter = EdgeFilter::property("k2").fuzzy_search("Pa", 2, false); + let filter = EdgeFilter.property("k2").fuzzy_search("Pa", 2, false); let expected_results = Vec::<&str>::new(); assert_search_edges_results( init_graph, diff --git a/raphtory/src/python/filter/edge_filter_builders.rs b/raphtory/src/python/filter/edge_filter_builders.rs index 055abe5501..8c95e16e19 100644 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ b/raphtory/src/python/filter/edge_filter_builders.rs @@ -5,11 +5,18 @@ use crate::{ InternalEdgeFilterBuilderOps, }, property_filter::{MetadataFilterBuilder, PropertyFilterBuilder}, - PropertyFilterFactory, + PropertyFilterFactory, Windowed, + }, + python::{ + filter::{ + filter_expr::PyFilterExpr, + window_filter::{PyEdgeWindow, PyExplodedEdgeWindow}, + }, + types::iterable::FromIterable, + utils::PyTime, }, - python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; -use pyo3::{pyclass, pymethods}; +use pyo3::{pyclass, pymethods, PyResult}; use raphtory_api::core::entities::GID; use std::sync::Arc; @@ -179,12 +186,19 @@ impl PyEdgeFilter { #[staticmethod] fn property(name: String) -> PropertyFilterBuilder { - EdgeFilter::property(name) + EdgeFilter.property(name) } #[staticmethod] fn metadata(name: String) -> MetadataFilterBuilder { - EdgeFilter::metadata(name) + EdgeFilter.metadata(name) + } + + #[staticmethod] + fn window(py_start: PyTime, py_end: PyTime) -> PyResult { + Ok(PyEdgeWindow(Windowed::::from_times( + py_start, py_end, + ))) } } @@ -196,11 +210,18 @@ pub struct PyExplodedEdgeFilter; impl PyExplodedEdgeFilter { #[staticmethod] fn property(name: String) -> PropertyFilterBuilder { - ExplodedEdgeFilter::property(name) + ExplodedEdgeFilter.property(name) } #[staticmethod] fn metadata(name: String) -> MetadataFilterBuilder { - ExplodedEdgeFilter::metadata(name) + ExplodedEdgeFilter.metadata(name) + } + + #[staticmethod] + fn window(py_start: PyTime, py_end: PyTime) -> PyResult { + Ok(PyExplodedEdgeWindow( + Windowed::::from_times(py_start, py_end), + )) } } diff --git a/raphtory/src/python/filter/filter_expr.rs b/raphtory/src/python/filter/filter_expr.rs index d9a9076157..75b3a32469 100644 --- a/raphtory/src/python/filter/filter_expr.rs +++ b/raphtory/src/python/filter/filter_expr.rs @@ -11,11 +11,9 @@ use crate::{ }, errors::GraphError, prelude::GraphViewOps, - python::filter::{ - create_filter::DynInternalFilterOps, property_filter_builders::PyPropertyFilterBuilder, - }, + python::filter::create_filter::DynInternalFilterOps, }; -use pyo3::{exceptions::PyTypeError, prelude::*}; +use pyo3::prelude::*; use std::sync::Arc; #[pyclass(frozen, name = "FilterExpr", module = "raphtory.filter")] diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index 1253cb050f..1aa6d6e80c 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -3,6 +3,7 @@ use crate::python::filter::{ filter_expr::PyFilterExpr, node_filter_builders::PyNodeFilter, property_filter_builders::{PyFilterOps, PyPropertyFilterBuilder}, + window_filter::{PyEdgeWindow, PyExplodedEdgeWindow, PyNodeWindow}, }; use pyo3::{ prelude::{PyModule, PyModuleMethods}, @@ -14,6 +15,7 @@ pub mod edge_filter_builders; pub mod filter_expr; pub mod node_filter_builders; pub mod property_filter_builders; +pub mod window_filter; pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> { let filter_module = PyModule::new(py, "filter")?; @@ -25,6 +27,10 @@ pub fn base_filter_module(py: Python<'_>) -> Result, PyErr> { filter_module.add_class::()?; filter_module.add_class::()?; filter_module.add_class::()?; + filter_module.add_class::()?; + filter_module.add_class::()?; + filter_module.add_class::()?; + filter_module.add_class::()?; Ok(filter_module) } diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs index 297cfc7c05..9714387de7 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -4,11 +4,15 @@ use crate::{ InternalNodeFilterBuilderOps, NodeFilter, NodeFilterBuilderOps, NodeIdFilterBuilder, }, property_filter::{MetadataFilterBuilder, PropertyFilterBuilder}, - PropertyFilterFactory, + PropertyFilterFactory, Windowed, + }, + python::{ + filter::{filter_expr::PyFilterExpr, window_filter::PyNodeWindow}, + types::iterable::FromIterable, + utils::PyTime, }, - python::{filter::filter_expr::PyFilterExpr, types::iterable::FromIterable}, }; -use pyo3::{pyclass, pymethods}; +use pyo3::{pyclass, pymethods, PyResult}; use raphtory_api::core::entities::GID; use std::sync::Arc; @@ -170,12 +174,19 @@ impl PyNodeFilter { #[staticmethod] fn property(name: String) -> PropertyFilterBuilder { - NodeFilter::property(name) + NodeFilter.property(name) } #[staticmethod] fn metadata(name: String) -> MetadataFilterBuilder { - NodeFilter::metadata(name) + NodeFilter.metadata(name) + } + + #[staticmethod] + fn window(py_start: PyTime, py_end: PyTime) -> PyResult { + Ok(PyNodeWindow(Windowed::::from_times( + py_start, py_end, + ))) } } diff --git a/raphtory/src/python/filter/property_filter_builders.rs b/raphtory/src/python/filter/property_filter_builders.rs index ce829c1ec8..17768dd538 100644 --- a/raphtory/src/python/filter/property_filter_builders.rs +++ b/raphtory/src/python/filter/property_filter_builders.rs @@ -3,8 +3,8 @@ use crate::{ internal::CreateFilter, model::{ property_filter::{ - ElemQualifierOps, ListAggOps, MetadataFilterBuilder, OpChainBuilder, - PropertyFilterBuilder, PropertyFilterOps, + ElemQualifierOps, ListAggOps, MetadataFilterBuilder, PropertyFilterBuilder, + PropertyFilterOps, }, TryAsCompositeFilter, }, @@ -194,7 +194,7 @@ impl PyFilterOps { Self { ops: Arc::new(t) } } - fn from_arc(ops: Arc) -> Self { + pub fn from_arc(ops: Arc) -> Self { Self { ops } } } @@ -316,16 +316,12 @@ pub struct PyPropertyFilterBuilder { } impl PyPropertyFilterBuilder { - fn wrap(t: T) -> Self { - Self { ops: Arc::new(t) } - } - - fn from_arc(ops: Arc) -> Self { + pub fn from_arc(ops: Arc) -> Self { Self { ops } } } -trait DynPropertyFilterBuilderOps: DynFilterOps { +pub trait DynPropertyFilterBuilderOps: DynFilterOps { fn temporal(&self) -> PyFilterOps; } diff --git a/raphtory/src/python/filter/window_filter.rs b/raphtory/src/python/filter/window_filter.rs new file mode 100644 index 0000000000..a99ff17468 --- /dev/null +++ b/raphtory/src/python/filter/window_filter.rs @@ -0,0 +1,79 @@ +use crate::{ + db::graph::views::filter::model::{ + edge_filter::{EdgeFilter, ExplodedEdgeFilter}, + node_filter::NodeFilter, + property_filter::{MetadataFilterBuilder, PropertyFilterBuilder}, + Windowed, + }, + python::filter::property_filter_builders::{PyFilterOps, PyPropertyFilterBuilder}, +}; +use pyo3::prelude::*; + +#[pyclass(frozen, name = "NodeWindow", module = "raphtory.filter")] +#[derive(Clone)] +pub struct PyNodeWindow(pub Windowed); + +#[pymethods] +impl PyNodeWindow { + fn property<'py>( + &self, + py: Python<'py>, + name: String, + ) -> PyResult> { + let b: PropertyFilterBuilder> = + PropertyFilterBuilder::new(name, self.0.clone()); + b.into_pyobject(py) + } + + fn metadata<'py>(&self, py: Python<'py>, name: String) -> PyResult> { + let b: MetadataFilterBuilder> = + MetadataFilterBuilder::new(name, self.0.clone()); + b.into_pyobject(py) + } +} + +#[pyclass(frozen, name = "EdgeWindow", module = "raphtory.filter")] +#[derive(Clone)] +pub struct PyEdgeWindow(pub Windowed); + +#[pymethods] +impl PyEdgeWindow { + fn property<'py>( + &self, + py: Python<'py>, + name: String, + ) -> PyResult> { + let b: PropertyFilterBuilder> = + PropertyFilterBuilder::new(name, self.0.clone()); + b.into_pyobject(py) + } + + fn metadata<'py>(&self, py: Python<'py>, name: String) -> PyResult> { + let b: MetadataFilterBuilder> = + MetadataFilterBuilder::new(name, self.0.clone()); + b.into_pyobject(py) + } +} + +#[pyclass(frozen, name = "ExplodedEdgeWindow", module = "raphtory.filter")] +#[derive(Clone)] +pub struct PyExplodedEdgeWindow(pub Windowed); + +#[pymethods] +impl PyExplodedEdgeWindow { + fn property<'py>( + &self, + py: Python<'py>, + name: String, + ) -> PyResult> { + let b: PropertyFilterBuilder> = + PropertyFilterBuilder::new(name, self.0.clone()); + b.into_pyobject(py) + } + + fn metadata<'py>(&self, py: Python<'py>, name: String) -> PyResult> { + let b: MetadataFilterBuilder> = + MetadataFilterBuilder::new(name, self.0.clone()); + b.into_pyobject(py) + } +} diff --git a/raphtory/src/python/graph/edges.rs b/raphtory/src/python/graph/edges.rs index ab768e8462..048a78ba53 100644 --- a/raphtory/src/python/graph/edges.rs +++ b/raphtory/src/python/graph/edges.rs @@ -84,7 +84,7 @@ impl From> for PyEdges { #[pymethods] impl PyEdges { fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let r = self.edges.filter_iter(filter)?; + let r = self.edges.select(filter)?; Ok(PyEdges::from(r)) } @@ -431,7 +431,7 @@ impl From> for PyNe #[pymethods] impl PyNestedEdges { fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let r = self.edges.filter_iter(filter)?; + let r = self.edges.select(filter)?; Ok(PyNestedEdges::from(r)) } diff --git a/raphtory/src/python/graph/node.rs b/raphtory/src/python/graph/node.rs index 9703842ffd..1a84f47465 100644 --- a/raphtory/src/python/graph/node.rs +++ b/raphtory/src/python/graph/node.rs @@ -481,7 +481,7 @@ impl PyNodes { } fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let r = self.nodes.filter_iter(filter)?; + let r = self.nodes.select(filter)?; Ok(PyNodes::from(r)) } } @@ -928,7 +928,7 @@ impl PyPathFromGraph { } fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let r = self.path.filter_iter(filter)?; + let r = self.path.select(filter)?; Ok(PyPathFromGraph::from(r)) } } @@ -1093,7 +1093,7 @@ impl PyPathFromNode { } fn __getitem__(&self, filter: PyFilterExpr) -> PyResult { - let r = self.path.filter_iter(filter)?; + let r = self.path.select(filter)?; Ok(PyPathFromNode::from(r)) } } diff --git a/raphtory/src/search/edge_filter_executor.rs b/raphtory/src/search/edge_filter_executor.rs index 12a53216e5..1fb82d42c3 100644 --- a/raphtory/src/search/edge_filter_executor.rs +++ b/raphtory/src/search/edge_filter_executor.rs @@ -14,7 +14,7 @@ use crate::{ }, }, errors::GraphError, - prelude::{GraphViewOps, PropertyFilter}, + prelude::{GraphViewOps, PropertyFilter, TimeOps}, search::{ collectors::unique_entity_filter_collector::UniqueEntityFilterCollector, fallback_filter_edges, fields, get_reader, graph_index::Index, @@ -22,7 +22,7 @@ use crate::{ }, }; use itertools::Itertools; -use raphtory_api::core::entities::EID; +use raphtory_api::core::{entities::EID, storage::timeindex::AsTime}; use raphtory_storage::graph::edges::edge_storage_ops::EdgeStorageOps; use std::{collections::HashSet, sync::Arc}; use tantivy::{ @@ -245,6 +245,25 @@ impl<'a> EdgeFilterExecutor<'a> { CompositeEdgeFilter::Property(filter) => { self.filter_property_index(graph, filter, limit, offset) } + CompositeEdgeFilter::PropertyWindowed(filter) => { + let start = filter.entity.start.t(); + let end = filter.entity.end.t(); + + let filter = PropertyFilter { + prop_ref: filter.prop_ref.clone(), + prop_value: filter.prop_value.clone(), + operator: filter.operator, + ops: filter.ops.clone(), + entity: EdgeFilter, + }; + + let res = + self.filter_property_index(&graph.window(start, end), &filter, limit, offset)?; + Ok(res + .into_iter() + .map(|x| EdgeView::new(graph.clone(), x.edge)) + .collect()) + } CompositeEdgeFilter::Edge(filter) => { self.filter_edge_index(graph, filter, limit, offset) } diff --git a/raphtory/src/search/exploded_edge_filter_executor.rs b/raphtory/src/search/exploded_edge_filter_executor.rs index fb4664a271..e1d6f1c044 100644 --- a/raphtory/src/search/exploded_edge_filter_executor.rs +++ b/raphtory/src/search/exploded_edge_filter_executor.rs @@ -6,14 +6,14 @@ use crate::{ views::filter::{ internal::CreateFilter, model::{ - edge_filter::{CompositeExplodedEdgeFilter, ExplodedEdgeFilter}, + edge_filter::{CompositeExplodedEdgeFilter, EdgeFilter, ExplodedEdgeFilter}, property_filter::PropertyRef, }, }, }, }, errors::GraphError, - prelude::{EdgeViewOps, PropertyFilter}, + prelude::{EdgeViewOps, PropertyFilter, TimeOps}, search::{ collectors::{ exploded_edge_property_filter_collector::ExplodedEdgePropertyFilterCollector, @@ -26,7 +26,10 @@ use crate::{ }, }; use itertools::Itertools; -use raphtory_api::core::{entities::EID, storage::timeindex::TimeIndexEntry}; +use raphtory_api::core::{ + entities::EID, + storage::timeindex::{AsTime, TimeIndexEntry}, +}; use raphtory_storage::graph::edges::edge_storage_ops::EdgeStorageOps; use std::{collections::HashSet, sync::Arc}; use tantivy::{collector::Collector, query::Query, IndexReader}; @@ -222,6 +225,25 @@ impl<'a> ExplodedEdgeFilterExecutor<'a> { CompositeExplodedEdgeFilter::Property(filter) => { self.filter_property_index(graph, filter, limit, offset) } + CompositeExplodedEdgeFilter::PropertyWindowed(filter) => { + let start = filter.entity.start.t(); + let end = filter.entity.end.t(); + + let filter = PropertyFilter { + prop_ref: filter.prop_ref.clone(), + prop_value: filter.prop_value.clone(), + operator: filter.operator, + ops: filter.ops.clone(), + entity: ExplodedEdgeFilter, + }; + + let res = + self.filter_property_index(&graph.window(start, end), &filter, limit, offset)?; + Ok(res + .into_iter() + .map(|x| EdgeView::new(graph.clone(), x.edge)) + .collect()) + } CompositeExplodedEdgeFilter::And(left, right) => { let left_result = self.filter_exploded_edges(graph, left, limit, offset)?; let right_result = self.filter_exploded_edges(graph, right, limit, offset)?; diff --git a/raphtory/src/search/graph_index.rs b/raphtory/src/search/graph_index.rs index 56091791ab..b0efbaeda9 100644 --- a/raphtory/src/search/graph_index.rs +++ b/raphtory/src/search/graph_index.rs @@ -558,7 +558,7 @@ mod graph_index_test { graph.create_index_in_ram().unwrap(); graph.node(1).unwrap().add_metadata([("x", 1u64)]).unwrap(); - let filter = NodeFilter::metadata("x").eq(1u64); + let filter = NodeFilter.metadata("x").eq(1u64); assert_eq!(search_nodes(&graph, filter.clone()), vec!["1"]); graph @@ -566,7 +566,7 @@ mod graph_index_test { .unwrap() .update_metadata([("x", 2u64)]) .unwrap(); - let filter = NodeFilter::metadata("x").eq(1u64); + let filter = NodeFilter.metadata("x").eq(1u64); assert_eq!(search_nodes(&graph, filter.clone()), Vec::<&str>::new()); graph @@ -574,7 +574,7 @@ mod graph_index_test { .unwrap() .update_metadata([("x", 2u64)]) .unwrap(); - let filter = NodeFilter::metadata("x").eq(2u64); + let filter = NodeFilter.metadata("x").eq(2u64); assert_eq!(search_nodes(&graph, filter.clone()), vec!["1"]); } @@ -589,7 +589,7 @@ mod graph_index_test { .add_metadata([("x", 1u64)], None) .unwrap(); - let filter = EdgeFilter::metadata("x").eq(1u64); + let filter = EdgeFilter.metadata("x").eq(1u64); assert_eq!(search_edges(&graph, filter.clone()), vec!["1->2"]); graph @@ -597,7 +597,7 @@ mod graph_index_test { .unwrap() .update_metadata([("x", 2u64)], None) .unwrap(); - let filter = EdgeFilter::metadata("x").eq(1u64); + let filter = EdgeFilter.metadata("x").eq(1u64); assert_eq!(search_edges(&graph, filter.clone()), Vec::<&str>::new()); graph @@ -605,7 +605,7 @@ mod graph_index_test { .unwrap() .update_metadata([("x", 2u64)], None) .unwrap(); - let filter = EdgeFilter::metadata("x").eq(2u64); + let filter = EdgeFilter.metadata("x").eq(2u64); assert_eq!(search_edges(&graph, filter.clone()), vec!["1->2"]); } } diff --git a/raphtory/src/search/mod.rs b/raphtory/src/search/mod.rs index 47c083b595..96f0a3832a 100644 --- a/raphtory/src/search/mod.rs +++ b/raphtory/src/search/mod.rs @@ -724,15 +724,17 @@ mod test_index { ); graph.create_index_in_ram_with_spec(index_spec).unwrap(); - let filter = NodeFilter::property("p1") + let filter = NodeFilter + .property("p1") .eq(5u64) - .and(NodeFilter::metadata("x").eq(true)); + .and(NodeFilter.metadata("x").eq(true)); let results = search_nodes(&graph, filter); assert_eq!(results, vec!["pometry"]); - let filter = EdgeFilter::property("e_p1") + let filter = EdgeFilter + .property("e_p1") .lt(5f64) - .and(EdgeFilter::metadata("e_y").eq(false)); + .and(EdgeFilter.metadata("e_y").eq(false)); let results = search_edges(&graph, filter); assert_eq!(results, vec!["raphtory->pometry"]); } @@ -756,19 +758,21 @@ mod test_index { ); graph.create_index_in_ram_with_spec(index_spec).unwrap(); - let filter = NodeFilter::property("p1") + let filter = NodeFilter + .property("p1") .eq(5u64) - .or(NodeFilter::metadata("y").eq(false)); + .or(NodeFilter.metadata("y").eq(false)); let results = search_nodes(&graph, filter); assert_eq!(results, vec!["pometry", "raphtory"]); - let filter = NodeFilter::metadata("y").eq(false); + let filter = NodeFilter.metadata("y").eq(false); let results = search_nodes(&graph, filter); assert_eq!(results, vec!["raphtory"]); - let filter = EdgeFilter::property("e_p1") + let filter = EdgeFilter + .property("e_p1") .lt(5f64) - .or(EdgeFilter::metadata("e_y").eq(false)); + .or(EdgeFilter.metadata("e_y").eq(false)); let results = search_edges(&graph, filter); assert_eq!(results, vec!["pometry->raphtory", "raphtory->pometry"]); } @@ -793,15 +797,17 @@ mod test_index { graph.create_index_in_ram_with_spec(index_spec).unwrap(); - let filter = NodeFilter::property("p1") + let filter = NodeFilter + .property("p1") .eq(5u64) - .and(NodeFilter::metadata("x").eq(true)); + .and(NodeFilter.metadata("x").eq(true)); let results = search_nodes(&graph, filter); assert_eq!(results, vec!["pometry"]); - let filter = EdgeFilter::property("e_p1") + let filter = EdgeFilter + .property("e_p1") .lt(5f64) - .or(EdgeFilter::metadata("e_y").eq(false)); + .or(EdgeFilter.metadata("e_y").eq(false)); let results = search_edges(&graph, filter); assert_eq!(results, vec!["pometry->raphtory", "raphtory->pometry"]); } @@ -828,15 +834,17 @@ mod test_index { graph.create_index_in_ram_with_spec(index_spec).unwrap(); - let filter = NodeFilter::property("p1") + let filter = NodeFilter + .property("p1") .eq(5u64) - .or(NodeFilter::metadata("y").eq(false)); + .or(NodeFilter.metadata("y").eq(false)); let results = search_nodes(&graph, filter); assert_eq!(results, vec!["pometry", "raphtory"]); - let filter = EdgeFilter::property("e_p1") + let filter = EdgeFilter + .property("e_p1") .lt(5f64) - .or(EdgeFilter::metadata("e_y").eq(false)); + .or(EdgeFilter.metadata("e_y").eq(false)); let results = search_edges(&graph, filter); assert_eq!(results, vec!["pometry->raphtory", "raphtory->pometry"]); } @@ -870,9 +878,9 @@ mod test_index { graph.create_index_with_spec(index_spec.clone()).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter::metadata("y").eq(false)); + let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter::metadata("e_y").eq(false)); + let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); assert_eq!(results, vec!["raphtory->pometry"]); let index_spec = IndexSpecBuilder::new(graph.clone()) @@ -886,9 +894,9 @@ mod test_index { graph.create_index_with_spec(index_spec.clone()).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter::metadata("y").eq(false)); + let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter::metadata("e_y").eq(false)); + let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); assert_eq!(results, vec!["raphtory->pometry"]); } @@ -908,9 +916,9 @@ mod test_index { let graph = Graph::decode(path.clone()).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter::metadata("y").eq(false)); + let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter::metadata("e_y").eq(false)); + let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); assert_eq!(results, vec!["raphtory->pometry"]); let index_spec = IndexSpecBuilder::new(graph.clone()) @@ -928,9 +936,9 @@ mod test_index { let graph = Graph::decode(path).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let results = search_nodes(&graph, NodeFilter::metadata("y").eq(false)); + let results = search_nodes(&graph, NodeFilter.metadata("y").eq(false)); assert_eq!(results, vec!["raphtory"]); - let results = search_edges(&graph, EdgeFilter::metadata("e_y").eq(false)); + let results = search_edges(&graph, EdgeFilter.metadata("e_y").eq(false)); assert_eq!(results, vec!["raphtory->pometry"]); } @@ -1019,7 +1027,7 @@ mod test_index { .build(); create_index_fn(&graph, index_spec.clone()).unwrap(); - let filter = NodeFilter::property("p2").temporal().last().eq(50u64); + let filter = NodeFilter.property("p2").temporal().last().eq(50u64); assert_eq!(search_nodes(&graph, filter.clone()), vec!["pometry"]); let node = graph @@ -1027,17 +1035,17 @@ mod test_index { .unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = NodeFilter::property("p1").temporal().last().eq(100u64); + let filter = NodeFilter.property("p1").temporal().last().eq(100u64); assert_eq!(search_nodes(&graph, filter.clone()), vec!["shivam"]); node.add_metadata([("z", true)]).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = NodeFilter::metadata("z").eq(true); + let filter = NodeFilter.metadata("z").eq(true); assert_eq!(search_nodes(&graph, filter.clone()), vec!["shivam"]); node.update_metadata([("z", false)]).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = NodeFilter::metadata("z").eq(false); + let filter = NodeFilter.metadata("z").eq(false); assert_eq!(search_nodes(&graph, filter.clone()), vec!["shivam"]); } @@ -1059,17 +1067,17 @@ mod test_index { .add_edge(1, "shivam", "kapoor", [("p1", 100u64)], None) .unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = EdgeFilter::property("p1").temporal().last().eq(100u64); + let filter = EdgeFilter.property("p1").temporal().last().eq(100u64); assert_eq!(search_edges(&graph, filter.clone()), vec!["shivam->kapoor"]); edge.add_metadata([("z", true)], None).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = EdgeFilter::metadata("z").eq(true); + let filter = EdgeFilter.metadata("z").eq(true); assert_eq!(search_edges(&graph, filter.clone()), vec!["shivam->kapoor"]); edge.update_metadata([("z", false)], None).unwrap(); assert_eq!(index_spec, graph.get_index_spec().unwrap()); - let filter = EdgeFilter::metadata("z").eq(false); + let filter = EdgeFilter.metadata("z").eq(false); assert_eq!(search_edges(&graph, filter.clone()), vec!["shivam->kapoor"]); } } diff --git a/raphtory/src/search/node_filter_executor.rs b/raphtory/src/search/node_filter_executor.rs index 53a45022c4..75c50342c7 100644 --- a/raphtory/src/search/node_filter_executor.rs +++ b/raphtory/src/search/node_filter_executor.rs @@ -2,6 +2,7 @@ use crate::{ db::{ api::view::{internal::FilterOps, BaseFilterOps, StaticGraphViewOps}, graph::{ + edge::EdgeView, node::NodeView, views::filter::{ internal::CreateFilter, @@ -14,7 +15,7 @@ use crate::{ }, }, errors::GraphError, - prelude::{GraphViewOps, PropertyFilter}, + prelude::{GraphViewOps, PropertyFilter, TimeOps}, search::{ collectors::unique_entity_filter_collector::UniqueEntityFilterCollector, fallback_filter_nodes, fields, get_reader, graph_index::Index, @@ -22,7 +23,7 @@ use crate::{ }, }; use itertools::Itertools; -use raphtory_api::core::entities::VID; +use raphtory_api::core::{entities::VID, storage::timeindex::AsTime}; use std::{collections::HashSet, sync::Arc}; use tantivy::{ collector::Collector, query::Query, schema::Value, DocAddress, Document, IndexReader, Score, @@ -249,6 +250,25 @@ impl<'a> NodeFilterExecutor<'a> { CompositeNodeFilter::Property(filter) => { self.filter_property_index(graph, filter, limit, offset) } + CompositeNodeFilter::PropertyWindowed(filter) => { + let start = filter.entity.start.t(); + let end = filter.entity.end.t(); + + let filter = PropertyFilter { + prop_ref: filter.prop_ref.clone(), + prop_value: filter.prop_value.clone(), + operator: filter.operator, + ops: filter.ops.clone(), + entity: NodeFilter, + }; + + let res = + self.filter_property_index(&graph.window(start, end), &filter, limit, offset)?; + Ok(res + .into_iter() + .map(|x| NodeView::new_internal(graph.clone(), x.node)) + .collect()) + } CompositeNodeFilter::Node(filter) => { self.filter_node_index(graph, filter, limit, offset) } diff --git a/raphtory/src/search/searcher.rs b/raphtory/src/search/searcher.rs index b1ffc3e527..727bde789a 100644 --- a/raphtory/src/search/searcher.rs +++ b/raphtory/src/search/searcher.rs @@ -159,18 +159,18 @@ mod search_tests { #[test] fn test_fuzzy_search_property() { - let filter = NodeFilter::property("p1").fuzzy_search("tano", 2, false); + let filter = NodeFilter.property("p1").fuzzy_search("tano", 2, false); let results = fuzzy_search_nodes(filter); assert_eq!(results, vec!["pometry"]); } #[test] fn test_fuzzy_search_property_prefix_match() { - let filter = NodeFilter::property("p1").fuzzy_search("char", 2, false); + let filter = NodeFilter.property("p1").fuzzy_search("char", 2, false); let results = fuzzy_search_nodes(filter); assert_eq!(results, Vec::::new()); - let filter = NodeFilter::property("p1").fuzzy_search("char", 2, true); + let filter = NodeFilter.property("p1").fuzzy_search("char", 2, true); let results = fuzzy_search_nodes(filter); assert_eq!(results, vec!["shivam_kapoor"]); } @@ -258,18 +258,18 @@ mod search_tests { #[test] fn test_fuzzy_search_property() { - let filter = EdgeFilter::property("p1").fuzzy_search("tano", 2, false); + let filter = EdgeFilter.property("p1").fuzzy_search("tano", 2, false); let results = fuzzy_search_edges(filter); assert_eq!(results, vec![("shivam".into(), "raphtory".into())]); } #[test] fn test_fuzzy_search_property_prefix_match() { - let filter = EdgeFilter::property("p1").fuzzy_search("charl", 1, false); + let filter = EdgeFilter.property("p1").fuzzy_search("charl", 1, false); let results = fuzzy_search_edges(filter); assert_eq!(results, Vec::<(String, String)>::new()); - let filter = EdgeFilter::property("p1").fuzzy_search("charl", 1, true); + let filter = EdgeFilter.property("p1").fuzzy_search("charl", 1, true); let results = fuzzy_search_edges(filter); assert_eq!(results, vec![("raphtory".into(), "pometry".into())]); }