diff --git a/python/python/raphtory/filter/__init__.pyi b/python/python/raphtory/filter/__init__.pyi index 51123a463a..c8ce692f56 100644 --- a/python/python/raphtory/filter/__init__.pyi +++ b/python/python/raphtory/filter/__init__.pyi @@ -199,6 +199,10 @@ class Node(object): @staticmethod def id(): ... @staticmethod + def layer(layer): ... + @staticmethod + def layers(layers): ... + @staticmethod def metadata(name): ... @staticmethod def name(): ... @@ -540,6 +544,10 @@ class Edge(object): @staticmethod def dst(): ... @staticmethod + def layer(layer): ... + @staticmethod + def layers(layers): ... + @staticmethod def metadata(name): ... @staticmethod def property(name): ... @@ -883,6 +891,10 @@ class EdgeEndpointTypeFilter(object): """ class ExplodedEdge(object): + @staticmethod + def layer(layer): ... + @staticmethod + def layers(layers): ... @staticmethod def metadata(name): ... @staticmethod diff --git a/python/test_utils/utils.py b/python/test_utils/utils.py index 87913920e9..e00dc60327 100644 --- a/python/test_utils/utils.py +++ b/python/test_utils/utils.py @@ -182,6 +182,42 @@ def run_group_graphql_error_test(queries_and_expected_error_messages, graph): ), f"Expected '{expected_error_message}', but got '{error_message}'" +def run_graphql_error_test_contains(query, expected_substrings, graph): + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir, create_index=True).start(PORT) as server: + client = server.get_client() + client.send_graph(path="g", graph=graph) + + with pytest.raises(Exception) as excinfo: + client.query(query) + + full_error_message = str(excinfo.value) + match = re.search(r'"message":"(.*?)"', full_error_message) + error_message = match.group(1) if match else "" + + for s in expected_substrings: + assert s in error_message, f"expected to find {s!r} in {error_message!r}" + + +def run_graphql_compare_test(query_a, query_b, graph): + tmp_work_dir = tempfile.mkdtemp() + with GraphServer(tmp_work_dir, create_index=True).start(PORT) as server: + client = server.get_client() + client.send_graph(path="g", graph=graph) + + resp_a = client.query(query_a) + resp_b = client.query(query_b) + + dict_a = json.loads(resp_a) if isinstance(resp_a, str) else resp_a + dict_b = json.loads(resp_b) if isinstance(resp_b, str) else resp_b + + assert sort_dict_recursive(dict_a) == sort_dict_recursive(dict_b), ( + f"Query A != Query B\n" + f"A={sort_dict_recursive(dict_a)}\n" + f"B={sort_dict_recursive(dict_b)}" + ) + + def assert_set_eq(left, right): """Check if two lists are the same set and same length""" assert len(left) == len(right) diff --git a/python/tests/test_base_install/test_filters/test_edge_property_filter.py b/python/tests/test_base_install/test_filters/test_edge_property_filter.py index 04942783ea..ecc047b858 100644 --- a/python/tests/test_base_install/test_filters/test_edge_property_filter.py +++ b/python/tests/test_base_install/test_filters/test_edge_property_filter.py @@ -1436,3 +1436,27 @@ def check(graph): } return check + + +@with_disk_variants(init_graph, variants=("graph", "persistent_graph")) +def test_filter_edges_temporal_layer_eq(): + def check(graph): + expr = ( + filter.Edge.layers(["air_nomads"]).property("p10").temporal().last() + == "Paper_ship" + ) + assert _pairs(graph.filter(expr).edges) == {("2", "3")} + + return check + + +@with_disk_variants(init_graph, variants=("graph", "persistent_graph")) +def test_filter_edges_temporal_layer_eq_is_empty(): + def check(graph): + expr = ( + filter.Edge.layer("air_nomads").property("p10").temporal().last() + == "Paper_airplane" + ) + assert _pairs(graph.filter(expr).edges) == set() + + return check diff --git a/python/tests/test_base_install/test_filters/test_node_property_filter.py b/python/tests/test_base_install/test_filters/test_node_property_filter.py index 4114aef26e..85bf2864d6 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 @@ -1139,3 +1139,24 @@ def check(graph): assert list(graph.filter(expr).nodes.id) == [] return check + + +@with_disk_variants(create_test_graph, variants=("graph", "persistent_graph")) +def test_filter_nodes_temporal_layer_sum_ge(): + def check(graph): + expr = ( + filter.Node.layers(["fire_nation"]) + .property("prop5") + .temporal() + .last() + .sum() + >= 12 + ) + msg = """Invalid layer: fire_nation. Valid layers: ["_default"]""" + with pytest.raises( + Exception, + match=re.escape(msg), + ): + graph.filter(expr).nodes.id + + return check diff --git a/python/tests/test_base_install/test_graphdb/test_graphdb.py b/python/tests/test_base_install/test_graphdb/test_graphdb.py index 0ab66b3d2d..a15e816a05 100644 --- a/python/tests/test_base_install/test_graphdb/test_graphdb.py +++ b/python/tests/test_base_install/test_graphdb/test_graphdb.py @@ -1767,7 +1767,7 @@ def check(g): with pytest.raises( Exception, match=re.escape( - "Invalid layer: test_layer. Valid layers: _default, layer1, layer2" + """Invalid layer: test_layer. Valid layers: ["_default", "layer1", "layer2"]""" ), ): g.layers(["test_layer"]) @@ -1775,7 +1775,7 @@ def check(g): with pytest.raises( Exception, match=re.escape( - "Invalid layer: test_layer. Valid layers: _default, layer1, layer2" + """Invalid layer: test_layer. Valid layers: ["_default", "layer1", "layer2"]""" ), ): g.edge(1, 2).layers(["test_layer"]) 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 20c7c0d86b..ecf6d4cb9c 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,7 +1,12 @@ import pytest from raphtory import Graph, PersistentGraph from filters_setup import create_test_graph, init_graph2 -from utils import run_graphql_test, run_graphql_error_test +from utils import ( + run_graphql_test, + run_graphql_error_test, + run_graphql_error_test_contains, + run_graphql_compare_test, +) EVENT_GRAPH = create_test_graph(Graph()) PERSISTENT_GRAPH = create_test_graph(PersistentGraph()) @@ -715,3 +720,149 @@ def test_edges_chained_selection_edges_filter_paired_ver2(graph): } } run_graphql_test(query, expected_output, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edge_temporal_property_filter_empty_layers_is_error(graph): + query = """ + query { + graph(path: "g") { + filterEdges(expr: { + temporalProperty: { + name: "prop5" + layers: [] + where: { any: { avg: { lt: { f64: 10.0 } } } } + } + }) { + edges { list { src { name } dst { name } } } + } + } + } + """ + + run_graphql_error_test_contains( + query, + [ + "EdgeFilter.temporalProperty", + "'layers' must be non-empty", + ], + graph, + ) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edge_temporal_property_filter_layer_and_layers_is_error(graph): + query = """ + query { + graph(path: "g") { + filterEdges(expr: { + temporalProperty: { + name: "prop5" + layer: "air_nomads" + layers: ["water_tribe"] + where: { any: { avg: { lt: { f64: 10.0 } } } } + } + }) { + edges { list { src { name } dst { name } } } + } + } + } + """ + + run_graphql_error_test_contains( + query, + [ + "EdgeFilter.temporalProperty", + "either 'layer' or 'layers'", + "not both", + ], + graph, + ) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_temporal_property_last_with_single_layer(graph): + query = """ + query { + graph(path: "g") { + filterEdges(expr: { + temporalProperty: { + name: "p10" + layer: "air_nomads" + where: { last: { eq: { str: "Paper_ship" } } } + } + }) { + edges { list { src { name } dst { name } } } + } + } + } + """ + + # Edge (2 -> 3) in air_nomads has p10 Paper_ship at time 2 + expected = { + "graph": { + "filterEdges": { + "edges": {"list": [{"src": {"name": "2"}, "dst": {"name": "3"}}]} + } + } + } + + run_graphql_test(query, expected, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_temporal_property_last_with_multiple_layers(graph): + query = """ + query { + graph(path: "g") { + filterEdges(expr: { + temporalProperty: { + name: "p10" + layers: ["fire_nation", "air_nomads"] + where: { last: { eq: { str: "Paper_airplane" } } } + } + }) { + edges { list { src { name } dst { name } } } + } + } + } + """ + + # fire_nation edge (1 -> 2) has p10 Paper_airplane at time 1 + expected = { + "graph": { + "filterEdges": { + "edges": {"list": [{"src": {"name": "1"}, "dst": {"name": "2"}}]} + } + } + } + run_graphql_test(query, expected, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_edges_temporal_property_last_with_default_layer(graph): + query = """ + query { + graph(path: "g") { + filterEdges(expr: { + temporalProperty: { + name: "p10" + layer: "_default" + where: { last: { eq: { str: "Paper_airplane" } } } + } + }) { + edges { list { src { name } dst { name } } } + } + } + } + """ + + # default-layer edge (2 -> 1) has p10 Paper_airplane at time 3 (edge_type is None) + expected = { + "graph": { + "filterEdges": { + "edges": {"list": [{"src": {"name": "2"}, "dst": {"name": "1"}}]} + } + } + } + run_graphql_test(query, expected, 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 c281ee5a22..be9bf5b942 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 @@ -7,7 +7,11 @@ init_graph, init_graph2, ) -from utils import run_graphql_test, run_graphql_error_test +from utils import ( + run_graphql_test, + run_graphql_error_test, + run_graphql_error_test_contains, +) EVENT_GRAPH = create_test_graph(Graph()) PERSISTENT_GRAPH = create_test_graph(PersistentGraph()) @@ -1051,3 +1055,31 @@ def test_nodes_temporal_property_filter_any_avg_with_window(graph): "graph": {"filterNodes": {"nodes": {"list": [{"name": "a"}, {"name": "c"}]}}} } run_graphql_test(query, expected, graph) + + +@pytest.mark.parametrize("graph", [EVENT_GRAPH, PERSISTENT_GRAPH]) +def test_node_property_layer_filter_not_supported(graph): + query = """ + query { + graph(path: "g") { + filterNodes(expr: { + temporalProperty: { + name: "prop5" + layer: "air_nomads" + where: { any: { avg: { lt: { f64: 10.0 } } } } + } + }) { + nodes { + list { name } + } + } + } + } + """ + + expected_needles = [ + "Invalid layer: air_nomads", + "Valid layers:", + ] + + run_graphql_error_test_contains(query, expected_needles, graph) diff --git a/raphtory-api/src/core/entities/layers.rs b/raphtory-api/src/core/entities/layers.rs index 8bb8aca042..1e5cc31650 100644 --- a/raphtory-api/src/core/entities/layers.rs +++ b/raphtory-api/src/core/entities/layers.rs @@ -6,7 +6,7 @@ use iter_enum::{ use rayon::prelude::*; use std::{iter::Copied, sync::Arc}; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Layer { All, None, diff --git a/raphtory-core/src/entities/graph/tgraph.rs b/raphtory-core/src/entities/graph/tgraph.rs index 1fd6d900b7..839bce2f75 100644 --- a/raphtory-core/src/entities/graph/tgraph.rs +++ b/raphtory-core/src/entities/graph/tgraph.rs @@ -54,10 +54,10 @@ pub struct TemporalGraph { } #[derive(Error, Debug)] -#[error("Invalid layer: {invalid_layer}. Valid layers: {valid_layers}")] +#[error("Invalid layer: {invalid_layer}. Valid layers: {valid_layers:?}")] pub struct InvalidLayer { invalid_layer: ArcStr, - valid_layers: String, + valid_layers: Vec, } #[derive(Error, Debug)] @@ -65,8 +65,7 @@ pub struct InvalidLayer { pub struct TooManyLayers; impl InvalidLayer { - pub fn new(invalid_layer: ArcStr, valid: Vec) -> Self { - let valid_layers = valid.join(", "); + pub fn new(invalid_layer: ArcStr, valid_layers: Vec) -> Self { Self { invalid_layer, valid_layers, diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index 18eef44808..f2bd693788 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -964,8 +964,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 { @@ -2236,6 +2236,8 @@ type Property { input PropertyFilterNew { name: String! window: Window + layer: String + layers: [String!] where: PropCondition! } @@ -2568,4 +2570,3 @@ schema { query: QueryRoot mutation: MutRoot } - diff --git a/raphtory-graphql/src/model/graph/filtering.rs b/raphtory-graphql/src/model/graph/filtering.rs index 2189f8e558..6b6b266f08 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -11,13 +11,14 @@ use raphtory::{ edge_filter::{CompositeEdgeFilter, EdgeFilter}, filter::{Filter, FilterValue}, filter_operator::FilterOperator, + layered_filter::Layered, node_filter::{CompositeNodeFilter, NodeFilter}, property_filter::{Op, PropertyFilter, PropertyFilterValue, PropertyRef}, windowed_filter::Windowed, }, errors::GraphError, }; -use raphtory_api::core::entities::{properties::prop::Prop, GID}; +use raphtory_api::core::entities::{properties::prop::Prop, Layer, GID}; use std::{ borrow::Cow, collections::HashSet, @@ -287,6 +288,10 @@ impl Display for NodeField { pub struct PropertyFilterNew { pub name: String, pub window: Option, + + pub layer: Option, + pub layers: Option>, + #[graphql(name = "where")] pub where_: PropCondition, } @@ -913,6 +918,42 @@ fn build_node_filter_from_prop_condition( } } +fn normalise_layers_to_layer( + layer: &Option, + layers: &Option>, + name_for_errors: &str, +) -> Result, GraphError> { + match (layer, layers) { + (None, None) => Ok(None), + (Some(l), None) => Ok(Some(Layer::from(l.as_str()))), + (None, Some(ls)) => { + if ls.is_empty() { + return Err(GraphError::InvalidGqlFilter(format!( + "{name_for_errors}: 'layers' must be non-empty" + ))); + } + Ok(Some(Layer::from(ls.clone()))) + } + (Some(_), Some(_)) => Err(GraphError::InvalidGqlFilter(format!( + "{name_for_errors}: provide either 'layer' or 'layers', not both" + ))), + } +} + +fn maybe_wrap_node_layer(filter: CompositeNodeFilter, layer: Option) -> CompositeNodeFilter { + match layer { + None => filter, + Some(layer) => CompositeNodeFilter::Layered(Box::new(Layered::new(layer, filter))), + } +} + +fn maybe_wrap_edge_layer(filter: CompositeEdgeFilter, layer: Option) -> CompositeEdgeFilter { + match layer { + None => filter, + Some(layer) => CompositeEdgeFilter::Layered(Box::new(Layered::new(layer, filter))), + } +} + impl TryFrom for CompositeNodeFilter { type Error = GraphError; fn try_from(filter: GqlNodeFilter) -> Result { @@ -927,22 +968,36 @@ impl TryFrom for CompositeNodeFilter { })) } GqlNodeFilter::Property(prop) => { + let layers = + normalise_layers_to_layer(&prop.layer, &prop.layers, "NodeFilter.property")?; let prop_ref = PropertyRef::Property(prop.name); - build_node_filter_from_prop_condition(prop_ref, &prop.where_) + let filter = build_node_filter_from_prop_condition(prop_ref, &prop.where_)?; + Ok(maybe_wrap_node_layer(filter, layers)) } GqlNodeFilter::Metadata(prop) => { + let layers = + normalise_layers_to_layer(&prop.layer, &prop.layers, "NodeFilter.metadata")?; let prop_ref = PropertyRef::Metadata(prop.name); - build_node_filter_from_prop_condition(prop_ref, &prop.where_) + let filter = build_node_filter_from_prop_condition(prop_ref, &prop.where_)?; + Ok(maybe_wrap_node_layer(filter, layers)) } GqlNodeFilter::TemporalProperty(prop) => { + let layers = normalise_layers_to_layer( + &prop.layer, + &prop.layers, + "NodeFilter.temporalProperty", + )?; let prop_ref = PropertyRef::TemporalProperty(prop.name); + + let mut filter = build_node_filter_from_prop_condition(prop_ref, &prop.where_)?; + if let Some(w) = prop.window { - let filter = build_node_filter_from_prop_condition(prop_ref, &prop.where_)?; - let filter = Windowed::from_times(w.start, w.end, filter); - let filter = CompositeNodeFilter::Windowed(Box::new(filter)); - return Ok(filter); + let windowed = Windowed::from_times(w.start, w.end, filter); + filter = CompositeNodeFilter::Windowed(Box::new(windowed)); } - build_node_filter_from_prop_condition(prop_ref, &prop.where_) + + filter = maybe_wrap_node_layer(filter, layers); + Ok(filter) } GqlNodeFilter::And(and_filters) => { let mut iter = and_filters.into_iter().map(TryInto::try_into); @@ -1029,22 +1084,34 @@ impl TryFrom for CompositeEdgeFilter { Ok(CompositeEdgeFilter::Dst(nf)) } GqlEdgeFilter::Property(p) => { + let layers = normalise_layers_to_layer(&p.layer, &p.layers, "EdgeFilter.property")?; let prop_ref = PropertyRef::Property(p.name); - build_edge_filter_from_prop_condition(prop_ref, &p.where_) + let filter = build_edge_filter_from_prop_condition(prop_ref, &p.where_)?; + Ok(maybe_wrap_edge_layer(filter, layers)) } GqlEdgeFilter::Metadata(p) => { + let layers = normalise_layers_to_layer(&p.layer, &p.layers, "EdgeFilter.metadata")?; let prop_ref = PropertyRef::Metadata(p.name); - build_edge_filter_from_prop_condition(prop_ref, &p.where_) + let filter = build_edge_filter_from_prop_condition(prop_ref, &p.where_)?; + Ok(maybe_wrap_edge_layer(filter, layers)) } GqlEdgeFilter::TemporalProperty(prop) => { + let layers = normalise_layers_to_layer( + &prop.layer, + &prop.layers, + "EdgeFilter.temporalProperty", + )?; let prop_ref = PropertyRef::TemporalProperty(prop.name); + + let mut filter = build_edge_filter_from_prop_condition(prop_ref, &prop.where_)?; + if let Some(w) = prop.window { - let filter = build_edge_filter_from_prop_condition(prop_ref, &prop.where_)?; - let filter = Windowed::from_times(w.start, w.end, filter); - let filter = CompositeEdgeFilter::Windowed(Box::new(filter)); - return Ok(filter); + let windowed = Windowed::from_times(w.start, w.end, filter); + filter = CompositeEdgeFilter::Windowed(Box::new(windowed)); } - build_edge_filter_from_prop_condition(prop_ref, &prop.where_) + + filter = maybe_wrap_edge_layer(filter, layers); + Ok(filter) } GqlEdgeFilter::And(and_filters) => { let mut iter = and_filters.into_iter().map(TryInto::try_into); 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 328edc4965..93117f76c5 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -8,6 +8,7 @@ use crate::{ edge_node_filtered_graph::EdgeNodeFilteredGraph, model::{ exploded_edge_filter::CompositeExplodedEdgeFilter, + layered_filter::Layered, node_filter::{ builders::{ InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, @@ -30,6 +31,7 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; +use raphtory_api::core::entities::Layer; use raphtory_core::utils::time::IntoTime; use std::{fmt, fmt::Display, sync::Arc}; @@ -52,6 +54,11 @@ impl EdgeFilter { pub fn window(start: S, end: E) -> Windowed { Windowed::from_times(start, end, EdgeFilter) } + + #[inline] + pub fn layer>(layer: L) -> Layered { + Layered::from_layers(layer, EdgeFilter) + } } impl Wrap for EdgeFilter { @@ -281,6 +288,7 @@ pub enum CompositeEdgeFilter { Dst(CompositeNodeFilter), Property(PropertyFilter), Windowed(Box>), + Layered(Box>), And(Box, Box), Or(Box, Box), Not(Box), @@ -293,6 +301,7 @@ impl Display for CompositeEdgeFilter { CompositeEdgeFilter::Dst(filter) => write!(f, "DST({})", filter), CompositeEdgeFilter::Property(filter) => write!(f, "{}", filter), CompositeEdgeFilter::Windowed(filter) => write!(f, "{}", filter), + CompositeEdgeFilter::Layered(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), @@ -328,6 +337,10 @@ impl CreateFilter for CompositeEdgeFilter { let dyn_graph: Arc = Arc::new(graph); i.create_filter(dyn_graph) } + CompositeEdgeFilter::Layered(i) => { + let dyn_graph: Arc = Arc::new(graph); + i.create_filter(dyn_graph) + } CompositeEdgeFilter::And(l, r) => { let (l, r) = (*l, *r); Ok(Arc::new( diff --git a/raphtory/src/db/graph/views/filter/model/exploded_edge_filter.rs b/raphtory/src/db/graph/views/filter/model/exploded_edge_filter.rs index 43329f7066..db7d66f528 100644 --- a/raphtory/src/db/graph/views/filter/model/exploded_edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/exploded_edge_filter.rs @@ -8,6 +8,7 @@ use crate::{ exploded_edge_node_filtered_graph::ExplodedEdgeNodeFilteredGraph, model::{ edge_filter::{CompositeEdgeFilter, Endpoint}, + layered_filter::Layered, node_filter::{ builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, CompositeNodeFilter, NodeFilter, @@ -26,6 +27,7 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; +use raphtory_api::core::entities::Layer; use raphtory_core::utils::time::IntoTime; use std::{fmt, fmt::Display, sync::Arc}; @@ -47,6 +49,11 @@ impl ExplodedEdgeFilter { pub fn window(start: S, end: E) -> Windowed { Windowed::from_times(start, end, ExplodedEdgeFilter) } + + #[inline] + pub fn layer>(layer: L) -> Layered { + Layered::from_layers(layer, ExplodedEdgeFilter) + } } impl Wrap for ExplodedEdgeFilter { @@ -262,7 +269,7 @@ pub enum CompositeExplodedEdgeFilter { Dst(CompositeNodeFilter), Property(PropertyFilter), Windowed(Box>), - + Layered(Box>), And( Box, Box, @@ -281,6 +288,7 @@ impl Display for CompositeExplodedEdgeFilter { CompositeExplodedEdgeFilter::Dst(filter) => write!(f, "DST({})", filter), CompositeExplodedEdgeFilter::Property(filter) => write!(f, "{}", filter), CompositeExplodedEdgeFilter::Windowed(filter) => write!(f, "{}", filter), + CompositeExplodedEdgeFilter::Layered(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), @@ -316,6 +324,10 @@ impl CreateFilter for CompositeExplodedEdgeFilter { let dyn_graph: Arc = Arc::new(graph); pw.create_filter(dyn_graph) } + Self::Layered(pw) => { + let dyn_graph: Arc = Arc::new(graph); + pw.create_filter(dyn_graph) + } Self::And(l, r) => { let (l, r) = (*l, *r); // move out, no clone Ok(Arc::new( diff --git a/raphtory/src/db/graph/views/filter/model/layered_filter.rs b/raphtory/src/db/graph/views/filter/model/layered_filter.rs new file mode 100644 index 0000000000..41a77043c3 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -0,0 +1,184 @@ +use crate::{ + db::{ + api::view::internal::GraphView, + graph::views::{ + filter::{ + model::{ + edge_filter::CompositeEdgeFilter, + node_filter::builders::{ + InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, + }, + property_filter::builders::{ + MetadataFilterBuilder, PropertyExprBuilder, PropertyFilterBuilder, + }, + ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, + InternalPropertyFilterBuilder, InternalPropertyFilterFactory, Op, PropertyRef, + TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + }, + CreateFilter, + }, + layer_graph::LayeredGraph, + }, + }, + errors::GraphError, + prelude::{GraphViewOps, LayerOps, PropertyFilter}, +}; +use raphtory_api::core::entities::Layer; +use std::{fmt, fmt::Display}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Layered { + pub layer: Layer, + pub inner: M, +} + +impl Display for Layered { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "LAYER[{:?}]({})", self.layer, self.inner) + } +} + +impl Layered { + #[inline] + pub fn new(layer: Layer, entity: M) -> Self { + Self { + layer, + inner: entity, + } + } + + #[inline] + pub fn from_layers>(layer: L, entity: M) -> Self { + Self::new(layer.into(), entity) + } +} + +impl InternalNodeFilterBuilder for Layered { + type FilterType = T::FilterType; + + fn field_name(&self) -> &'static str { + self.inner.field_name() + } +} + +impl InternalNodeIdFilterBuilder for Layered { + fn field_name(&self) -> &'static str { + self.inner.field_name() + } +} + +impl InternalPropertyFilterBuilder for Layered { + type Filter = Layered; + type ExprBuilder = Layered; + type Marker = T::Marker; + + fn property_ref(&self) -> PropertyRef { + self.inner.property_ref() + } + + fn ops(&self) -> &[Op] { + self.inner.ops() + } + + fn entity(&self) -> Self::Marker { + self.inner.entity() + } + + fn filter(&self, filter: PropertyFilter) -> Self::Filter { + self.wrap(self.inner.filter(filter)) + } + + fn into_expr_builder(&self, builder: PropertyExprBuilder) -> Self::ExprBuilder { + self.wrap(self.inner.into_expr_builder(builder)) + } +} + +impl TryAsCompositeFilter for Layered { + fn try_as_composite_node_filter(&self) -> Result { + let filter = self.inner.try_as_composite_node_filter()?; + let filter = CompositeNodeFilter::Layered(Box::new(self.wrap(filter))); + Ok(filter) + } + + fn try_as_composite_edge_filter(&self) -> Result { + let filter = self.inner.try_as_composite_edge_filter()?; + let filter = CompositeEdgeFilter::Layered(Box::new(self.wrap(filter))); + Ok(filter) + } + + fn try_as_composite_exploded_edge_filter( + &self, + ) -> Result { + let filter = self.inner.try_as_composite_exploded_edge_filter()?; + let filter = CompositeExplodedEdgeFilter::Layered(Box::new(self.wrap(filter))); + Ok(filter) + } +} + +impl CreateFilter for Layered { + type EntityFiltered<'graph, G> + = T::EntityFiltered<'graph, LayeredGraph> + where + G: GraphViewOps<'graph>; + + type NodeFilter<'graph, G> + = T::NodeFilter<'graph, LayeredGraph> + where + G: GraphView + 'graph; + + fn create_filter<'graph, G>( + self, + graph: G, + ) -> Result, GraphError> + where + G: GraphViewOps<'graph>, + { + self.inner.create_filter(graph.layers(self.layer)?) + } + + fn create_node_filter<'graph, G>( + self, + graph: G, + ) -> Result, GraphError> + where + G: GraphView + 'graph, + { + self.inner.create_node_filter(graph.layers(self.layer)?) + } +} + +impl ComposableFilter for Layered {} + +impl Wrap for Layered { + type Wrapped = Layered; + + fn wrap(&self, value: T) -> Self::Wrapped { + Layered::new(self.layer.clone(), value) + } +} + +impl InternalPropertyFilterFactory for Layered { + type Entity = T::Entity; + type PropertyBuilder = Layered; + type MetadataBuilder = Layered; + + fn entity(&self) -> Self::Entity { + self.inner.entity() + } + + fn property_builder( + &self, + builder: PropertyFilterBuilder, + ) -> Self::PropertyBuilder { + self.wrap(self.inner.property_builder(builder)) + } + + fn metadata_builder( + &self, + builder: MetadataFilterBuilder, + ) -> Self::MetadataBuilder { + self.wrap(self.inner.metadata_builder(builder)) + } +} + +impl TemporalPropertyFilterFactory for Layered {} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index e4596dbdf1..5285283ad8 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -40,6 +40,7 @@ pub mod edge_filter; pub mod exploded_edge_filter; pub mod filter; pub mod filter_operator; +pub mod layered_filter; pub mod node_filter; pub mod not_filter; pub mod or_filter; diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs b/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs index 9050a0e568..bd98844f47 100644 --- a/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/node_filter/mod.rs @@ -13,6 +13,7 @@ use crate::{ model::{ edge_filter::CompositeEdgeFilter, filter::Filter, + layered_filter::Layered, node_filter::{ builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, validate::validate, @@ -29,6 +30,7 @@ use crate::{ errors::GraphError, prelude::{GraphViewOps, PropertyFilter}, }; +use raphtory_api::core::entities::Layer; use raphtory_core::utils::time::IntoTime; use raphtory_storage::core_ops::CoreGraphOps; use std::{fmt, fmt::Display, sync::Arc}; @@ -41,21 +43,30 @@ mod validate; pub struct NodeFilter; impl NodeFilter { + #[inline] pub fn id() -> NodeIdFilterBuilder { NodeIdFilterBuilder } + #[inline] pub fn name() -> NodeNameFilterBuilder { NodeNameFilterBuilder } + #[inline] pub fn node_type() -> NodeTypeFilterBuilder { NodeTypeFilterBuilder } + #[inline] pub fn window(start: S, end: E) -> Windowed { Windowed::from_times(start, end, NodeFilter) } + + #[inline] + pub fn layer>(layer: L) -> Layered { + Layered::from_layers(layer, NodeFilter) + } } impl Wrap for NodeFilter { @@ -271,6 +282,7 @@ pub enum CompositeNodeFilter { Node(Filter), Property(PropertyFilter), Windowed(Box>), + Layered(Box>), And(Box, Box), Or(Box, Box), Not(Box), @@ -281,6 +293,7 @@ impl Display for CompositeNodeFilter { match self { CompositeNodeFilter::Property(filter) => write!(f, "{}", filter), CompositeNodeFilter::Windowed(filter) => write!(f, "{}", filter), + CompositeNodeFilter::Layered(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), @@ -321,6 +334,10 @@ impl CreateFilter for CompositeNodeFilter { let dyn_graph: Arc = Arc::new(graph); i.create_node_filter(dyn_graph) } + CompositeNodeFilter::Layered(i) => { + let dyn_graph: Arc = Arc::new(graph); + i.create_node_filter(dyn_graph) + } CompositeNodeFilter::And(l, r) => Ok(Arc::new(AndOp { left: l.clone().create_node_filter(graph.clone())?, right: r.clone().create_node_filter(graph.clone())?, diff --git a/raphtory/src/python/filter/edge_filter_builders.rs b/raphtory/src/python/filter/edge_filter_builders.rs index 6de624964a..f682a7a4e0 100644 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ b/raphtory/src/python/filter/edge_filter_builders.rs @@ -286,4 +286,14 @@ impl PyEdgeFilter { fn window(start: PyTime, end: PyTime) -> PyPropertyFilterFactory { PyPropertyFilterFactory::wrap(EdgeFilter::window(start, end)) } + + #[staticmethod] + fn layer(layer: String) -> PyPropertyFilterFactory { + PyPropertyFilterFactory::wrap(EdgeFilter::layer(layer)) + } + + #[staticmethod] + fn layers(layers: Vec) -> PyPropertyFilterFactory { + PyPropertyFilterFactory::wrap(EdgeFilter::layer(layers)) + } } diff --git a/raphtory/src/python/filter/exploded_edge_filter_builder.rs b/raphtory/src/python/filter/exploded_edge_filter_builder.rs index 5660c35233..4978d07c6f 100644 --- a/raphtory/src/python/filter/exploded_edge_filter_builder.rs +++ b/raphtory/src/python/filter/exploded_edge_filter_builder.rs @@ -40,4 +40,14 @@ impl PyExplodedEdgeFilter { fn window(start: PyTime, end: PyTime) -> PyPropertyFilterFactory { PyPropertyFilterFactory::wrap(ExplodedEdgeFilter::window(start, end)) } + + #[staticmethod] + fn layer(layer: String) -> PyPropertyFilterFactory { + PyPropertyFilterFactory::wrap(ExplodedEdgeFilter::layer(layer)) + } + + #[staticmethod] + fn layers(layers: Vec) -> PyPropertyFilterFactory { + PyPropertyFilterFactory::wrap(ExplodedEdgeFilter::layer(layers)) + } } diff --git a/raphtory/src/python/filter/mod.rs b/raphtory/src/python/filter/mod.rs index ed53fe1c29..de5a2a5e83 100644 --- a/raphtory/src/python/filter/mod.rs +++ b/raphtory/src/python/filter/mod.rs @@ -15,9 +15,9 @@ use pyo3::{ Bound, PyErr, Python, }; -mod create_filter; +pub mod create_filter; pub mod edge_filter_builders; -mod exploded_edge_filter_builder; +pub mod exploded_edge_filter_builder; pub mod filter_expr; pub mod node_filter_builders; pub mod property_filter_builders; diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs index b6586244d9..268dceeda8 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -383,4 +383,14 @@ impl PyNodeFilter { fn window(start: PyTime, end: PyTime) -> PyPropertyFilterFactory { PyPropertyFilterFactory::wrap(NodeFilter::window(start, end)) } + + #[staticmethod] + fn layer(layer: String) -> PyPropertyFilterFactory { + PyPropertyFilterFactory::wrap(NodeFilter::layer(layer)) + } + + #[staticmethod] + fn layers(layers: Vec) -> PyPropertyFilterFactory { + PyPropertyFilterFactory::wrap(NodeFilter::layer(layers)) + } } diff --git a/raphtory/src/search/edge_filter_executor.rs b/raphtory/src/search/edge_filter_executor.rs index 9b087563a3..089bc37ac6 100644 --- a/raphtory/src/search/edge_filter_executor.rs +++ b/raphtory/src/search/edge_filter_executor.rs @@ -13,7 +13,7 @@ use crate::{ }, }, errors::GraphError, - prelude::{GraphViewOps, NodeViewOps, PropertyFilter, TimeOps}, + prelude::{GraphViewOps, LayerOps, NodeViewOps, PropertyFilter, TimeOps}, search::{ collectors::unique_entity_filter_collector::UniqueEntityFilterCollector, fallback_filter_edges, fields, get_reader, graph_index::Index, @@ -256,6 +256,16 @@ impl<'a> EdgeFilterExecutor<'a> { .map(|x| EdgeView::new(graph.clone(), x.edge)) .collect()) } + CompositeEdgeFilter::Layered(filter) => { + let layer = filter.layer.clone(); + let dyn_graph: Arc = Arc::new((*graph).clone()); + let dyn_graph = dyn_graph.layers(layer)?; + let res = self.filter_edges(&dyn_graph, &filter.inner, limit, offset)?; + Ok(res + .into_iter() + .map(|x| EdgeView::new(graph.clone(), x.edge)) + .collect()) + } CompositeEdgeFilter::And(left, right) => { let left_result = self.filter_edges(graph, left, limit, offset)?; let right_result = self.filter_edges(graph, right, limit, offset)?; diff --git a/raphtory/src/search/exploded_edge_filter_executor.rs b/raphtory/src/search/exploded_edge_filter_executor.rs index 2c579da337..d72270d072 100644 --- a/raphtory/src/search/exploded_edge_filter_executor.rs +++ b/raphtory/src/search/exploded_edge_filter_executor.rs @@ -13,7 +13,7 @@ use crate::{ }, }, errors::GraphError, - prelude::{EdgeViewOps, PropertyFilter, TimeOps}, + prelude::{EdgeViewOps, LayerOps, PropertyFilter, TimeOps}, search::{ collectors::{ exploded_edge_property_filter_collector::ExplodedEdgePropertyFilterCollector, @@ -241,6 +241,17 @@ impl<'a> ExplodedEdgeFilterExecutor<'a> { .map(|x| EdgeView::new(graph.clone(), x.edge)) .collect()) } + CompositeExplodedEdgeFilter::Layered(filter) => { + let layer = filter.layer.clone(); + + let dyn_graph: Arc = Arc::new((*graph).clone()); + let dyn_graph = dyn_graph.layers(layer)?; + let res = self.filter_exploded_edges(&dyn_graph, &filter.inner, 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/node_filter_executor.rs b/raphtory/src/search/node_filter_executor.rs index 649f2ce689..059bb247e4 100644 --- a/raphtory/src/search/node_filter_executor.rs +++ b/raphtory/src/search/node_filter_executor.rs @@ -14,7 +14,7 @@ use crate::{ }, }, errors::GraphError, - prelude::{GraphViewOps, PropertyFilter, TimeOps}, + prelude::{GraphViewOps, LayerOps, PropertyFilter, TimeOps}, search::{ collectors::unique_entity_filter_collector::UniqueEntityFilterCollector, fallback_filter_nodes, fields, get_reader, graph_index::Index, @@ -260,6 +260,16 @@ impl<'a> NodeFilterExecutor<'a> { .map(|x| NodeView::new_internal(graph.clone(), x.node)) .collect()) } + CompositeNodeFilter::Layered(filter) => { + let layer = filter.layer.clone(); + let dyn_graph: Arc = Arc::new((*graph).clone()); + let dyn_graph = dyn_graph.layers(layer)?; + let res = self.filter_nodes(&dyn_graph, &filter.inner, 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/tests/test_filters.rs b/raphtory/tests/test_filters.rs index 5f751c6229..8945a43b91 100644 --- a/raphtory/tests/test_filters.rs +++ b/raphtory/tests/test_filters.rs @@ -3686,6 +3686,31 @@ mod test_node_property_filter { TestVariants::All, ); } + + #[test] + fn test_nodes_layer_filter() { + let filter = NodeFilter::layer("_default") + .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, + ); + } } #[cfg(test)] @@ -10192,6 +10217,31 @@ mod test_edge_property_filter { TestVariants::All, ); } + + #[test] + fn test_edges_layer_filter() { + let filter = EdgeFilter::layer("fire_nation") + .property("p2") + .temporal() + .sum() + .ge(2u64); + + let expected_results = vec!["1->2", "3->1"]; + 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, + ); + } } #[cfg(test)]