diff --git a/Cargo.lock b/Cargo.lock index 9fa2368f42..c4277a35a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8014,9 +8014,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" +checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0" dependencies = [ "cc", "libc", @@ -8024,9 +8024,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" +checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd" dependencies = [ "libc", "tikv-jemalloc-sys", diff --git a/Cargo.toml b/Cargo.toml index 994e7a442d..d74be5e16b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,7 @@ async-graphql = { version = "7.2.1", features = ["dynamic-schema"] } async-graphql-poem = "7.2.1" dynamic-graphql = "0.10.2" derive_more = "2.1.1" -tikv-jemallocator = "0.6.1" +tikv-jemallocator = "0.7.0" reqwest = { version = "0.13.3", default-features = false, features = [ "multipart", "json", diff --git a/raphtory-api/src/core/entities/properties/prop/mod.rs b/raphtory-api/src/core/entities/properties/prop/mod.rs index 4f563cdf57..4d712d75ed 100644 --- a/raphtory-api/src/core/entities/properties/prop/mod.rs +++ b/raphtory-api/src/core/entities/properties/prop/mod.rs @@ -12,7 +12,6 @@ mod serde; mod template; pub use arrow::*; - pub use prop_array::*; pub use prop_enum::*; pub use prop_ref_enum::*; diff --git a/raphtory-graphql/schema.graphql b/raphtory-graphql/schema.graphql index acf7bc2581..692957f058 100644 --- a/raphtory-graphql/schema.graphql +++ b/raphtory-graphql/schema.graphql @@ -5614,4 +5614,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 aa7d7796e7..0b870b4d95 100644 --- a/raphtory-graphql/src/model/graph/filtering.rs +++ b/raphtory-graphql/src/model/graph/filtering.rs @@ -9,7 +9,7 @@ use dynamic_graphql::{ use raphtory::{ db::graph::views::filter::model::{ edge_filter::{CompositeEdgeFilter, EdgeFilter}, - filter::{Filter, FilterValue}, + filter::{FieldFilterValue, Filter}, filter_operator::FilterOperator, graph_filter::GraphFilter, is_active_edge_filter::IsActiveEdge, @@ -1028,17 +1028,17 @@ fn require_u64_value(op: &str, v: &Value) -> Result { } } -fn parse_node_id_scalar(op: &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()))), + Value::U64(i) => Ok(FieldFilterValue::ID(GID::U64(*i))), + Value::Str(s) => Ok(FieldFilterValue::ID(GID::Str(s.clone()))), other => Err(GraphError::InvalidGqlFilter(format!( "{op} requires int or str, got {other}" ))), } } -fn parse_node_id_list(op: &str, v: &Value) -> Result { +fn parse_node_id_list(op: &str, v: &Value) -> Result { let Value::List(vs) = v else { return Err(GraphError::InvalidGqlFilter(format!( "{op} requires a list value, got {v}" @@ -1067,10 +1067,10 @@ fn parse_node_id_list(op: &str, v: &Value) -> Result { } } } - Ok(FilterValue::IDSet(Arc::new(set))) + Ok(FieldFilterValue::IDSet(Arc::new(set))) } -fn parse_string_list(op: &str, v: &Value) -> Result { +fn parse_string_list(op: &str, v: &Value) -> Result { let Value::List(vs) = v else { return Err(GraphError::InvalidGqlFilter(format!( "{op} requires a list value, got {v}" @@ -1090,13 +1090,15 @@ fn parse_string_list(op: &str, v: &Value) -> Result { }) .collect::, _>>()?; - Ok(FilterValue::Set(Arc::new(strings.into_iter().collect()))) + Ok(FieldFilterValue::Set(Arc::new( + strings.into_iter().collect(), + ))) } fn translate_node_field_where( field: NodeField, cond: &NodeFieldCondition, -) -> Result<(String, FilterValue, FilterOperator), GraphError> { +) -> Result<(String, FieldFilterValue, FilterOperator), GraphError> { use FilterOperator as FO; use NodeField::*; use NodeFieldCondition::*; @@ -1109,43 +1111,43 @@ fn translate_node_field_where( (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)?)), + FieldFilterValue::ID(GID::U64(require_u64_value(op, v)?)), FO::Gt, ), (NodeId, Ge(v)) => ( field_name, - FilterValue::ID(GID::U64(require_u64_value(op, v)?)), + FieldFilterValue::ID(GID::U64(require_u64_value(op, v)?)), FO::Ge, ), (NodeId, Lt(v)) => ( field_name, - FilterValue::ID(GID::U64(require_u64_value(op, v)?)), + FieldFilterValue::ID(GID::U64(require_u64_value(op, v)?)), FO::Lt, ), (NodeId, Le(v)) => ( field_name, - FilterValue::ID(GID::U64(require_u64_value(op, v)?)), + FieldFilterValue::ID(GID::U64(require_u64_value(op, v)?)), FO::Le, ), (NodeId, StartsWith(v)) => ( field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FieldFilterValue::ID(GID::Str(require_string_value(op, v)?)), FO::StartsWith, ), (NodeId, EndsWith(v)) => ( field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FieldFilterValue::ID(GID::Str(require_string_value(op, v)?)), FO::EndsWith, ), (NodeId, Contains(v)) => ( field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FieldFilterValue::ID(GID::Str(require_string_value(op, v)?)), FO::Contains, ), (NodeId, NotContains(v)) => ( field_name, - FilterValue::ID(GID::Str(require_string_value(op, v)?)), + FieldFilterValue::ID(GID::Str(require_string_value(op, v)?)), FO::NotContains, ), @@ -1154,53 +1156,53 @@ fn translate_node_field_where( (NodeName, Eq(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Eq, ), (NodeName, Ne(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Ne, ), (NodeName, Gt(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Gt, ), (NodeName, Ge(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Ge, ), (NodeName, Lt(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Lt, ), (NodeName, Le(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Le, ), (NodeName, StartsWith(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::StartsWith, ), (NodeName, EndsWith(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::EndsWith, ), (NodeName, Contains(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Contains, ), (NodeName, NotContains(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::NotContains, ), @@ -1209,53 +1211,53 @@ fn translate_node_field_where( (NodeType, Eq(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Eq, ), (NodeType, Ne(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Ne, ), (NodeType, Gt(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Gt, ), (NodeType, Ge(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Ge, ), (NodeType, Lt(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Lt, ), (NodeType, Le(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Le, ), (NodeType, StartsWith(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::StartsWith, ), (NodeType, EndsWith(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::EndsWith, ), (NodeType, Contains(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::Contains, ), (NodeType, NotContains(v)) => ( field_name, - FilterValue::Single(require_string_value(op, v)?), + FieldFilterValue::Single(require_string_value(op, v)?), FO::NotContains, ), @@ -1380,13 +1382,19 @@ impl TryFrom for CompositeNodeFilter { fn try_from(filter: GqlNodeFilter) -> Result { match filter { GqlNodeFilter::Node(node) => { + let field = node.field; let (field_name, field_value, operator) = translate_node_field_where(node.field, &node.where_)?; - Ok(CompositeNodeFilter::Node(Filter { + let filter = Filter { field_name, field_value, operator, - })) + }; + Ok(match field { + NodeField::NodeId => CompositeNodeFilter::Id(filter), + NodeField::NodeName => CompositeNodeFilter::Name(filter), + NodeField::NodeType => CompositeNodeFilter::Type(filter), + }) } GqlNodeFilter::Property(prop) => { let prop_ref = PropertyRef::Property(prop.name.clone()); diff --git a/raphtory-tests/tests/node_property_filter.rs b/raphtory-tests/tests/node_property_filter.rs index e612f3323b..54e2464bb5 100644 --- a/raphtory-tests/tests/node_property_filter.rs +++ b/raphtory-tests/tests/node_property_filter.rs @@ -6,7 +6,7 @@ use raphtory::{ graph::{ graph::assert_edges_equal, views::filter::model::{ - node_filter::{ops::NodeFilterOps, NodeFilter}, + node_filter::{ops::NodeFilterOps, NodeFilter, NodeFilterFactory}, property_filter::ops::PropertyFilterOps, ComposableFilter, PropertyFilterFactory, }, @@ -34,7 +34,8 @@ fn test_node_filter_on_nodes() { g.add_node(2, "David", [("band", "Pink Floyd")], None, None) .unwrap(); - let filter_expr = NodeFilter::name() + let filter_expr = NodeFilter + .name() .eq("John") .and(NodeFilter.property("band").eq("Dead & Company")); let filtered_nodes = g.nodes().filter(filter_expr).unwrap(); diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index 711c78f637..b943e8236c 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -114,7 +114,7 @@ proptest.workspace = true proptest-derive.workspace = true [target.'cfg(target_os = "macos")'.dependencies] -tikv-jemallocator = "0.6.1" +tikv-jemallocator = "0.7.0" [features] default = [] # we can't depend on ourselves but we want to share test-utils diff --git a/raphtory/src/db/api/state/ops/filter.rs b/raphtory/src/db/api/state/ops/filter.rs index e4cadfbb90..7835530a91 100644 --- a/raphtory/src/db/api/state/ops/filter.rs +++ b/raphtory/src/db/api/state/ops/filter.rs @@ -10,7 +10,7 @@ use crate::{ graph::{ create_node_type_filter, views::filter::model::{ - filter::{Filter, FilterValue}, + filter::{FieldFilterValue, Filter}, node_filter::NodeFilter, FilterOperator, }, @@ -96,7 +96,7 @@ impl NodeOp for NodeIdFilterOp { let op = &self.filter.operator; match op { FilterOperator::Eq => match &self.filter.field_value { - FilterValue::ID(id) => { + FieldFilterValue::ID(id) => { let vid = storage.internalise_node(id.as_node_ref()); NodeList::List { elems: vid.into_iter().collect(), @@ -105,7 +105,7 @@ impl NodeOp for NodeIdFilterOp { _ => unreachable!(), }, FilterOperator::IsIn => match &self.filter.field_value { - FilterValue::IDSet(ids) => NodeList::List { + FieldFilterValue::IDSet(ids) => NodeList::List { elems: ids .iter() .filter_map(|id| storage.internalise_node(id.as_node_ref())) @@ -153,7 +153,7 @@ impl NodeOp for NodeNameFilterOp { let op = &self.filter.operator; match op { FilterOperator::Eq => match &self.filter.field_value { - FilterValue::Single(name) => { + FieldFilterValue::Single(name) => { let vid = storage.internalise_node(name.as_node_ref()); NodeList::List { elems: vid.into_iter().collect(), @@ -162,7 +162,7 @@ impl NodeOp for NodeNameFilterOp { _ => unreachable!(), }, FilterOperator::IsIn => match &self.filter.field_value { - FilterValue::Set(names) => NodeList::List { + FieldFilterValue::Set(names) => NodeList::List { elems: names .iter() .filter_map(|name| storage.internalise_node(name.as_node_ref())) diff --git a/raphtory/src/db/api/state/ops/mod.rs b/raphtory/src/db/api/state/ops/mod.rs index 83afa1851e..361dbf2abb 100644 --- a/raphtory/src/db/api/state/ops/mod.rs +++ b/raphtory/src/db/api/state/ops/mod.rs @@ -3,21 +3,31 @@ pub mod history; pub mod node; pub mod properties; -use crate::db::api::{ - state::ops::filter::{AndOp, NotOp, OrOp}, - view::internal::NodeList, +use crate::db::{ + api::{ + state::ops::filter::{AndOp, NotOp, OrOp}, + view::internal::NodeList, + }, + graph::views::filter::model::{node_expr::BinaryCmpNodeOp, BinaryOp, Comparable}, }; pub use history::*; pub use node::*; pub use properties::*; -use raphtory_api::core::entities::VID; +use raphtory_api::core::entities::{properties::prop::PropType, VID}; use raphtory_storage::graph::graph::GraphStorage; use serde::{Deserialize, Serialize}; use std::{fmt::Debug, marker::PhantomData, ops::Deref, sync::Arc}; +// this probably needs the 'graph lifetime to make bin_cmp work with ops that capture the graph pub trait NodeOp: Send + Sync { type Output: Clone + Send + Sync; + /// The output type of this operation used for validation. + /// Returns `PropType::Empty` by default (unknown type). + fn prop_type(&self) -> PropType { + PropType::Empty + } + /// The domain of validity for this node op fn domain(&self, _storage: &GraphStorage) -> NodeList { NodeList::All @@ -41,6 +51,23 @@ pub trait NodeOp: Send + Sync { { Map { op: self, map } } + + /// Override if binary comparison can be optimized + fn bin_cmp( + &self, + op: BinaryOp, + rhs: Arc>, + ) -> Arc> + where + Self: Clone + 'static, + Self::Output: Comparable, + { + Arc::new(BinaryCmpNodeOp { + left: Arc::new(self.clone()), + right: rhs, + op, + }) + } } pub trait IntoArrowNodeOp: NodeOp + Sized { @@ -168,6 +195,10 @@ impl<'a, V: Clone + Send + Sync> NodeOp for Arc + 'a> { self.deref().apply(storage, node) } + fn prop_type(&self) -> PropType { + self.deref().prop_type() + } + fn const_value(&self) -> Option { self.deref().const_value() } diff --git a/raphtory/src/db/api/state/ops/node.rs b/raphtory/src/db/api/state/ops/node.rs index 20e588b2bb..74bc4678f9 100644 --- a/raphtory/src/db/api/state/ops/node.rs +++ b/raphtory/src/db/api/state/ops/node.rs @@ -75,6 +75,7 @@ pub struct Type; pub struct TypeStruct { node_type: Option, } + impl From> for TypeStruct { fn from(node_type: Option) -> Self { TypeStruct { node_type } diff --git a/raphtory/src/db/api/view/filter_ops.rs b/raphtory/src/db/api/view/filter_ops.rs index 76bb5acb4a..4bacd8d468 100644 --- a/raphtory/src/db/api/view/filter_ops.rs +++ b/raphtory/src/db/api/view/filter_ops.rs @@ -10,12 +10,8 @@ pub trait Filter<'graph>: InternalFilter<'graph> { fn filter( &self, filter: F, - ) -> Result< - Self::Filtered>>, - GraphError, - > { - let fg = filter.filter_graph_view(self.base_graph().clone())?; - Ok(self.apply_filter(filter.create_filter(fg)?)) + ) -> Result>, GraphError> { + Ok(self.apply_filter(filter.create_filter(self.base_graph().clone())?)) } } @@ -23,12 +19,8 @@ pub trait NodeSelect<'graph>: InternalNodeSelect<'graph> { fn select( &self, filter: F, - ) -> Result< - Self::IterFiltered>>, - GraphError, - > { - let fg = filter.filter_graph_view(self.iter_graph().clone())?; - Ok(self.apply_iter_filter(filter.create_node_filter(fg)?)) + ) -> Result>, GraphError> { + Ok(self.apply_iter_filter(filter.create_node_filter(self.iter_graph().clone())?)) } } @@ -36,12 +28,8 @@ pub trait EdgeSelect<'graph>: InternalEdgeSelect<'graph> { fn select( &self, filter: F, - ) -> Result< - Self::IterFiltered>>, - GraphError, - > { - let fg = filter.filter_graph_view(self.iter_graph().clone())?; - Ok(self.apply_iter_filter(filter.create_filter(fg)?)) + ) -> Result>, GraphError> { + Ok(self.apply_iter_filter(filter.create_filter(self.iter_graph().clone())?)) } } diff --git a/raphtory/src/db/graph/views/filter/mod.rs b/raphtory/src/db/graph/views/filter/mod.rs index 2f030dedf4..3fd683d565 100644 --- a/raphtory/src/db/graph/views/filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/mod.rs @@ -52,13 +52,6 @@ impl CreateFilter for Unfiltered { ) -> Result, GraphError> { Ok(NodeExistsOp::new(graph)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } pub trait CreateFilter: Sized { @@ -86,11 +79,6 @@ pub trait CreateFilter: Sized { self, graph: G, ) -> Result, GraphError>; - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError>; } impl CreateFilter for T { @@ -128,14 +116,4 @@ impl CreateFilter for T { { Ok(self) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> - where - Self: 'graph, - { - Ok(graph) - } } diff --git a/raphtory/src/db/graph/views/filter/model/and_filter.rs b/raphtory/src/db/graph/views/filter/model/and_filter.rs index daf8d5222e..16ef5b9d09 100644 --- a/raphtory/src/db/graph/views/filter/model/and_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/and_filter.rs @@ -36,19 +36,12 @@ impl ComposableFilter for AndFilter {} impl CreateFilter for AndFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> - = AndFilteredGraph< - G, - L::EntityFiltered<'graph, L::FilteredGraph<'graph, G>>, - R::EntityFiltered<'graph, R::FilteredGraph<'graph, G>>, - > + = AndFilteredGraph, R::EntityFiltered<'graph, G>> where Self: 'graph; type NodeFilter<'graph, G: GraphView + 'graph> - = AndOp< - L::NodeFilter<'graph, L::FilteredGraph<'graph, G>>, - R::NodeFilter<'graph, R::FilteredGraph<'graph, G>>, - > + = AndOp, R::NodeFilter<'graph, G>> where Self: 'graph; @@ -62,10 +55,8 @@ impl CreateFilter for AndFilter { self, graph: G, ) -> Result, GraphError> { - let l = self.left.filter_graph_view(graph.clone())?; - let r = self.right.filter_graph_view(graph.clone())?; - let left = self.left.create_filter(l)?; - let right = self.right.create_filter(r)?; + let left = self.left.create_filter(graph.clone())?; + let right = self.right.create_filter(graph.clone())?; let layer_ids = left.layer_ids().intersect(right.layer_ids()); Ok(AndFilteredGraph { graph, @@ -82,22 +73,10 @@ impl CreateFilter for AndFilter { where Self: 'graph, { - let l = self.left.filter_graph_view(graph.clone())?; - let r = self.right.filter_graph_view(graph.clone())?; - let left = self.left.create_node_filter(l)?; - let right = self.right.create_node_filter(r)?; + let left = self.left.create_node_filter(graph.clone())?; + let right = self.right.create_node_filter(graph)?; Ok(left.and(right)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> - where - Self: 'graph, - { - Ok(graph) - } } impl TryAsCompositeFilter for AndFilter { 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 7e8883e69e..b4a1152532 100644 --- a/raphtory/src/db/graph/views/filter/model/edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/edge_filter.rs @@ -1,7 +1,7 @@ use crate::{ db::{ api::{ - state::ops::NotANodeFilter, + state::ops::{Id, Name, NotANodeFilter, Type}, view::{internal::GraphView, BoxableGraphView}, }, graph::views::filter::{ @@ -15,23 +15,13 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_filter::{ - builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder, - }, - CompositeNodeFilter, NodeFilter, - }, - property_filter::{ - builders::{ - MetadataFilterBuilder, PropertyExprBuilderInput, PropertyFilterBuilder, - }, - Op, PropertyFilter, PropertyFilterInput, PropertyRef, + builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, + property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, AndFilter, CombinedFilter, ComposableFilter, EdgeViewFilterOps, EntityMarker, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, - NotFilter, OrFilter, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + InternalViewWrapOps, NotFilter, OrFilter, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -39,7 +29,7 @@ use crate::{ errors::GraphError, prelude::GraphViewOps, }; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; use std::{fmt, fmt::Display, sync::Arc}; // User facing entry for building edge filters. @@ -80,24 +70,6 @@ impl InternalViewWrapOps for EdgeFilter { } } -impl InternalPropertyFilterFactory for EdgeFilter { - type Entity = EdgeFilter; - type PropertyBuilder = PropertyFilterBuilder; - type MetadataBuilder = MetadataFilterBuilder; - - fn entity(&self) -> Self::Entity { - EdgeFilter - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - PropertyFilterBuilder(property, self.entity()) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, self.entity()) - } -} - impl EdgeViewFilterOps for EdgeFilter { type Output = T; @@ -156,18 +128,18 @@ impl EdgeEndpointWrapper { impl EdgeEndpointWrapper { #[inline] - pub fn id(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::id(), self.endpoint) + pub fn id(&self) -> EdgeEndpointWrapper { + EdgeEndpointWrapper::new(Id, self.endpoint) } #[inline] - pub fn name(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::name(), self.endpoint) + pub fn name(&self) -> EdgeEndpointWrapper { + EdgeEndpointWrapper::new(Name, self.endpoint) } #[inline] - pub fn node_type(&self) -> EdgeEndpointWrapper { - EdgeEndpointWrapper::new(NodeFilter::node_type(), self.endpoint) + pub fn node_type(&self) -> EdgeEndpointWrapper { + EdgeEndpointWrapper::new(Type, self.endpoint) } } @@ -184,12 +156,6 @@ impl Wrap for EdgeEndpointWrapper { impl ComposableFilter for EdgeEndpointWrapper where T: TryAsCompositeFilter + Clone {} -impl InternalNodeIdFilterBuilder for EdgeEndpointWrapper { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - impl InternalNodeFilterBuilder for EdgeEndpointWrapper { type FilterType = T::FilterType; fn field_name(&self) -> &'static str { @@ -197,55 +163,6 @@ impl InternalNodeFilterBuilder for EdgeEndpointWra } } -impl InternalPropertyFilterBuilder for EdgeEndpointWrapper { - type Filter = EdgeEndpointWrapper; - type ExprBuilder = EdgeEndpointWrapper; - type Marker = T::Marker; - - #[inline] - fn property_ref(&self) -> PropertyRef { - self.inner.property_ref() - } - - #[inline] - fn ops(&self) -> &[Op] { - self.inner.ops() - } - - #[inline] - fn entity(&self) -> Self::Marker { - self.inner.entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - -impl InternalPropertyFilterFactory for EdgeEndpointWrapper { - type Entity = T::Entity; - type PropertyBuilder = EdgeEndpointWrapper; - type MetadataBuilder = EdgeEndpointWrapper; - - fn entity(&self) -> Self::Entity { - self.inner.entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.wrap(self.inner.metadata_builder(property)) - } -} - -impl TemporalPropertyFilterFactory for EdgeEndpointWrapper {} - impl CreateFilter for EdgeEndpointWrapper { type EntityFiltered<'graph, G> = EdgeNodeFilteredGraph> @@ -260,7 +177,7 @@ impl CreateFilter for EdgeEndpointWrapper G: GraphView + 'graph; type FilteredGraph<'graph, G> - = T::FilteredGraph<'graph, G> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -279,13 +196,6 @@ impl CreateFilter for EdgeEndpointWrapper ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - self.inner.filter_graph_view(graph) - } } impl TryAsCompositeFilter for EdgeEndpointWrapper { @@ -435,50 +345,6 @@ impl CreateFilter for CompositeEdgeFilter { ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - match self.clone() { - CompositeEdgeFilter::Src(filter) => { - let wrapped = EdgeEndpointWrapper::new(filter, Endpoint::Src); - let filtered_graph = wrapped.filter_graph_view(graph)?; - Ok(Arc::new(filtered_graph)) - } - CompositeEdgeFilter::Dst(filter) => { - let wrapped = EdgeEndpointWrapper::new(filter, Endpoint::Dst); - let filtered_graph = wrapped.filter_graph_view(graph)?; - Ok(Arc::new(filtered_graph)) - } - CompositeEdgeFilter::Property(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::Windowed(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::Latest(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::SnapshotAt(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::SnapshotLatest(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::IsActiveEdge(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::IsValidEdge(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::IsDeletedEdge(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::IsSelfLoopEdge(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::Layered(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeEdgeFilter::And(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - AndFilter { left: l, right: r }.filter_graph_view(graph)?, - )) - } - CompositeEdgeFilter::Or(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - OrFilter { left: l, right: r }.filter_graph_view(graph)?, - )) - } - CompositeEdgeFilter::Not(f) => { - let base = *f; - Ok(Arc::new(NotFilter(base).filter_graph_view(graph)?)) - } - } - } } impl TryAsCompositeFilter for CompositeEdgeFilter { 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 c6a190d463..73cec17d8e 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 @@ -15,20 +15,13 @@ use crate::{ latest_filter::Latest, layered_filter::Layered, node_filter::{ - builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, - CompositeNodeFilter, NodeFilter, - }, - property_filter::{ - builders::{ - MetadataFilterBuilder, PropertyExprBuilderInput, PropertyFilterBuilder, - }, - Op, PropertyFilter, PropertyFilterInput, PropertyRef, + builders::InternalNodeFilterBuilder, CompositeNodeFilter, NodeFilter, }, + property_filter::PropertyFilter, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, - AndFilter, CombinedFilter, EdgeViewFilterOps, EntityMarker, - InternalPropertyFilterBuilder, InternalPropertyFilterFactory, InternalViewWrapOps, - NotFilter, OrFilter, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + AndFilter, CombinedFilter, EdgeViewFilterOps, EntityMarker, InternalViewWrapOps, + NotFilter, OrFilter, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -76,24 +69,6 @@ impl InternalViewWrapOps for ExplodedEdgeFilter { } } -impl InternalPropertyFilterFactory for ExplodedEdgeFilter { - type Entity = ExplodedEdgeFilter; - type PropertyBuilder = PropertyFilterBuilder; - type MetadataBuilder = MetadataFilterBuilder; - - fn entity(&self) -> Self::Entity { - ExplodedEdgeFilter - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - PropertyFilterBuilder(property, self.entity()) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, self.entity()) - } -} - impl EdgeViewFilterOps for ExplodedEdgeFilter { type Output = T; @@ -152,14 +127,6 @@ impl Wrap for ExplodedEdgeEndpointWrapper { } } -impl InternalNodeIdFilterBuilder - for ExplodedEdgeEndpointWrapper -{ - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - impl InternalNodeFilterBuilder for ExplodedEdgeEndpointWrapper { type FilterType = T::FilterType; @@ -168,62 +135,6 @@ impl InternalNodeFilterBuilder for ExplodedEdgeEnd } } -impl InternalPropertyFilterBuilder - for ExplodedEdgeEndpointWrapper -{ - type Filter = ExplodedEdgeEndpointWrapper; - type ExprBuilder = ExplodedEdgeEndpointWrapper; - type Marker = T::Marker; - - #[inline] - fn property_ref(&self) -> PropertyRef { - self.inner.property_ref() - } - - #[inline] - fn ops(&self) -> &[Op] { - self.inner.ops() - } - - #[inline] - fn entity(&self) -> Self::Marker { - self.inner.entity() - } - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - -impl InternalPropertyFilterFactory - for ExplodedEdgeEndpointWrapper -{ - type Entity = T::Entity; - type PropertyBuilder = ExplodedEdgeEndpointWrapper; - type MetadataBuilder = ExplodedEdgeEndpointWrapper; - - fn entity(&self) -> Self::Entity { - self.inner.entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.wrap(self.inner.metadata_builder(property)) - } -} - -impl TemporalPropertyFilterFactory - for ExplodedEdgeEndpointWrapper -{ -} - impl CreateFilter for ExplodedEdgeEndpointWrapper { type EntityFiltered<'graph, G: GraphViewOps<'graph>> = ExplodedEdgeNodeFilteredGraph> @@ -237,7 +148,7 @@ impl CreateFilter for ExplodedEdgeEndpointWra Self: 'graph, G: GraphView + 'graph; type FilteredGraph<'graph, G> - = T::FilteredGraph<'graph, G> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -263,13 +174,6 @@ impl CreateFilter for ExplodedEdgeEndpointWra ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - self.inner.filter_graph_view(graph) - } } impl TryAsCompositeFilter for ExplodedEdgeEndpointWrapper @@ -420,50 +324,6 @@ impl CreateFilter for CompositeExplodedEdgeFilter { ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - match self.clone() { - Self::Src(filter) => { - let wrapped = ExplodedEdgeEndpointWrapper::new(filter, Endpoint::Src); - let filtered_graph = wrapped.filter_graph_view(graph)?; - Ok(Arc::new(filtered_graph)) - } - Self::Dst(filter) => { - let wrapped = ExplodedEdgeEndpointWrapper::new(filter, Endpoint::Dst); - let filtered_graph = wrapped.filter_graph_view(graph)?; - Ok(Arc::new(filtered_graph)) - } - Self::Property(p) => Ok(Arc::new(p.filter_graph_view(graph)?)), - Self::Windowed(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::Latest(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::SnapshotAt(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::SnapshotLatest(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::Layered(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::IsActiveEdge(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::IsValidEdge(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::IsDeletedEdge(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::IsSelfLoopEdge(pw) => Ok(Arc::new(pw.filter_graph_view(graph)?)), - Self::And(l, r) => { - let (l, r) = (*l, *r); // move out, no clone - Ok(Arc::new( - AndFilter { left: l, right: r }.filter_graph_view(graph)?, - )) - } - Self::Or(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - OrFilter { left: l, right: r }.filter_graph_view(graph)?, - )) - } - Self::Not(f) => { - let base = *f; - Ok(Arc::new(NotFilter(base).filter_graph_view(graph)?)) - } - } - } } impl TryAsCompositeFilter for CompositeExplodedEdgeFilter { diff --git a/raphtory/src/db/graph/views/filter/model/filter.rs b/raphtory/src/db/graph/views/filter/model/filter.rs index 0b9fdebbe1..022a64222c 100644 --- a/raphtory/src/db/graph/views/filter/model/filter.rs +++ b/raphtory/src/db/graph/views/filter/model/filter.rs @@ -2,8 +2,9 @@ use crate::db::graph::views::filter::model::FilterOperator; use raphtory_api::core::entities::{GidRef, GID}; use std::{collections::HashSet, fmt, fmt::Display, sync::Arc}; +/// Filter value for field-based filters (node name, node type, node/edge id). #[derive(Debug, Clone, PartialEq, Eq)] -pub enum FilterValue { +pub enum FieldFilterValue { Single(String), Set(Arc>), ID(GID), @@ -13,17 +14,17 @@ pub enum FilterValue { #[derive(Debug, Clone, PartialEq, Eq)] pub struct Filter { pub field_name: String, - pub field_value: FilterValue, + pub field_value: FieldFilterValue, pub operator: FilterOperator, } impl Display for Filter { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match &self.field_value { - FilterValue::Single(value) => { + FieldFilterValue::Single(value) => { write!(f, "{} {} {}", self.field_name, self.operator, value) } - FilterValue::Set(values) => { + FieldFilterValue::Set(values) => { let mut sorted: Vec<&String> = values.iter().collect(); sorted.sort(); let values_str = sorted @@ -33,10 +34,10 @@ impl Display for Filter { .join(", "); write!(f, "{} {} [{}]", self.field_name, self.operator, values_str) } - FilterValue::ID(id) => { + FieldFilterValue::ID(id) => { write!(f, "{} {} {}", self.field_name, self.operator, id) } - FilterValue::IDSet(values) => { + FieldFilterValue::IDSet(values) => { let mut sorted: Vec<&GID> = values.iter().collect(); sorted.sort(); let values_str = sorted @@ -54,7 +55,7 @@ impl Filter { pub fn eq(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Single(field_value.into()), + field_value: FieldFilterValue::Single(field_value.into()), operator: FilterOperator::Eq, } } @@ -62,7 +63,7 @@ impl Filter { pub fn ne(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Single(field_value.into()), + field_value: FieldFilterValue::Single(field_value.into()), operator: FilterOperator::Ne, } } @@ -73,7 +74,7 @@ impl Filter { ) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Set(Arc::new( + field_value: FieldFilterValue::Set(Arc::new( field_values.into_iter().map(|s| s.into()).collect(), )), operator: FilterOperator::IsIn, @@ -91,7 +92,7 @@ impl Filter { ) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Set(Arc::new( + field_value: FieldFilterValue::Set(Arc::new( field_values.into_iter().map(|s| s.into()).collect(), )), operator: FilterOperator::IsNotIn, @@ -101,7 +102,7 @@ impl Filter { pub fn starts_with(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Single(field_value.into()), + field_value: FieldFilterValue::Single(field_value.into()), operator: FilterOperator::StartsWith, } } @@ -109,7 +110,7 @@ impl Filter { pub fn ends_with(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Single(field_value.into()), + field_value: FieldFilterValue::Single(field_value.into()), operator: FilterOperator::EndsWith, } } @@ -117,7 +118,7 @@ impl Filter { pub fn contains(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Single(field_value.into()), + field_value: FieldFilterValue::Single(field_value.into()), operator: FilterOperator::Contains, } } @@ -125,7 +126,7 @@ impl Filter { pub fn not_contains(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Single(field_value.into()), + field_value: FieldFilterValue::Single(field_value.into()), operator: FilterOperator::NotContains, } } @@ -148,7 +149,7 @@ impl Filter { ) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::Single(field_value.into()), + field_value: FieldFilterValue::Single(field_value.into()), operator: FilterOperator::FuzzySearch { levenshtein_distance, prefix_match, @@ -159,7 +160,7 @@ impl Filter { pub fn eq_id(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::ID(field_value.into()), + field_value: FieldFilterValue::ID(field_value.into()), operator: FilterOperator::Eq, } } @@ -167,7 +168,7 @@ impl Filter { pub fn ne_id(field_name: impl Into, field_value: impl Into) -> Self { Self { field_name: field_name.into(), - field_value: FilterValue::ID(field_value.into()), + field_value: FieldFilterValue::ID(field_value.into()), operator: FilterOperator::Ne, } } @@ -180,7 +181,7 @@ impl Filter { let set: HashSet = field_values.into_iter().map(|x| x.into()).collect(); Self { field_name: field_name.into(), - field_value: FilterValue::IDSet(Arc::new(set)), + field_value: FieldFilterValue::IDSet(Arc::new(set)), operator: FilterOperator::IsIn, } } @@ -193,7 +194,7 @@ impl Filter { let set: HashSet = field_values.into_iter().map(|x| x.into()).collect(); Self { field_name: field_name.into(), - field_value: FilterValue::IDSet(Arc::new(set)), + field_value: FieldFilterValue::IDSet(Arc::new(set)), operator: FilterOperator::IsNotIn, } } @@ -201,7 +202,7 @@ impl Filter { pub fn lt>(field_name: impl Into, field_value: V) -> Self { Filter { field_name: field_name.into(), - field_value: FilterValue::ID(field_value.into()), + field_value: FieldFilterValue::ID(field_value.into()), operator: FilterOperator::Lt, } .into() @@ -210,7 +211,7 @@ impl Filter { pub fn le>(field_name: impl Into, field_value: V) -> Self { Filter { field_name: field_name.into(), - field_value: FilterValue::ID(field_value.into()), + field_value: FieldFilterValue::ID(field_value.into()), operator: FilterOperator::Le, } .into() @@ -219,7 +220,7 @@ impl Filter { pub fn gt>(field_name: impl Into, field_value: V) -> Self { Filter { field_name: field_name.into(), - field_value: FilterValue::ID(field_value.into()), + field_value: FieldFilterValue::ID(field_value.into()), operator: FilterOperator::Gt, } .into() @@ -228,7 +229,7 @@ impl Filter { pub fn ge>(field_name: impl Into, field_value: V) -> Self { Filter { field_name: field_name.into(), - field_value: FilterValue::ID(field_value.into()), + field_value: FieldFilterValue::ID(field_value.into()), operator: FilterOperator::Ge, } .into() 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 7c3e0e33be..0f32aace4c 100644 --- a/raphtory/src/db/graph/views/filter/model/filter_operator.rs +++ b/raphtory/src/db/graph/views/filter/model/filter_operator.rs @@ -1,10 +1,260 @@ use crate::db::graph::views::filter::model::{ - filter::FilterValue, property_filter::PropertyFilterValue, + filter::FieldFilterValue, filter_value::FilterValue, property_filter::PropertyFilterValue, +}; +use raphtory_api::core::{ + entities::{properties::prop::Prop, GidRef, GID}, + storage::arc_str::ArcStr, }; -use raphtory_api::core::entities::{properties::prop::Prop, GidRef, GID}; use std::{collections::HashSet, fmt, fmt::Display, ops::Deref}; use strsim::levenshtein; +// ───────────────────────────────────────────────────────────────────────────── +// Comparable — type-driven ordering/equality comparison for BinaryCmpNodeOp +// ───────────────────────────────────────────────────────────────────────────── + +pub trait Comparable: Clone + Send + Sync + 'static { + fn binary_cmp(op: &BinaryOp, left: &Self, right: &Self) -> bool; +} + +impl Comparable for usize { + fn binary_cmp(op: &BinaryOp, left: &usize, right: &usize) -> bool { + match op { + BinaryOp::Eq => left == right, + BinaryOp::Ne => left != right, + BinaryOp::Lt => left < right, + BinaryOp::Le => left <= right, + BinaryOp::Gt => left > right, + BinaryOp::Ge => left >= right, + } + } +} + +macro_rules! impl_comparable_str { + ($ty:ty) => { + impl Comparable for $ty { + fn binary_cmp(op: &BinaryOp, left: &$ty, right: &$ty) -> bool { + let (l, r): (&str, &str) = (left, right); + match op { + BinaryOp::Eq => l == r, + BinaryOp::Ne => l != r, + BinaryOp::Lt => l < r, + BinaryOp::Le => l <= r, + BinaryOp::Gt => l > r, + BinaryOp::Ge => l >= r, + } + } + } + }; +} + +impl_comparable_str!(String); +impl_comparable_str!(ArcStr); +impl_comparable_str!(&'static str); + +impl Comparable for Prop { + fn binary_cmp(op: &BinaryOp, left: &Prop, right: &Prop) -> bool { + use std::cmp::Ordering::*; + match op { + BinaryOp::Eq => left == right, + BinaryOp::Ne => left != right, + BinaryOp::Lt => left.partial_cmp(right).map(|o| o == Less).unwrap_or(false), + BinaryOp::Le => left + .partial_cmp(right) + .map(|o| o != Greater) + .unwrap_or(false), + BinaryOp::Gt => left + .partial_cmp(right) + .map(|o| o == Greater) + .unwrap_or(false), + BinaryOp::Ge => left.partial_cmp(right).map(|o| o != Less).unwrap_or(false), + } + } +} + +impl Comparable for GID { + fn binary_cmp(op: &BinaryOp, left: &GID, right: &GID) -> bool { + match (left, right) { + (GID::U64(l), GID::U64(r)) => match op { + BinaryOp::Eq => l == r, + BinaryOp::Ne => l != r, + BinaryOp::Lt => l < r, + BinaryOp::Le => l <= r, + BinaryOp::Gt => l > r, + BinaryOp::Ge => l >= r, + }, + (GID::Str(l), GID::Str(r)) => String::binary_cmp(op, l, r), + _ => matches!(op, BinaryOp::Ne), + } + } +} + +impl Comparable for Option { + fn binary_cmp(op: &BinaryOp, left: &Option, right: &Option) -> bool { + match (left, right) { + (Some(l), Some(r)) => T::binary_cmp(op, l, r), + (None, None) => matches!(op, BinaryOp::Eq), + (None, Some(_)) | (Some(_), None) => matches!(op, BinaryOp::Ne), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// StringComparable — type-driven string comparison for StringOpNodeOp +// ───────────────────────────────────────────────────────────────────────────── + +pub trait StringComparable: Clone + Send + Sync + 'static { + fn string_cmp(op: &StringOp, left: &Self, right: &Self) -> bool; +} + +macro_rules! impl_string_comparable_str { + ($ty:ty) => { + impl StringComparable for $ty { + fn string_cmp(op: &StringOp, left: &$ty, right: &$ty) -> bool { + let (l, r): (&str, &str) = (left, right); + match op { + StringOp::StartsWith => l.starts_with(r), + StringOp::EndsWith => l.ends_with(r), + StringOp::Contains => l.contains(r), + StringOp::NotContains => !l.contains(r), + StringOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => { + let l = l.to_lowercase(); + let r = r.to_lowercase(); + let lev = levenshtein(&r, &l) <= *levenshtein_distance; + let prefix = *prefix_match && l.as_str().starts_with(r.as_str()); + lev || prefix + } + } + } + } + }; +} + +impl_string_comparable_str!(String); +impl_string_comparable_str!(ArcStr); +impl_string_comparable_str!(&'static str); + +impl StringComparable for Prop { + fn string_cmp(op: &StringOp, left: &Prop, right: &Prop) -> bool { + match (left, right) { + (Prop::Str(l), Prop::Str(r)) => ArcStr::string_cmp(op, l, r), + _ => false, + } + } +} + +impl StringComparable for GID { + fn string_cmp(op: &StringOp, left: &GID, right: &GID) -> bool { + match (left, right) { + (GID::Str(l), GID::Str(r)) => String::string_cmp(op, l, r), + _ => false, + } + } +} + +impl StringComparable for Option { + fn string_cmp(op: &StringOp, left: &Option, right: &Option) -> bool { + match (left, right) { + (Some(l), Some(r)) => T::string_cmp(op, l, r), + _ => false, + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Focused operator enums for the NodeExpr expression system +// ───────────────────────────────────────────────────────────────────────────── + +/// Ordering and equality operators used by `BinaryCmpNodeFilter`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BinaryOp { + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + +impl Display for BinaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BinaryOp::Eq => write!(f, "=="), + BinaryOp::Ne => write!(f, "!="), + BinaryOp::Lt => write!(f, "<"), + BinaryOp::Le => write!(f, "<="), + BinaryOp::Gt => write!(f, ">"), + BinaryOp::Ge => write!(f, ">="), + } + } +} + +/// String-only operators used by `StringNodeFilter`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StringOp { + StartsWith, + EndsWith, + Contains, + NotContains, + FuzzySearch { + levenshtein_distance: usize, + prefix_match: bool, + }, +} + +impl Display for StringOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + StringOp::StartsWith => write!(f, "STARTS_WITH"), + StringOp::EndsWith => write!(f, "ENDS_WITH"), + StringOp::Contains => write!(f, "CONTAINS"), + StringOp::NotContains => write!(f, "NOT_CONTAINS"), + StringOp::FuzzySearch { + levenshtein_distance, + prefix_match, + } => write!(f, "FUZZY_SEARCH({},{})", levenshtein_distance, prefix_match), + } + } +} + +/// Unary presence operators used by `UnaryNodeFilter`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum UnaryOp { + IsSome, + IsNone, +} + +impl Display for UnaryOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + UnaryOp::IsSome => write!(f, "IS_SOME"), + UnaryOp::IsNone => write!(f, "IS_NONE"), + } + } +} + +/// Set membership operators used by `SetNodeFilter`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SetOp { + IsIn, + IsNotIn, +} + +impl Display for SetOp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + SetOp::IsIn => write!(f, "IS_IN"), + SetOp::IsNotIn => write!(f, "IS_NOT_IN"), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// FilterOperator — kept for the PropertyFilter system +// ───────────────────────────────────────────────────────────────────────────── + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FilterOperator { Eq, @@ -125,7 +375,7 @@ impl FilterOperator { pub fn apply_to_property(&self, left: &PropertyFilterValue, right: Option<&Prop>) -> bool { use std::cmp::Ordering::*; use FilterOperator::*; - use PropertyFilterValue::*; + use FilterValue::{None as FNone, Set as FSet, Single as FSingle}; let cmp = |op: &FilterOperator, r: &Prop, l: &Prop| -> bool { match op { @@ -140,13 +390,13 @@ impl FilterOperator { }; match left { - None => match self { + FNone => match self { IsSome => right.is_some(), IsNone => right.is_none(), - _ => false, // Missing RHS never matches for other ops + _ => false, }, - Single(lv) => match self { + FSingle(lv) => match self { Eq | Ne | Lt | Le | Gt | Ge => { if let Some(r) = right { cmp(self, r, lv) @@ -190,7 +440,7 @@ impl FilterOperator { } => { if let (Some(Prop::Str(rs)), Prop::Str(ls)) = (right, lv) { let f = self.fuzzy_search(*levenshtein_distance, *prefix_match); - f(ls, rs) + f(ls.deref(), rs.deref()) } else { false } @@ -199,29 +449,17 @@ impl FilterOperator { IsIn | IsNotIn | IsSome | IsNone => false, }, - Set(set) => match self { - IsIn => { - if let Some(r) = right { - set.contains(r) - } else { - false - } - } - IsNotIn => { - if let Some(r) = right { - !set.contains(r) - } else { - false - } - } + FSet(set) => match self { + IsIn => right.map(|r| set.contains(r)).unwrap_or(false), + IsNotIn => right.map(|r| !set.contains(r)).unwrap_or(false), _ => false, }, } } - pub fn apply(&self, left: &FilterValue, right: Option<&str>) -> bool { + pub fn apply(&self, left: &FieldFilterValue, right: Option<&str>) -> bool { match left { - FilterValue::Single(l) => match self { + FieldFilterValue::Single(l) => match self { FilterOperator::Eq | FilterOperator::Ne => match right { Some(r) => self.operation()(r, l), None => matches!(self, FilterOperator::Ne), @@ -240,7 +478,7 @@ impl FilterOperator { _ => unreachable!(), }, - FilterValue::Set(l) => match self { + FieldFilterValue::Set(l) => match self { FilterOperator::IsIn | FilterOperator::IsNotIn => match right { Some(r) => self.collection_operation()(l, &r.to_string()), None => matches!(self, FilterOperator::IsNotIn), @@ -248,13 +486,13 @@ impl FilterOperator { _ => unreachable!(), }, - FilterValue::ID(_) | FilterValue::IDSet(_) => unreachable!(), + FieldFilterValue::ID(_) | FieldFilterValue::IDSet(_) => unreachable!(), } } - pub fn apply_id(&self, left: &FilterValue, right: GidRef<'_>) -> bool { + pub fn apply_id(&self, left: &FieldFilterValue, right: GidRef<'_>) -> bool { match left { - FilterValue::ID(GID::U64(l)) => match right { + FieldFilterValue::ID(GID::U64(l)) => match right { GidRef::U64(r) => match self { FilterOperator::Eq | FilterOperator::Ne @@ -267,7 +505,7 @@ impl FilterOperator { GidRef::Str(_) => false, }, - FilterValue::ID(GID::Str(ls)) | FilterValue::Single(ls) => match right { + FieldFilterValue::ID(GID::Str(ls)) | FieldFilterValue::Single(ls) => match right { GidRef::Str(rs) => match self { FilterOperator::Eq | FilterOperator::Ne => self.operation()(&rs, &ls.as_str()), FilterOperator::StartsWith => rs.starts_with(ls), @@ -286,7 +524,7 @@ impl FilterOperator { GidRef::U64(_) => false, }, - FilterValue::IDSet(set) => match right { + FieldFilterValue::IDSet(set) => match right { GidRef::U64(r) => match self { FilterOperator::IsIn => set.contains(&GID::U64(r)), FilterOperator::IsNotIn => !set.contains(&GID::U64(r)), @@ -299,7 +537,7 @@ impl FilterOperator { }, }, - FilterValue::Set(set) => match right { + FieldFilterValue::Set(set) => match right { GidRef::U64(_) => false, GidRef::Str(s) => match self { FilterOperator::IsIn => set.contains(s), @@ -309,4 +547,71 @@ impl FilterOperator { }, } } + + /// Compare two optional values symmetrically. + /// + /// Used by `BinaryCmpNodeFilter` where both sides are expressions that may return `None`. + /// Supports Eq, Ne, Lt, Le, Gt, Ge. All other operators return `false`. + pub fn compare_values(&self, left: Option<&T>, right: Option<&T>) -> bool + where + T: PartialEq + PartialOrd, + { + use std::cmp::Ordering::*; + use FilterOperator::*; + + match (left, right) { + (Some(l), Some(r)) => match self { + Eq => l == r, + Ne => l != r, + Lt => l.partial_cmp(r).map(|o| o == Less).unwrap_or(false), + Le => l.partial_cmp(r).map(|o| o != Greater).unwrap_or(false), + Gt => l.partial_cmp(r).map(|o| o == Greater).unwrap_or(false), + Ge => l.partial_cmp(r).map(|o| o != Less).unwrap_or(false), + _ => false, + }, + // both absent → treat as equal + (None, None) => matches!(self, Eq), + // one absent, one present → not equal + (None, Some(_)) | (Some(_), None) => matches!(self, Ne), + } + } + + /// Apply a filter against any ordered/hashable value type. + /// + /// Supports: Eq, Ne, Lt, Le, Gt, Ge, IsIn, IsNotIn, IsSome, IsNone. + /// String and fuzzy operators return `false` — use `apply_to_property` for those. + pub fn apply_value(&self, left: &FilterValue, right: Option<&T>) -> bool + where + T: PartialEq + PartialOrd + Eq + std::hash::Hash, + { + use std::cmp::Ordering::*; + use FilterOperator::*; + + match left { + FilterValue::None => match self { + IsSome => right.is_some(), + IsNone => right.is_none(), + _ => false, + }, + FilterValue::Single(lv) => { + let Some(r) = right else { + return matches!(self, Ne); + }; + match self { + Eq => r == lv, + Ne => r != lv, + Lt => r.partial_cmp(lv).map(|o| o == Less).unwrap_or(false), + Le => r.partial_cmp(lv).map(|o| o != Greater).unwrap_or(false), + Gt => r.partial_cmp(lv).map(|o| o == Greater).unwrap_or(false), + Ge => r.partial_cmp(lv).map(|o| o != Less).unwrap_or(false), + _ => false, + } + } + FilterValue::Set(set) => match self { + IsIn => right.map(|r| set.contains(r)).unwrap_or(false), + IsNotIn => right.map(|r| !set.contains(r)).unwrap_or(false), + _ => false, + }, + } + } } diff --git a/raphtory/src/db/graph/views/filter/model/filter_value.rs b/raphtory/src/db/graph/views/filter/model/filter_value.rs new file mode 100644 index 0000000000..ef778604c1 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/filter_value.rs @@ -0,0 +1,50 @@ +use std::{collections::HashSet, fmt, hash::Hash, sync::Arc}; + +/// A generic filter value container used by both property and attribute filters. +/// +/// `T` is the value type being compared against (e.g. `Prop` for stored properties, +/// `usize` for degree, etc.). +#[derive(Debug, Clone)] +pub enum FilterValue { + /// Sentinel for `IS_SOME` / `IS_NONE` operators — no RHS value. + None, + /// Single value for equality/ordering comparisons. + Single(T), + /// Set of values for `IS_IN` / `IS_NOT_IN` comparisons. + Set(Arc>), +} + +impl PartialEq for FilterValue { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (FilterValue::None, FilterValue::None) => true, + (FilterValue::Single(a), FilterValue::Single(b)) => a == b, + (FilterValue::Set(a), FilterValue::Set(b)) => a == b, + _ => false, + } + } +} + +impl Eq for FilterValue {} + +impl fmt::Display for FilterValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FilterValue::None => write!(f, ""), + FilterValue::Single(v) => write!(f, "{}", v), + FilterValue::Set(vs) => { + let mut sorted: Vec<&T> = vs.iter().collect(); + sorted.sort(); + write!( + f, + "[{}]", + sorted + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(", ") + ) + } + } + } +} diff --git a/raphtory/src/db/graph/views/filter/model/graph_filter.rs b/raphtory/src/db/graph/views/filter/model/graph_filter.rs index 417fd1baa8..2940454e89 100644 --- a/raphtory/src/db/graph/views/filter/model/graph_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/graph_filter.rs @@ -64,13 +64,6 @@ impl CreateFilter for GraphFilter { ) -> Result, GraphError> { Ok(NodeExistsOp::new(graph)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for GraphFilter { diff --git a/raphtory/src/db/graph/views/filter/model/is_active_edge_filter.rs b/raphtory/src/db/graph/views/filter/model/is_active_edge_filter.rs index 8f858be2dc..db5f0bdaae 100644 --- a/raphtory/src/db/graph/views/filter/model/is_active_edge_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/is_active_edge_filter.rs @@ -58,13 +58,6 @@ impl CreateFilter for IsActiveEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(IsActiveGraph::new(graph))) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl ComposableFilter for IsActiveEdge {} diff --git a/raphtory/src/db/graph/views/filter/model/is_active_node_filter.rs b/raphtory/src/db/graph/views/filter/model/is_active_node_filter.rs index 994d3d2521..564c6765e7 100644 --- a/raphtory/src/db/graph/views/filter/model/is_active_node_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/is_active_node_filter.rs @@ -4,7 +4,7 @@ use crate::{ graph::views::filter::{ model::{ edge_filter::CompositeEdgeFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, TryAsCompositeFilter, + CompositeNodeFilter, CreateView, TryAsCompositeFilter, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -16,15 +16,17 @@ use crate::{ use std::fmt; #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct IsActiveNode; +pub struct IsActiveNode { + pub(crate) view_expr: E, +} -impl fmt::Display for IsActiveNode { +impl fmt::Display for IsActiveNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "IS_ACTIVE_NODE") } } -impl CreateFilter for IsActiveNode { +impl CreateFilter for IsActiveNode { type EntityFiltered<'graph, G> = NodeFilteredGraph> where @@ -32,7 +34,7 @@ impl CreateFilter for IsActiveNode { G: GraphViewOps<'graph>; type NodeFilter<'graph, G> - = Map, bool> + = Map>, bool> where Self: 'graph, G: GraphView + 'graph; @@ -55,23 +57,17 @@ impl CreateFilter for IsActiveNode { self, graph: G, ) -> Result, GraphError> { - let op: Map, bool> = HistoryOp::new(graph).map(|h| !h.is_empty()); - Ok(op) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) + Ok(HistoryOp::new(self.view_expr.create_view(graph)?).map(|h| !h.is_empty())) } } -impl ComposableFilter for IsActiveNode {} +impl ComposableFilter for IsActiveNode {} -impl TryAsCompositeFilter for IsActiveNode { +impl TryAsCompositeFilter for IsActiveNode { fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::IsActiveNode(IsActiveNode)) + Ok(CompositeNodeFilter::IsActiveNode(Box::new( + self.view_expr.try_as_composite_node_filter()?, + ))) } fn try_as_composite_edge_filter(&self) -> Result { diff --git a/raphtory/src/db/graph/views/filter/model/is_deleted_filter.rs b/raphtory/src/db/graph/views/filter/model/is_deleted_filter.rs index 3f90aba886..27c08e885e 100644 --- a/raphtory/src/db/graph/views/filter/model/is_deleted_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/is_deleted_filter.rs @@ -58,13 +58,6 @@ impl CreateFilter for IsDeletedEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(IsDeletedGraph::new(graph))) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl ComposableFilter for IsDeletedEdge {} diff --git a/raphtory/src/db/graph/views/filter/model/is_self_loop_filter.rs b/raphtory/src/db/graph/views/filter/model/is_self_loop_filter.rs index 6eeaeff58f..d397dfdab4 100644 --- a/raphtory/src/db/graph/views/filter/model/is_self_loop_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/is_self_loop_filter.rs @@ -58,13 +58,6 @@ impl CreateFilter for IsSelfLoopEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(IsSelfLoopGraph::new(graph))) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl ComposableFilter for IsSelfLoopEdge {} diff --git a/raphtory/src/db/graph/views/filter/model/is_valid_filter.rs b/raphtory/src/db/graph/views/filter/model/is_valid_filter.rs index 75a2c55279..a42ada47c3 100644 --- a/raphtory/src/db/graph/views/filter/model/is_valid_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/is_valid_filter.rs @@ -58,13 +58,6 @@ impl CreateFilter for IsValidEdge { ) -> Result, GraphError> { Ok(NodeExistsOp::new(ValidGraph::new(graph))) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl ComposableFilter for IsValidEdge {} diff --git a/raphtory/src/db/graph/views/filter/model/latest_filter.rs b/raphtory/src/db/graph/views/filter/model/latest_filter.rs index 04d4d5e23e..38ffb8cda1 100644 --- a/raphtory/src/db/graph/views/filter/model/latest_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/latest_filter.rs @@ -4,21 +4,9 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, - PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -57,45 +45,6 @@ impl InternalViewWrapOps for Latest { } } -impl InternalNodeFilterBuilder for Latest { - type FilterType = T::FilterType; - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalNodeIdFilterBuilder for Latest { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalPropertyFilterBuilder for Latest { - type Filter = Latest; - type ExprBuilder = Latest; - 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: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl TryAsCompositeFilter for Latest { fn try_as_composite_node_filter(&self) -> Result { Ok(CompositeNodeFilter::Latest(Box::new(Latest::new( @@ -130,7 +79,7 @@ impl CreateFilter for Latest G: GraphView + TimeOps<'graph> + Clone + 'graph; type FilteredGraph<'graph, G> - = WindowedGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -154,13 +103,6 @@ impl CreateFilter for Latest { self.inner.create_node_filter(graph) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(self.inner.filter_graph_view(graph)?.latest()) - } } impl ComposableFilter for Latest {} @@ -171,51 +113,3 @@ impl Wrap for Latest { Latest::new(value) } } - -impl InternalPropertyFilterFactory for Latest { - type Entity = T::Entity; - type PropertyBuilder = Latest; - type MetadataBuilder = Latest; - - fn entity(&self) -> Self::Entity { - self.inner.entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.wrap(self.inner.metadata_builder(property)) - } -} - -impl TemporalPropertyFilterFactory for Latest {} - -impl NodeViewFilterOps for Latest { - type Output = Latest>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } -} - -impl EdgeViewFilterOps for Latest { - type Output = Latest>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } - - fn is_valid(&self) -> Self::Output { - self.wrap(self.inner.is_valid()) - } - - fn is_deleted(&self) -> Self::Output { - self.wrap(self.inner.is_deleted()) - } - - fn is_self_loop(&self) -> Self::Output { - self.wrap(self.inner.is_self_loop()) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/layered_filter.rs b/raphtory/src/db/graph/views/filter/model/layered_filter.rs index faaaa79791..1ec1fc6322 100644 --- a/raphtory/src/db/graph/views/filter/model/layered_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/layered_filter.rs @@ -4,20 +4,9 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, - PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -69,46 +58,6 @@ impl InternalViewWrapOps for Layered { } } -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: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl TryAsCompositeFilter for Layered { fn try_as_composite_node_filter(&self) -> Result { let filter = self.inner.try_as_composite_node_filter()?; @@ -143,7 +92,7 @@ impl CreateFilter for Layered - = LayeredGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -167,15 +116,6 @@ impl CreateFilter for Layered( - &self, - graph: G, - ) -> Result, GraphError> { - self.inner - .filter_graph_view(graph)? - .layers(self.layer.clone()) - } } impl ComposableFilter for Layered {} @@ -187,51 +127,3 @@ impl Wrap for Layered { 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, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.wrap(self.inner.metadata_builder(property)) - } -} - -impl TemporalPropertyFilterFactory for Layered {} - -impl NodeViewFilterOps for Layered { - type Output = Layered>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } -} - -impl EdgeViewFilterOps for Layered { - type Output = Layered>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } - - fn is_valid(&self) -> Self::Output { - self.wrap(self.inner.is_valid()) - } - - fn is_deleted(&self) -> Self::Output { - self.wrap(self.inner.is_deleted()) - } - - fn is_self_loop(&self) -> Self::Output { - self.wrap(self.inner.is_self_loop()) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/mod.rs b/raphtory/src/db/graph/views/filter/model/mod.rs index 967f6794e0..19f6e3bb71 100644 --- a/raphtory/src/db/graph/views/filter/model/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/mod.rs @@ -1,28 +1,4 @@ pub(crate) use crate::db::graph::views::filter::model::and_filter::AndFilter; -use crate::db::{ - api::{ - state::{ - ops::{filter::NO_FILTER, Const}, - NodeOp, - }, - view::BoxableGraphView, - }, - graph::views::filter::model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - latest_filter::Latest, - layered_filter::Layered, - property_filter::{ - builders::PropertyExprBuilderInput, Op, PropertyFilterInput, PropertyRef, - }, - snapshot_filter::{SnapshotAt, SnapshotLatest}, - windowed_filter::Windowed, - }, -}; pub use crate::{ db::{ api::view::internal::GraphView, @@ -34,8 +10,18 @@ pub use crate::{ CompositeExplodedEdgeFilter, ExplodedEdgeEndpointWrapper, ExplodedEdgeFilter, }, - filter_operator::FilterOperator, - node_filter::{NodeFilter, NodeNameFilter, NodeTypeFilter}, + filter_operator::{ + BinaryOp, Comparable, FilterOperator, SetOp, StringComparable, StringOp, + UnaryOp, + }, + node_expr::{ + Aggregated, AllMode, AnyMode, AvgExpr, BinaryCmpNodeFilter, ConstExpr, + DegreeExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, Metadata, MinExpr, + NodeExpr, NodeExprFilterOps, Property, Quantified, QuantifiedNodeFilter, + QuantifierMode, SetNodeFilter, StringNodeFilter, SumExpr, TemporalExprOps, + TemporalProp, UnaryNodeFilter, + }, + node_filter::NodeFilter, not_filter::NotFilter, or_filter::OrFilter, }, @@ -47,9 +33,46 @@ pub use crate::{ errors::GraphError, prelude::{GraphViewOps, TimeOps}, }; +use crate::{ + db::{ + api::{ + properties::TemporalPropertyView, + state::{ + ops::{filter::NO_FILTER, Const}, + NodeOp, + }, + view::BoxableGraphView, + }, + graph::views::{ + filter::model::{ + edge_filter::CompositeEdgeFilter, + is_active_edge_filter::IsActiveEdge, + is_active_node_filter::IsActiveNode, + is_deleted_filter::IsDeletedEdge, + is_self_loop_filter::IsSelfLoopEdge, + is_valid_filter::IsValidEdge, + latest_filter::Latest, + layered_filter::Layered, + node_expr::{NodeMetaOp, NodePropOp}, + node_filter::NodeFilterFactory, + property_filter::{ + builders::{ + InternalPropertyFilterBuilder, PropertyExprBuilder, + PropertyExprBuilderInput, + }, + Op, PropertyFilterInput, PropertyRef, + }, + snapshot_filter::{SnapshotAt, SnapshotLatest}, + windowed_filter::Windowed, + }, + layer_graph::LayeredGraph, + }, + }, + prelude::LayerOps, +}; pub use node_filter::CompositeNodeFilter; use raphtory_api::core::{ - entities::Layer, + entities::{properties::prop::Prop, Layer}, storage::timeindex::{AsTime, EventTime}, utils::time::IntoTime, }; @@ -60,6 +83,7 @@ pub mod edge_filter; pub mod exploded_edge_filter; pub mod filter; pub mod filter_operator; +pub mod filter_value; pub mod graph_filter; pub mod is_active_edge_filter; pub mod is_active_node_filter; @@ -68,6 +92,7 @@ pub mod is_self_loop_filter; pub mod is_valid_filter; pub mod latest_filter; pub mod layered_filter; +pub mod node_expr; pub mod node_filter; pub mod node_state_filter; pub mod not_filter; @@ -109,13 +134,6 @@ impl CreateFilter for NoFilter { ) -> Result, GraphError> { Ok(NO_FILTER) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for NoFilter { @@ -167,22 +185,6 @@ pub trait ComposableFilter: Sized { } } -pub trait InternalPropertyFilterBuilder: Send + Sync { - type Filter: CombinedFilter; - type ExprBuilder: InternalPropertyFilterBuilder; - type Marker: Into + Send + Sync + Clone + 'static; - - fn property_ref(&self) -> PropertyRef; - - fn ops(&self) -> &[Op]; - - fn entity(&self) -> Self::Marker; - - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter; - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder; -} - pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { fn create_dyn_filter<'graph>( &self, @@ -193,11 +195,6 @@ pub trait DynCreateFilter: TryAsCompositeFilter + Send + Sync + 'static { &self, graph: Arc, ) -> Result + 'graph>, GraphError>; - - fn dyn_filter_graph_view<'graph>( - &self, - graph: Arc, - ) -> Result, GraphError>; } impl DynCreateFilter for T @@ -217,13 +214,6 @@ where ) -> Result + 'graph>, GraphError> { Ok(Arc::new(self.clone().create_node_filter(graph)?)) } - - fn dyn_filter_graph_view<'graph>( - &self, - graph: Arc, - ) -> Result, GraphError> { - Ok(Arc::new(self.clone().filter_graph_view(graph)?)) - } } impl CreateFilter for Arc { @@ -253,226 +243,166 @@ impl CreateFilter for Arc { ) -> Result, GraphError> { self.deref().create_dyn_node_filter(Arc::new(graph)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - self.deref().dyn_filter_graph_view(Arc::new(graph)) - } } -pub trait DynPropertyFilterBuilder: Send + Sync + 'static { - fn dyn_property_ref(&self) -> PropertyRef; - - fn dyn_ops(&self) -> &[Op]; - - fn dyn_entity(&self) -> EntityMarker; - - fn dyn_filter(&self, filter: PropertyFilterInput) -> Arc; +#[derive(Copy, Clone)] +pub enum EntityMarker { + Node, + Edge, + ExplodedEdge, +} - fn dyn_into_expr_builder( - &self, - builder: PropertyExprBuilderInput, - ) -> Arc; +#[derive(Clone)] +pub struct PropertyExpr { + view_expr: E, + name: String, } -impl DynPropertyFilterBuilder for T { - fn dyn_property_ref(&self) -> PropertyRef { - self.property_ref() - } +impl NodeExpr + for PropertyExpr +{ + type Output = Option; - fn dyn_ops(&self) -> &[Op] { - self.ops() + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + let prop_id = graph + .node_meta() + .get_prop_id(&self.name, false) + .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; + let graph = self.view_expr.create_view(graph)?; + Ok(Arc::new(NodePropOp { graph, prop_id })) } +} - fn dyn_entity(&self) -> EntityMarker { - self.entity().into() - } +#[derive(Clone)] +pub struct MetadataExpr { + view_expr: E, + name: String, +} - fn dyn_filter(&self, filter: PropertyFilterInput) -> Arc { - Arc::new(self.filter(filter)) - } +impl NodeExpr + for MetadataExpr +{ + type Output = Option; - fn dyn_into_expr_builder( + fn create_node_op<'g, G: GraphView + 'g>( &self, - builder: PropertyExprBuilderInput, - ) -> Arc { - Arc::new(self.with_expr_builder(builder)) + graph: G, + ) -> Result + 'g>, GraphError> { + let prop_id = graph + .node_meta() + .get_prop_id(&self.name, true) + .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; + let graph = self.view_expr.create_view(graph)?; + Ok(Arc::new(NodeMetaOp { graph, prop_id })) } } -impl InternalPropertyFilterBuilder for Arc { - type Filter = Arc; - type ExprBuilder = Arc; - type Marker = EntityMarker; +pub trait PropertyFilterFactory: Sized { + fn property(&self, name: impl Into) -> PropertyExpr; - fn property_ref(&self) -> PropertyRef { - self.deref().dyn_property_ref() - } + fn metadata(&self, name: impl Into) -> MetadataExpr; +} - fn ops(&self) -> &[Op] { - self.deref().dyn_ops() +impl PropertyFilterFactory for T { + fn property(&self, name: impl Into) -> PropertyExpr { + PropertyExpr { + view_expr: self.clone(), + name: name.into(), + } } - fn entity(&self) -> Self::Marker { - self.deref().dyn_entity() + fn metadata(&self, name: impl Into) -> MetadataExpr { + MetadataExpr { + view_expr: self.clone(), + name: name.into(), + } } +} - fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().dyn_filter(filter) - } +pub trait DynPropertyFilterFactory { + fn property(&self, name: String) -> PropertyExpr>; +} - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().dyn_into_expr_builder(builder) +impl DynPropertyFilterFactory for T { + fn property(&self, name: String) -> PropertyExpr> { + PropertyExpr { + view_expr: Arc::new(self.clone()) as Arc, + name, + } } } -impl InternalPropertyFilterBuilder for Arc { - type Filter = Arc; - type ExprBuilder = Arc; - type Marker = EntityMarker; +impl InternalPropertyFilterBuilder for PropertyExpr +where + E: Into + Send + Sync + Clone + 'static, + crate::prelude::PropertyFilter: CombinedFilter, + PropertyExprBuilder: InternalPropertyFilterBuilder, +{ + type Filter = crate::prelude::PropertyFilter; + type ExprBuilder = PropertyExprBuilder; + type Marker = E; fn property_ref(&self) -> PropertyRef { - self.deref().dyn_property_ref() + PropertyRef::Property(self.name.clone()) } fn ops(&self) -> &[Op] { - self.deref().dyn_ops() + &[] } fn entity(&self) -> Self::Marker { - self.deref().dyn_entity() + self.view_expr.clone() } fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().dyn_filter(filter) + filter.with_entity(self.entity()) } fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().dyn_into_expr_builder(builder) + builder.with_entity(self.entity()) } } -impl InternalPropertyFilterBuilder for Arc { - type Filter = T::Filter; - type ExprBuilder = T::ExprBuilder; - type Marker = T::Marker; +impl InternalPropertyFilterBuilder for MetadataExpr +where + E: Into + Send + Sync + Clone + 'static, + crate::prelude::PropertyFilter: CombinedFilter, + PropertyExprBuilder: InternalPropertyFilterBuilder, +{ + type Filter = crate::prelude::PropertyFilter; + type ExprBuilder = PropertyExprBuilder; + type Marker = E; fn property_ref(&self) -> PropertyRef { - self.deref().property_ref() + PropertyRef::Metadata(self.name.clone()) } fn ops(&self) -> &[Op] { - self.deref().ops() + &[] } fn entity(&self) -> Self::Marker { - self.deref().entity() + self.view_expr.clone() } fn filter(&self, filter: PropertyFilterInput) -> Self::Filter { - self.deref().filter(filter) + filter.with_entity(self.entity()) } fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.deref().with_expr_builder(builder) - } -} - -#[derive(Copy, Clone)] -pub enum EntityMarker { - Node, - Edge, - ExplodedEdge, -} - -pub trait InternalPropertyFilterFactory { - type Entity: Clone + Send + Sync + Into + 'static; - type PropertyBuilder: InternalPropertyFilterBuilder + TemporalPropertyFilterFactory; - type MetadataBuilder: InternalPropertyFilterBuilder; - - fn entity(&self) -> Self::Entity; - - fn property_builder(&self, property: String) -> Self::PropertyBuilder; - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder; -} - -pub trait DynPropertyFilterFactory: Send + Sync + 'static { - fn dyn_entity(&self) -> EntityMarker; - - fn dyn_property_builder(&self, property: String) -> Arc; - - fn dyn_metadata_builder(&self, property: String) -> Arc; -} - -impl DynPropertyFilterFactory for T { - fn dyn_entity(&self) -> EntityMarker { - self.entity().into() - } - - fn dyn_property_builder(&self, property: String) -> Arc { - Arc::new(self.property_builder(property)) - } - - fn dyn_metadata_builder(&self, property: String) -> Arc { - Arc::new(self.metadata_builder(property)) - } -} - -impl InternalPropertyFilterFactory for Arc { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; - - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) - } -} - -pub trait PropertyFilterFactory: InternalPropertyFilterFactory { - fn property(&self, name: impl Into) -> Self::PropertyBuilder { - self.property_builder(name.into()) - } - - fn metadata(&self, name: impl Into) -> Self::MetadataBuilder { - self.metadata_builder(name.into()) + builder.with_entity(self.entity()) } } -impl PropertyFilterFactory for T {} - -pub trait TemporalPropertyFilterFactory: InternalPropertyFilterBuilder { - fn temporal(&self) -> Self::ExprBuilder { - let builder = PropertyExprBuilderInput { - prop_ref: PropertyRef::TemporalProperty(self.property_ref().name().to_string()), - ops: vec![], - }; - self.with_expr_builder(builder) - } -} - -pub trait DynTemporalPropertyFilterBuilder: DynPropertyFilterBuilder { - fn dyn_temporal(&self) -> Arc; -} - -impl DynTemporalPropertyFilterBuilder for T { - fn dyn_temporal(&self) -> Arc { - Arc::new(self.temporal()) +impl PropertyExpr { + pub fn temporal(&self) -> TemporalProp { + TemporalProp::new(self.view_expr.clone(), self.name.clone()) } } -impl TemporalPropertyFilterFactory for Arc {} - pub trait TryAsCompositeFilter: Send + Sync { fn try_as_composite_node_filter(&self) -> Result; @@ -599,32 +529,77 @@ pub trait ViewWrapOps: InternalViewWrapOps + Sized { impl ViewWrapOps for T {} -pub trait ViewWrapPropOps: InternalViewWrapOps + InternalPropertyFilterFactory + Sized {} - -impl ViewWrapPropOps for T where T: InternalViewWrapOps + InternalPropertyFilterFactory + Sized {} +pub trait CreateView: Clone + Send + Sync + 'static { + type View<'graph, G: GraphView + 'graph>: GraphView + 'graph; + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError>; +} -pub trait DynInternalViewWrapPropOps: DynInternalViewWrapOps + DynPropertyFilterFactory {} +pub trait DynCreateView: Send + Sync { + fn dyn_create_view<'graph>( + &self, + view: Arc, + ) -> Result, GraphError>; +} -impl DynInternalViewWrapPropOps for T where T: DynInternalViewWrapOps + DynPropertyFilterFactory {} +impl DynCreateView for T { + fn dyn_create_view<'graph>( + &self, + view: Arc, + ) -> Result, GraphError> { + Ok(Arc::new(self.create_view(view)?)) + } +} -impl InternalPropertyFilterFactory for Arc { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; +impl CreateView for Arc { + type View<'graph, G: GraphView + 'graph> = Arc; - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.deref().dyn_create_view(Arc::new(view)) } +} - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) +impl CreateView for NodeFilter { + type View<'graph, G: GraphView + 'graph> = G; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + Ok(view) } +} - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) +impl CreateView for Layered { + type View<'graph, G: GraphView + 'graph> = LayeredGraph; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + view.layers(self.layer.clone()) } } +pub trait ViewWrapPropOps: InternalViewWrapOps + PropertyFilterFactory + Sized {} + +impl ViewWrapPropOps for T where T: InternalViewWrapOps + PropertyFilterFactory + Sized {} + +pub trait DynInternalViewWrapPropOps: + DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView +{ +} + +impl DynInternalViewWrapPropOps for T where + T: DynInternalViewWrapOps + DynPropertyFilterFactory + DynCreateView +{ +} + impl InternalViewWrapOps for Arc { type Window = Arc; @@ -637,6 +612,17 @@ impl InternalViewWrapOps for Arc { } } +impl CreateView for Arc { + type View<'graph, G: GraphView + 'graph> = Arc; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.deref().dyn_create_view(Arc::new(view)) + } +} + pub trait DynViewFilter: DynInternalViewWrapOps + DynCreateFilter + Send + Sync + 'static {} impl DynViewFilter for T where T: DynInternalViewWrapOps + DynCreateFilter + Send + Sync + 'static {} @@ -663,14 +649,16 @@ impl InternalViewWrapOps for DynView { pub trait NodeViewFilterOps: ViewWrapOps { type Output: CombinedFilter; - fn is_active(&self) -> Self::Output; + fn is_active(&self) -> Self::Output>; } -pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps { +pub trait DynNodeViewFilterOps: DynInternalViewWrapPropOps + TryAsCompositeFilter { fn dyn_is_active(&self) -> Arc; } -impl DynNodeViewFilterOps for T { +impl DynNodeViewFilterOps + for T +{ fn dyn_is_active(&self) -> Arc { Arc::new(self.is_active()) } @@ -688,7 +676,7 @@ pub trait EdgeViewFilterOps: ViewWrapOps { fn is_self_loop(&self) -> Self::Output; } -pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps { +pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps + TryAsCompositeFilter { fn dyn_is_active(&self) -> Arc; fn dyn_is_valid(&self) -> Arc; @@ -698,7 +686,9 @@ pub trait DynEdgeViewFilterOps: DynInternalViewWrapPropOps { fn dyn_is_self_loop(&self) -> Arc; } -impl DynEdgeViewFilterOps for T { +impl DynEdgeViewFilterOps + for T +{ fn dyn_is_active(&self) -> Arc { Arc::new(self.is_active()) } @@ -718,6 +708,17 @@ impl DynEdgeViewFilterOps for pub type DynNodeViewProps = Arc; +impl CreateView for DynNodeViewProps { + type View<'graph, G: GraphView + 'graph> = Arc; + + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.deref().dyn_create_view(Arc::new(view)) + } +} + impl InternalViewWrapOps for DynNodeViewProps { type Window = DynNodeViewProps; @@ -733,31 +734,32 @@ impl InternalViewWrapOps for DynNodeViewProps { impl NodeViewFilterOps for DynNodeViewProps { type Output = Arc; - fn is_active(&self) -> Self::Output { + fn is_active(&self) -> Self::Output> { self.deref().dyn_is_active() } } -impl InternalPropertyFilterFactory for DynNodeViewProps { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; - - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() +impl DynNodeViewFilterOps for Windowed { + fn dyn_is_active(&self) -> Arc { + Arc::new(IsActiveNode { + view_expr: self.clone(), + }) } +} - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) - } +pub type DynEdgeViewProps = Arc; + +impl CreateView for DynEdgeViewProps { + type View<'graph, G: GraphView + 'graph> = Arc; - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + self.deref().dyn_create_view(Arc::new(view)) } } -pub type DynEdgeViewProps = Arc; - impl InternalViewWrapOps for DynEdgeViewProps { type Window = DynEdgeViewProps; @@ -790,20 +792,20 @@ impl EdgeViewFilterOps for DynEdgeViewProps { } } -impl InternalPropertyFilterFactory for DynEdgeViewProps { - type Entity = EntityMarker; - type PropertyBuilder = Arc; - type MetadataBuilder = Arc; +impl DynEdgeViewFilterOps for Windowed { + fn dyn_is_active(&self) -> Arc { + Arc::new(Windowed::new(self.start, self.end, IsActiveEdge)) + } - fn entity(&self) -> Self::Entity { - self.deref().dyn_entity() + fn dyn_is_valid(&self) -> Arc { + Arc::new(Windowed::new(self.start, self.end, IsValidEdge)) } - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.deref().dyn_property_builder(property) + fn dyn_is_deleted(&self) -> Arc { + Arc::new(Windowed::new(self.start, self.end, IsDeletedEdge)) } - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.deref().dyn_metadata_builder(property) + fn dyn_is_self_loop(&self) -> Arc { + self.inner.deref().dyn_is_self_loop() } } diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/exprs.rs b/raphtory/src/db/graph/views/filter/model/node_expr/exprs.rs new file mode 100644 index 0000000000..ed2c6bf514 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/exprs.rs @@ -0,0 +1,483 @@ +//! Node expressions — what value a node can produce. +//! +//! An expression is a pure data structure (no graph reference). It describes *what to compute* +//! without computing it. Call [`NodeExpr::create_node_op`] to compile it against a specific graph +//! view, performing name→ID resolution once. +//! +//! # Field expressions +//! +//! ```rust,ignore +//! NodeFilter::id() // Id — NodeExpr — e.g. .eq(GID::Str("v1".into())) +//! NodeFilter::name() // Name — NodeExpr — e.g. .eq("Alice") +//! NodeFilter::node_type() // Type — NodeExpr> — e.g. .is_some() +//! ``` +//! +//! # Degree expressions +//! +//! ```rust,ignore +//! NodeFilter::degree() // DegreeExpr — NodeExpr — e.g. .gt(2usize) +//! NodeFilter::in_degree() // DegreeExpr — NodeExpr — e.g. .eq(0usize) (no in-edges) +//! NodeFilter::out_degree() // DegreeExpr — NodeExpr — e.g. .gt(NodeFilter::in_degree()) +//! ``` +//! +//! # Property expressions +//! +//! ```rust,ignore +//! NodeFilter::property("age") // Property — NodeExpr> — e.g. .gt(30i64) +//! NodeFilter::property("score").is_some() // Property — nodes where "score" is set +//! NodeFilter::metadata("region") // Metadata — NodeExpr> — e.g. .eq(Prop::Str("EU".into())) +//! ``` +//! +//! # Temporal property expressions +//! +//! ```rust,ignore +//! NodeFilter::temporal_property("score") // TemporalPropertyExpr — NodeExpr (Prop::List of all values in window) +//! +//! // Quantifiers (QuantifiedNodeFilter via AnyMode / AllMode): +//! NodeFilter::temporal_property("score").any().gt(10i64) // pass if any value > 10 +//! NodeFilter::temporal_property("score").all().gt(0i64) // pass if every value > 0 +//! +//! // Aggregators (BinaryCmpNodeFilter via SumExpr / AvgExpr / etc.): +//! NodeFilter::temporal_property("price").sum().gt(100i64) // SumExpr — pass if total > 100 +//! NodeFilter::temporal_property("price").avg().lt(50i64) // AvgExpr — pass if average < 50 +//! NodeFilter::temporal_property("ts").len().gt(3usize) // LenExpr — pass if more than 3 updates +//! NodeFilter::temporal_property("ts").first().eq(Prop::I64(0)) // FirstExpr — pass if first value == 0 +//! NodeFilter::temporal_property("ts").last().eq(Prop::I64(1)) // LastExpr — pass if last value == 1 +//! NodeFilter::temporal_property("v").min().gt(0i64) // MinExpr — pass if minimum > 0 +//! NodeFilter::temporal_property("v").max().lt(100i64) // MaxExpr — pass if maximum < 100 +//! ``` +//! +//! # Literal (RHS) expressions +//! +//! ```rust,ignore +//! // Plain Rust values implement NodeExpr — pass them directly as the RHS of any comparison: +//! NodeFilter::degree().gt(2usize) // usize — NodeExpr +//! NodeFilter::name().eq("Alice") // &str — NodeExpr +//! NodeFilter::name().eq("Bob".to_string()) // String — NodeExpr +//! NodeFilter::property("age").gt(30i64) // i64 — NodeExpr> +//! NodeFilter::property("score").eq(Prop::F64(9.5)) // Prop — NodeExpr> +//! // ConstExpr for custom comparable types not covered above +//! ``` + +use super::{ + ops::{ + AvgNodeOp, FirstNodeOp, LastNodeOp, LenNodeOp, MaxNodeOp, MinNodeOp, NodeMetaOp, + NodePropOp, SumNodeOp, TemporalNodePropOp, + }, + NodeExpr, +}; +use crate::{ + db::{ + api::{ + state::ops::{Const, Degree, Id, Name, NodeOp, Type}, + view::internal::GraphView, + }, + graph::views::filter::model::{ + filter_operator::Comparable, node_filter::NodeFilter, CreateView, + }, + }, + errors::GraphError, +}; +use raphtory_api::core::{ + entities::{ + properties::prop::{Prop, PropType}, + GID, + }, + storage::arc_str::ArcStr, + Direction, +}; +use std::sync::Arc; + +// ───────────────────────────────────────────────────────────────────────────── +// Node field expressions — identity, name, type +// +// Id, Name, Type are zero-sized structs defined in db::api::state::ops. +// NodeExpr is implemented here so they can appear as LHS or RHS in filter expressions. +// NodeFilter::id() uses Id — NodeExpr +// NodeFilter::name() uses Name — NodeExpr +// NodeFilter::node_type() uses Type — NodeExpr> +// ───────────────────────────────────────────────────────────────────────────── + +impl NodeExpr for Id { + type Output = GID; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Id)) + } +} + +impl NodeExpr for GID { + type Output = GID; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(self.clone()))) + } +} + +impl NodeExpr for Name { + type Output = String; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Name)) + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +impl NodeExpr for Type { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Type)) + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Constant value expressions — literal RHS values +// +// Allows passing raw values directly to filter operators: +// NodeFilter::degree().gt(2usize) +// NodeFilter::name().eq("Alice") +// NodeFilter::property("age").gt(30i64) +// ───────────────────────────────────────────────────────────────────────────── + +impl NodeExpr for usize { + type Output = usize; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(*self))) + } + + fn prop_type(&self) -> PropType { + PropType::U64 + } +} + +impl NodeExpr for String { + type Output = String; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(self.clone()))) + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +impl NodeExpr for ArcStr { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone())))) + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +impl NodeExpr for &'static str { + type Output = &'static str; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(*self))) + } + + fn prop_type(&self) -> PropType { + PropType::Str + } +} + +impl NodeExpr for Prop { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(self.clone())))) + } + + fn prop_type(&self) -> PropType { + self.dtype() + } +} + +macro_rules! impl_node_expr_for_numeric { + ($prim:ty, $variant:ident) => { + impl NodeExpr for $prim { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result> + 'g>, GraphError> { + Ok(Arc::new(Const(Some(Prop::$variant(*self))))) + } + } + }; +} + +impl_node_expr_for_numeric!(i32, I32); +impl_node_expr_for_numeric!(i64, I64); +impl_node_expr_for_numeric!(u32, U32); +impl_node_expr_for_numeric!(u64, U64); +impl_node_expr_for_numeric!(f32, F32); +impl_node_expr_for_numeric!(f64, F64); +impl_node_expr_for_numeric!(bool, Bool); +impl_node_expr_for_numeric!(u8, U8); +impl_node_expr_for_numeric!(u16, U16); + +/// A constant expression for custom output types not covered by the built-in impls. +/// +/// Built-in types (`usize`, `String`, `Prop`, numerics, `&'static str`) implement +/// [`NodeExpr`] directly and can be passed as-is. `ConstExpr` is only needed +/// for custom comparable types. +/// +/// ```rust,ignore +/// some_expr.gt(ConstExpr(my_custom_value)) +/// ``` +#[derive(Clone)] +pub struct ConstExpr(pub T); + +impl NodeExpr for ConstExpr { + type Output = T; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + _graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Const(self.0.clone()))) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Named property / degree expressions +// ───────────────────────────────────────────────────────────────────────────── + +/// Degree of a node in a given direction. +/// +/// Created by `NodeFilter::degree()` / `::in_degree()` / `::out_degree()`. +/// `E` is the view expression that scopes the edges counted (window / layer / etc.). +/// Compiles to the `Degree` op from `db::api::state::ops`. +/// +/// ```rust,ignore +/// NodeFilter::degree().gt(2usize) +/// NodeFilter::out_degree().gt(NodeFilter::in_degree()) +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DegreeExpr { + pub dir: Direction, + pub view_expr: E, +} + +impl NodeExpr for DegreeExpr { + type Output = usize; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + Ok(Arc::new(Degree { + dir: self.dir, + view: self.view_expr.create_view(graph)?, + })) + } +} + +/// Current (latest) value of a named property. +/// +/// Created by `NodeFilter::property("name")`. +/// Resolves the property name to a column ID once at `create_node_op` time, +/// then compiles to a `NodePropOp { graph, prop_id }`. +/// +/// ```rust,ignore +/// NodeFilter::property("age").gt(30i64) +/// NodeFilter::property("score").is_some() +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Property { + pub name: String, +} + +impl Property { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } +} + +impl NodeExpr for Property { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let (prop_id, _) = graph + .node_meta() + .get_prop_id_and_type(&self.name, false) + .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; + Ok(Arc::new(NodePropOp { graph, prop_id })) + } +} + +/// Static (non-temporal) metadata field. +/// +/// Created by `NodeFilter::metadata("name")`. +/// Resolves the metadata name to a column ID once at `create_node_op` time, +/// then compiles to a `NodeMetaOp { graph, prop_id }`. +/// +/// ```rust,ignore +/// NodeFilter::metadata("region").eq(Prop::Str("EU".into())) +/// NodeFilter::metadata("tier").is_some() +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Metadata { + pub name: String, +} + +impl Metadata { + pub fn new(name: impl Into) -> Self { + Self { name: name.into() } + } +} + +impl NodeExpr for Metadata { + type Output = Option; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result> + 'g>, GraphError> { + let (prop_id, _) = graph + .node_meta() + .get_prop_id_and_type(&self.name, true) + .ok_or_else(|| GraphError::MetadataMissingError(self.name.clone()))?; + Ok(Arc::new(NodeMetaOp { graph, prop_id })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Temporal property expression — returns Prop::List of all values in the window +// ───────────────────────────────────────────────────────────────────────────── + +/// All temporal values of a named property over the current view window. +/// +/// Produces `Prop::List` of every recorded value within the view. +/// +/// Not constructed directly — created internally by the fluent chain started +/// by `NodeFilter::temporal_property(name)`: +/// +/// ```rust,ignore +/// // NodeFilter::temporal_property("score") returns TemporalProp, not this type. +/// // TemporalPropertyExpr is created inside .any() / .all() / .sum() etc., e.g.: +/// // .any().gt(10i64) → QuantifiedNodeFilter, AnyMode, i64> +/// // .sum().gt(100i64) → BinaryCmpNodeFilter>, i64> +/// ``` +#[derive(Clone)] +pub struct TemporalPropertyExpr { + pub view_expr: E, + pub name: String, +} + +impl TemporalPropertyExpr { + pub fn new(name: impl Into) -> Self { + Self { + view_expr: NodeFilter, + name: name.into(), + } + } +} + +impl NodeExpr for TemporalPropertyExpr { + type Output = Prop; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + let (prop_id, _) = graph + .node_meta() + .get_prop_id_and_type(&self.name, false) + .ok_or_else(|| GraphError::PropertyMissingError(self.name.clone()))?; + let graph = self.view_expr.create_view(graph)?; + Ok(Arc::new(TemporalNodePropOp { graph, prop_id })) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Aggregator Exprs — NodeExpr wrappers producing a single scalar +// +// Each wraps a NodeExpr (typically TemporalPropertyExpr) and reduces +// the Prop::List it produces to a scalar. Not constructed directly — +// TemporalProp / TemporalExprOps methods return Aggregated>: +// +// .temporal_property("v").sum() → Aggregated>> +// .temporal_property("v").len() → Aggregated>> +// +// Calling .gt() / .eq() etc. on Aggregated then produces: +// BinaryCmpNodeFilter>, RHS> +// ───────────────────────────────────────────────────────────────────────────── + +macro_rules! impl_agg_expr { + ($expr:ident, $op_ty:ident, $output:ty) => { + pub struct $expr>(pub E); + + impl> Clone for $expr { + fn clone(&self) -> Self { + $expr(self.0.clone()) + } + } + + impl> NodeExpr for $expr { + type Output = $output; + + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError> { + let inner = self.0.create_node_op(graph)?; + Ok(Arc::new($op_ty { inner })) + } + } + }; +} + +impl_agg_expr!(SumExpr, SumNodeOp, Option); +impl_agg_expr!(AvgExpr, AvgNodeOp, Option); +impl_agg_expr!(MinExpr, MinNodeOp, Option); +impl_agg_expr!(MaxExpr, MaxNodeOp, Option); +impl_agg_expr!(FirstExpr, FirstNodeOp, Option); +impl_agg_expr!(LastExpr, LastNodeOp, Option); +impl_agg_expr!(LenExpr, LenNodeOp, usize); diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs b/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs new file mode 100644 index 0000000000..880deebf7a --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/filters.rs @@ -0,0 +1,1152 @@ +//! Filter types — bridge from expressions to a filtered graph. +//! +//! A filter is a pure data structure that pairs two expressions with an operator. +//! Calling `create_filter(graph)` compiles both sides into [`NodeOp`]s and wraps the +//! graph in a [`NodeFilteredGraph`] that skips non-matching nodes during iteration. +//! +//! # Three-phase pipeline +//! +//! ```text +//! Phase 1 — Build (pure Rust data, no graph): +//! NodeFilter::property("age").gt(30i64) +//! ──► BinaryCmpNodeFilter { left: Property("age"), op: Gt, right: ConstExpr(30i64) } +//! +//! Phase 2 — Compile (bind to graph, resolve names): +//! BinaryCmpNodeFilter::create_node_filter(graph)? +//! ──► Arc> +//! = BinaryCmpNodeOp { left: NodePropOp(id=3), right: ConstNodeOp(30), op: Gt } +//! +//! Phase 3 — Runtime (per-node, O(1)): +//! filter.apply(storage, vid) → age_value = NodePropOp.apply(...) +//! Prop::binary_cmp(Gt, age_value, 30) → true/false +//! ``` +//! +//! # Temporal quantification +//! +//! ```rust,ignore +//! // "pass if any temporal value of 'score' > 10" +//! NodeFilter::temporal_property("score").any().gt(10i64) +//! ──► QuantifiedNodeFilter> +//! create_node_filter(graph)? +//! ──► AnyNodeOp { inner: PropListCompareOp { temporal_op, rhs: ConstNodeOp(10), op: Gt } } +//! +//! // "pass if sum of 'score' > 100" +//! NodeFilter::temporal_property("score").sum().gt(100i64) +//! ──► BinaryCmpNodeFilter, ConstExpr> +//! ``` + +use super::{ + ops::{ + AllNodeOp, AnyNodeOp, BinaryCmpNodeOp, PropListCompareOp, SetNodeOp, StringNodeOp, + UnaryNodeOp, + }, + AvgExpr, FirstExpr, LastExpr, LenExpr, MaxExpr, MinExpr, NodeExpr, SumExpr, + TemporalPropertyExpr, +}; +use crate::{ + db::{ + api::{state::ops::NodeOp, view::internal::GraphView}, + graph::views::filter::{ + model::{ + edge_filter::CompositeEdgeFilter, + filter_operator::{ + BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp, + }, + ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateFilter, + CreateView, TryAsCompositeFilter, + }, + node_filtered_graph::NodeFilteredGraph, + }, + }, + errors::GraphError, + prelude::GraphViewOps, +}; +use raphtory_api::core::entities::{ + properties::prop::{Prop, PropType}, + VID, +}; +use std::{collections::HashSet, hash::Hash, marker::PhantomData, sync::Arc}; + +// ───────────────────────────────────────────────────────────────────────────── +// Sealed trait for QuantifierMode +// ───────────────────────────────────────────────────────────────────────────── + +mod sealed { + pub trait Sealed {} +} + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifierMode — AnyMode / AllMode +// ───────────────────────────────────────────────────────────────────────────── + +/// Sealed marker trait used as a type parameter on [`QuantifiedNodeFilter`] and +/// [`Quantified`] to distinguish `any` vs `all` semantics at compile time. +/// Never instantiated — only used as `` / `` in type positions. +pub trait QuantifierMode: sealed::Sealed + Clone + Copy + Send + Sync + 'static {} + +/// Marker for "pass if *any* temporal value matches" — used as `Q` in +/// `QuantifiedNodeFilter`. Selects [`AnyNodeOp`] at compile time. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AnyMode; + +/// Marker for "pass if *all* temporal values match" — used as `Q` in +/// `QuantifiedNodeFilter`. Selects [`AllNodeOp`] at compile time. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct AllMode; + +impl sealed::Sealed for AnyMode {} +impl sealed::Sealed for AllMode {} +impl QuantifierMode for AnyMode {} +impl QuantifierMode for AllMode {} + +// ───────────────────────────────────────────────────────────────────────────── +// BinaryCmpNodeFilter — binary expression filter +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that compares two [`NodeExpr`] values using a [`BinaryOp`]. +/// +/// The output type is determined by the left expression (`L::Output`); +/// the right expression must produce the same type. +/// +/// Created by [`NodeExprFilterOps`] methods (`.gt`, `.lt`, `.eq`, `.ne`, `.ge`, `.le`). +/// Compiles to a `BinaryCmpNodeOp` wrapped in `Arc>`. +/// +/// ```rust,ignore +/// NodeFilter::degree().gt(2usize) +/// → BinaryCmpNodeFilter, usize> +/// → BinaryCmpNodeOp { left: Degree(..), right: ConstNodeOp(2), op: Gt } +/// +/// NodeFilter::property("age").eq(30i64) +/// → BinaryCmpNodeFilter +/// → BinaryCmpNodeOp { left: NodePropOp(prop_id=N), right: ConstNodeOp(30), op: Eq } +/// ``` +pub struct BinaryCmpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ + pub left: L, + pub op: BinaryOp, + pub right: R, +} + +impl BinaryCmpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ + pub fn new(left: L, op: BinaryOp, right: R) -> Self { + Self { left, op, right } + } +} + +impl Clone for BinaryCmpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ + fn clone(&self) -> Self { + Self { + left: self.left.clone(), + op: self.op, + right: self.right.clone(), + } + } +} + +impl ComposableFilter for BinaryCmpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ +} + +/// Reject ordering operators on boolean properties. +fn validate_binary_op(op: &BinaryOp, prop_type: &PropType) -> Result<(), GraphError> { + if *prop_type != PropType::Empty + && matches!( + op, + BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge + ) + && *prop_type == PropType::Bool + { + return Err(GraphError::InvalidFilter(format!( + "operator {:?} is not valid for boolean properties", + op + ))); + } + Ok(()) +} + +/// Reject string operators on non-string properties. +/// +/// Only fires when the type is known (`!= PropType::Empty`). +fn validate_string_op(prop_type: &PropType) -> Result<(), GraphError> { + if *prop_type != PropType::Empty && *prop_type != PropType::Str { + return Err(GraphError::InvalidFilter(format!( + "string operator requires a Str property, but the property type is {}", + prop_type + ))); + } + Ok(()) +} + +impl CreateFilter for BinaryCmpNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: Comparable, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone())?; + Ok(NodeFilteredGraph::new(graph, filter)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + validate_binary_op(&self.op, &left.prop_type())?; + Ok(Arc::new(BinaryCmpNodeOp { + left, + right, + op: self.op, + })) + } +} + +impl TryAsCompositeFilter for BinaryCmpNodeFilter +where + L: NodeExpr, + R: NodeExpr, +{ + 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 { + Err(GraphError::NotSupported) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnaryNodeFilter — is_some / is_none on nullable expressions +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that tests the presence of an `Option`-valued expression. +/// +/// Created by `.is_some()` / `.is_none()` on any `NodeExpr>`. +/// Compiles to a `UnaryNodeOp { inner, op }`. +/// +/// ```rust,ignore +/// NodeFilter::property("age").is_some() +/// → UnaryNodeFilter +/// → UnaryNodeOp { inner: NodePropOp(prop_id=N), op: IsSome } +/// ``` +pub struct UnaryNodeFilter +where + E: NodeExpr>, + I: Clone + Send + Sync + 'static, +{ + pub expr: E, + pub op: UnaryOp, + pub(crate) _phantom: PhantomData, +} + +impl Clone for UnaryNodeFilter +where + E: NodeExpr>, + I: Clone + Send + Sync + 'static, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + op: self.op, + _phantom: PhantomData, + } + } +} + +impl ComposableFilter for UnaryNodeFilter +where + E: NodeExpr>, + I: Clone + Send + Sync + 'static, +{ +} + +impl CreateFilter for UnaryNodeFilter +where + E: NodeExpr>, + I: Clone + Send + Sync + 'static, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = UnaryNodeOp<'graph, I>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone())?; + Ok(NodeFilteredGraph::new(graph, filter)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = self.expr.create_node_op(graph)?; + Ok(UnaryNodeOp { inner, op: self.op }) + } +} + +impl TryAsCompositeFilter for UnaryNodeFilter +where + E: NodeExpr>, + I: Clone + Send + Sync + 'static, +{ + 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 { + Err(GraphError::NotSupported) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// SetNodeFilter — is_in / is_not_in on nullable expressions +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that checks whether an `Option`-valued expression is contained +/// in (or absent from) a fixed set of values. +/// +/// Created by `.is_in(values)` / `.is_not_in(values)`. +/// Compiles to a `SetNodeOp { inner, op, values }`. +/// +/// ```rust,ignore +/// NodeFilter::node_type().is_in(["Person", "Account"]) +/// → SetNodeFilter +/// → SetNodeOp { inner: TypeOp, op: IsIn, values: {"Person", "Account"} } +/// ``` +pub struct SetNodeFilter +where + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{ + pub expr: E, + pub op: SetOp, + pub values: Arc>, + pub(crate) _phantom: PhantomData, +} + +impl Clone for SetNodeFilter +where + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + op: self.op, + values: self.values.clone(), + _phantom: PhantomData, + } + } +} + +impl ComposableFilter for SetNodeFilter +where + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{ +} + +impl CreateFilter for SetNodeFilter +where + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = SetNodeOp<'graph, I>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone())?; + Ok(NodeFilteredGraph::new(graph, filter)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = self.expr.create_node_op(graph)?; + Ok(SetNodeOp { + inner, + op: self.op, + values: self.values, + }) + } +} + +impl TryAsCompositeFilter for SetNodeFilter +where + E: NodeExpr>, + I: Eq + Hash + Clone + Send + Sync + 'static, +{ + 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 { + Err(GraphError::NotSupported) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// StringNodeFilter — string expression filter +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that applies a [`StringOp`] to two [`NodeExpr`] values. +/// +/// Both sides must produce the same string-comparable type (`L::Output: StringComparable`). +/// Created by the string methods on [`NodeExprFilterOps`] (`.starts_with`, `.ends_with`, +/// `.contains`, `.not_contains`, `.fuzzy_search`). +/// Compiles to a `StringNodeOp` wrapped in `Arc>`. +/// +/// ```rust,ignore +/// NodeFilter::name().starts_with("Al") +/// → StringNodeFilter +/// → StringNodeOp { left: NameOp, right: ConstNodeOp("Al"), op: StartsWith } +/// +/// NodeFilter::property("tag").contains(Prop::Str("foo".into())) +/// → StringNodeFilter +/// → StringNodeOp { left: NodePropOp(prop_id=N), right: ConstNodeOp(Str("foo")), op: Contains } +/// ``` +pub struct StringNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: StringComparable, +{ + pub left: L, + pub op: StringOp, + pub right: R, +} + +impl StringNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: StringComparable, +{ + pub fn new(left: L, op: StringOp, right: R) -> Self { + Self { left, op, right } + } +} + +impl Clone for StringNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: StringComparable, +{ + fn clone(&self) -> Self { + Self { + left: self.left.clone(), + op: self.op, + right: self.right.clone(), + } + } +} + +impl ComposableFilter for StringNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: StringComparable, +{ +} + +impl CreateFilter for StringNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: StringComparable, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = + NodeFilteredGraph>; + + type NodeFilter<'graph, G: GraphView + 'graph> = Arc + 'graph>; + + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone())?; + Ok(NodeFilteredGraph::new(graph, filter)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + let left = self.left.create_node_op(graph.clone())?; + let right = self.right.create_node_op(graph)?; + validate_string_op(&left.prop_type())?; + Ok(Arc::new(StringNodeOp { + left, + right, + op: self.op, + })) + } +} + +impl TryAsCompositeFilter for StringNodeFilter +where + L: NodeExpr, + R: NodeExpr, + L::Output: StringComparable, +{ + 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 { + Err(GraphError::NotSupported) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// QuantifiedNodeFilter — leaf filter wrapping a quantified comparison +// ───────────────────────────────────────────────────────────────────────────── + +/// A node filter that applies a [`BinaryOp`] to every temporal value and reduces +/// the results using `Q` ([`AnyMode`] or [`AllMode`]). +/// +/// Not constructed directly — returned by `Quantified::gt/eq/…`: +/// ```rust,ignore +/// // NodeFilter::temporal_property("score").any().gt(10i64) +/// // → QuantifiedNodeFilter, AnyMode, i64> +/// // compiles to: AnyNodeOp { inner: PropListCompareOp { …, op: Gt } } +/// +/// // NodeFilter::temporal_property("score").all().gt(0i64) +/// // → QuantifiedNodeFilter, AllMode, i64> +/// // compiles to: AllNodeOp { inner: PropListCompareOp { …, op: Gt } } +/// ``` +pub struct QuantifiedNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, + R: NodeExpr>, +{ + pub expr: E, + pub rhs: R, + pub op: BinaryOp, + pub(crate) _q: PhantomData, +} + +impl QuantifiedNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, + R: NodeExpr>, +{ + pub fn new(expr: E, op: BinaryOp, rhs: R) -> Self { + Self { + expr, + rhs, + op, + _q: PhantomData, + } + } +} + +impl Clone for QuantifiedNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, + R: NodeExpr>, +{ + fn clone(&self) -> Self { + Self { + expr: self.expr.clone(), + rhs: self.rhs.clone(), + op: self.op, + _q: PhantomData, + } + } +} + +impl ComposableFilter for QuantifiedNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, + R: NodeExpr>, +{ +} + +impl CreateFilter for QuantifiedNodeFilter +where + E: NodeExpr, + R: NodeExpr>, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; + type NodeFilter<'graph, G: GraphView + 'graph> = AnyNodeOp<'graph>; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone())?; + Ok(NodeFilteredGraph::new(graph, filter)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = Arc::new(PropListCompareOp { + inner: self.expr.create_node_op(graph.clone())?, + rhs: self.rhs.create_node_op(graph)?, + op: self.op, + }); + Ok(AnyNodeOp { inner }) + } +} + +impl CreateFilter for QuantifiedNodeFilter +where + E: NodeExpr, + R: NodeExpr>, +{ + type EntityFiltered<'graph, G: GraphViewOps<'graph>> = NodeFilteredGraph>; + type NodeFilter<'graph, G: GraphView + 'graph> = AllNodeOp<'graph>; + type FilteredGraph<'graph, G> + = G + where + Self: 'graph, + G: GraphViewOps<'graph>; + + fn create_filter<'graph, G: GraphViewOps<'graph>>( + self, + graph: G, + ) -> Result, GraphError> { + let filter = self.create_node_filter(graph.clone())?; + Ok(NodeFilteredGraph::new(graph, filter)) + } + + fn create_node_filter<'graph, G: GraphView + 'graph>( + self, + graph: G, + ) -> Result, GraphError> { + let inner = Arc::new(PropListCompareOp { + inner: self.expr.create_node_op(graph.clone())?, + rhs: self.rhs.create_node_op(graph)?, + op: self.op, + }); + Ok(AllNodeOp { inner }) + } +} + +impl TryAsCompositeFilter for QuantifiedNodeFilter +where + E: NodeExpr, + Q: QuantifierMode, + R: NodeExpr>, +{ + 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 { + Err(GraphError::NotSupported) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Quantified / Aggregated / TemporalProp — intermediate types in the fluent chain +// ───────────────────────────────────────────────────────────────────────────── + +/// Returned by [`TemporalProp::any`] / [`TemporalProp::all`]. +/// +/// Carries the temporal expression `E` and the quantifier `Q` until a comparison +/// operator is called, which produces the final [`QuantifiedNodeFilter`]: +/// ```rust,ignore +/// NodeFilter::temporal_property("score").any() // → Quantified, AnyMode> +/// .gt(10i64) // → QuantifiedNodeFilter, AnyMode, i64> +/// ``` +pub struct Quantified +where + E: NodeExpr, + Q: QuantifierMode, +{ + pub(crate) expr: E, + pub(crate) _q: PhantomData, +} + +impl Quantified +where + E: NodeExpr, + Q: QuantifierMode, +{ + fn finish>>( + self, + op: BinaryOp, + rhs: R, + ) -> QuantifiedNodeFilter { + QuantifiedNodeFilter::new(self.expr, op, rhs) + } + + pub fn eq>>(self, rhs: R) -> QuantifiedNodeFilter { + self.finish(BinaryOp::Eq, rhs) + } + + pub fn ne>>(self, rhs: R) -> QuantifiedNodeFilter { + self.finish(BinaryOp::Ne, rhs) + } + + pub fn gt>>(self, rhs: R) -> QuantifiedNodeFilter { + self.finish(BinaryOp::Gt, rhs) + } + + pub fn ge>>(self, rhs: R) -> QuantifiedNodeFilter { + self.finish(BinaryOp::Ge, rhs) + } + + pub fn lt>>(self, rhs: R) -> QuantifiedNodeFilter { + self.finish(BinaryOp::Lt, rhs) + } + + pub fn le>>(self, rhs: R) -> QuantifiedNodeFilter { + self.finish(BinaryOp::Le, rhs) + } +} + +/// Returned by [`TemporalProp::sum`], `.avg()`, `.min()` etc. +/// +/// Wraps the aggregator expression `E` (e.g. `SumExpr>`) until +/// a comparison operator is called, which produces a [`BinaryCmpNodeFilter`]: +/// ```rust,ignore +/// NodeFilter::temporal_property("price").sum() // → Aggregated>> +/// .gt(100i64) // → BinaryCmpNodeFilter>, i64> +/// ``` +pub struct Aggregated { + pub(crate) expr: E, +} + +impl Aggregated { + fn finish>( + self, + op: BinaryOp, + rhs: R, + ) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self.expr, op, rhs) + } + + pub fn eq>(self, rhs: R) -> BinaryCmpNodeFilter { + self.finish(BinaryOp::Eq, rhs) + } + + pub fn ne>(self, rhs: R) -> BinaryCmpNodeFilter { + self.finish(BinaryOp::Ne, rhs) + } + + pub fn gt>(self, rhs: R) -> BinaryCmpNodeFilter { + self.finish(BinaryOp::Gt, rhs) + } + + pub fn ge>(self, rhs: R) -> BinaryCmpNodeFilter { + self.finish(BinaryOp::Ge, rhs) + } + + pub fn lt>(self, rhs: R) -> BinaryCmpNodeFilter { + self.finish(BinaryOp::Lt, rhs) + } + + pub fn le>(self, rhs: R) -> BinaryCmpNodeFilter { + self.finish(BinaryOp::Le, rhs) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalProp — entry point returned from `.temporal_property(name)` +// ───────────────────────────────────────────────────────────────────────────── + +/// Entry point returned by `NodeFilter::temporal_property(name)`. +/// +/// `E` is the view expression (e.g. `NodeFilter`, `Windowed`, `Layered`) +/// that scopes which temporal property values are visible. +/// +/// Calling a method produces the next step in the chain: +/// ```rust,ignore +/// NodeFilter::temporal_property("score") // → TemporalProp +/// .any() // → Quantified, AnyMode> +/// .gt(10i64) // → QuantifiedNodeFilter<.., AnyMode, i64> +/// +/// NodeFilter::temporal_property("price") // → TemporalProp +/// .sum() // → Aggregated>> +/// .gt(100i64) // → BinaryCmpNodeFilter, i64> +/// +/// NodeFilter.window(0, 100) +/// .temporal_property("score") // → TemporalProp> +/// .any().gt(10i64) +/// ``` +pub struct TemporalProp { + pub(crate) view_expr: E, + pub(crate) name: String, +} + +impl TemporalProp { + pub(crate) fn new(view_expr: E, name: impl Into) -> Self { + Self { + view_expr, + name: name.into(), + } + } + + fn make_expr(self) -> TemporalPropertyExpr { + TemporalPropertyExpr { + view_expr: self.view_expr, + name: self.name, + } + } + + pub fn any(self) -> Quantified, AnyMode> { + Quantified { + expr: self.make_expr(), + _q: PhantomData, + } + } + + pub fn all(self) -> Quantified, AllMode> { + Quantified { + expr: self.make_expr(), + _q: PhantomData, + } + } + + pub fn sum(self) -> Aggregated>> { + Aggregated { + expr: SumExpr(self.make_expr()), + } + } + + pub fn avg(self) -> Aggregated>> { + Aggregated { + expr: AvgExpr(self.make_expr()), + } + } + + pub fn min(self) -> Aggregated>> { + Aggregated { + expr: MinExpr(self.make_expr()), + } + } + + pub fn max(self) -> Aggregated>> { + Aggregated { + expr: MaxExpr(self.make_expr()), + } + } + + pub fn first(self) -> Aggregated>> { + Aggregated { + expr: FirstExpr(self.make_expr()), + } + } + + pub fn last(self) -> Aggregated>> { + Aggregated { + expr: LastExpr(self.make_expr()), + } + } + + pub fn len(self) -> Aggregated>> { + Aggregated { + expr: LenExpr(self.make_expr()), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NodeExprFilterOps — comparison and set operators on NodeExpr +// ───────────────────────────────────────────────────────────────────────────── + +/// Comparison, string, set, and presence operators on any [`NodeExpr`]. +/// +/// ```rust,ignore +/// DegreeExpr(Direction::BOTH).gt(2usize) +/// DegreeExpr(Direction::OUT).gt(DegreeExpr(Direction::IN)) +/// NodeFilter::property("age").gt(30i64) +/// DegreeExpr(Direction::BOTH).is_in([2usize, 3usize]) +/// ``` +pub trait NodeExprFilterOps: NodeExpr + Sized { + fn gt>(self, rhs: R) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self, BinaryOp::Gt, rhs) + } + + fn ge>(self, rhs: R) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self, BinaryOp::Ge, rhs) + } + + fn lt>(self, rhs: R) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self, BinaryOp::Lt, rhs) + } + + fn le>(self, rhs: R) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self, BinaryOp::Le, rhs) + } + + fn eq>(self, rhs: R) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self, BinaryOp::Eq, rhs) + } + + fn ne>(self, rhs: R) -> BinaryCmpNodeFilter { + BinaryCmpNodeFilter::new(self, BinaryOp::Ne, rhs) + } + + fn starts_with>(self, rhs: R) -> StringNodeFilter + where + Self::Output: StringComparable, + { + StringNodeFilter::new(self, StringOp::StartsWith, rhs) + } + + fn ends_with>(self, rhs: R) -> StringNodeFilter + where + Self::Output: StringComparable, + { + StringNodeFilter::new(self, StringOp::EndsWith, rhs) + } + + fn contains>(self, rhs: R) -> StringNodeFilter + where + Self::Output: StringComparable, + { + StringNodeFilter::new(self, StringOp::Contains, rhs) + } + + fn not_contains>(self, rhs: R) -> StringNodeFilter + where + Self::Output: StringComparable, + { + StringNodeFilter::new(self, StringOp::NotContains, rhs) + } + + fn fuzzy_search>( + self, + rhs: R, + levenshtein_distance: usize, + prefix_match: bool, + ) -> StringNodeFilter + where + Self::Output: StringComparable, + { + StringNodeFilter::new( + self, + StringOp::FuzzySearch { + levenshtein_distance, + prefix_match, + }, + rhs, + ) + } + + fn is_some(self) -> UnaryNodeFilter + where + Self: NodeExpr>, + Inner: Clone + Send + Sync + 'static, + { + UnaryNodeFilter { + expr: self, + op: UnaryOp::IsSome, + _phantom: PhantomData, + } + } + + fn is_none(self) -> UnaryNodeFilter + where + Self: NodeExpr>, + Inner: Clone + Send + Sync + 'static, + { + UnaryNodeFilter { + expr: self, + op: UnaryOp::IsNone, + _phantom: PhantomData, + } + } + + fn is_true(self) -> BinaryCmpNodeFilter + where + Self: NodeExpr>, + { + self.eq(Prop::Bool(true)) + } + + fn is_false(self) -> BinaryCmpNodeFilter + where + Self: NodeExpr>, + { + self.eq(Prop::Bool(false)) + } + + fn is_in(self, values: Iter) -> SetNodeFilter + where + Self: NodeExpr>, + Inner: Eq + Hash + Clone + Send + Sync + 'static, + Iter: IntoIterator, + { + let set: HashSet<_> = values.into_iter().collect(); + SetNodeFilter { + expr: self, + op: SetOp::IsIn, + values: Arc::new(set), + _phantom: PhantomData, + } + } + + fn is_not_in(self, values: Iter) -> SetNodeFilter + where + Self: NodeExpr>, + Inner: Eq + Hash + Clone + Send + Sync + 'static, + Iter: IntoIterator, + { + let set: HashSet<_> = values.into_iter().collect(); + SetNodeFilter { + expr: self, + op: SetOp::IsNotIn, + values: Arc::new(set), + _phantom: PhantomData, + } + } +} + +impl NodeExprFilterOps for E {} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalExprOps — blanket trait for E: NodeExpr +// ───────────────────────────────────────────────────────────────────────────── + +/// Quantifier and aggregator operators for temporal property sequences. +/// +/// Available on any `NodeExpr` that returns a `Prop::List` (e.g. [`TemporalPropertyExpr`]). +pub trait TemporalExprOps: NodeExpr + Sized { + fn any(self) -> Quantified { + Quantified { + expr: self, + _q: PhantomData, + } + } + + fn all(self) -> Quantified { + Quantified { + expr: self, + _q: PhantomData, + } + } + + fn sum(self) -> SumExpr { + SumExpr(self) + } + + fn avg(self) -> AvgExpr { + AvgExpr(self) + } + + fn min(self) -> MinExpr { + MinExpr(self) + } + + fn max(self) -> MaxExpr { + MaxExpr(self) + } + + fn first(self) -> FirstExpr { + FirstExpr(self) + } + + fn last(self) -> LastExpr { + LastExpr(self) + } + + fn len(self) -> LenExpr { + LenExpr(self) + } +} + +impl> TemporalExprOps for E {} diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs new file mode 100644 index 0000000000..0d14e116a6 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/mod.rs @@ -0,0 +1,55 @@ +use crate::{ + db::api::{state::ops::NodeOp, view::internal::GraphView}, + errors::GraphError, +}; +use raphtory_api::core::entities::properties::prop::PropType; +use std::sync::Arc; + +pub mod exprs; +pub mod filters; +pub mod ops; + +#[cfg(test)] +mod tests; + +pub use exprs::*; +pub use filters::*; +pub use ops::*; + +// ───────────────────────────────────────────────────────────────────────────── +// NodeExpr — typed node expression with associated Output type +// ───────────────────────────────────────────────────────────────────────────── + +/// A typed expression that produces a value per node. +/// +/// `Output` carries nullability only where the value can genuinely be absent: +/// `Option` for properties/metadata, `Option` for node type. +/// Always-present values use non-optional types: `usize` for degree, `String` for name. +/// +/// Calling `create_node_op` resolves name→ID lookups once against the graph, +/// returning a `NodeOp` that evaluates in O(1) per node. +/// +/// Usage: +/// ```rust,ignore +/// NodeFilter::degree().gt(2usize) +/// NodeFilter::out_degree().gt(NodeFilter::in_degree()) +/// NodeFilter::property("age").gt(30i64) +/// NodeFilter::name().eq("Alice") +/// ``` +/// +pub trait NodeExpr: Clone + Send + Sync + 'static { + type Output: Clone + Send + Sync + 'static; + + /// Compile the expression against a specific graph view. + /// + /// Any name→ID resolution (property, metadata) happens here, once. + fn create_node_op<'g, G: GraphView + 'g>( + &self, + graph: G, + ) -> Result + 'g>, GraphError>; + + /// A priory known type (for early validation where possible) + fn prop_type(&self) -> PropType { + PropType::Empty + } +} diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs new file mode 100644 index 0000000000..df8da53159 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/ops.rs @@ -0,0 +1,443 @@ +//! Runtime evaluators — given a node ID, return a typed value. +//! +//! A [`NodeOp`] is a *compiled* expression: name→ID lookups are resolved and the +//! op holds a reference to the graph view it was compiled against. +//! `apply(storage, vid)` returns the value in O(1). +//! +//! Ops are produced by [`NodeExpr::create_node_op`] — never constructed directly. +//! +//! # Evaluation pipeline +//! +//! ```text +//! NodeFilter::property("age") ← NodeExpr (pure data) +//! .create_node_op(graph)? ← resolve "age" → prop_id = 3 +//! ──► NodePropOp { graph, prop_id: 3 } ← NodeOp: apply() reads column 3 in O(1) +//! +//! NodeFilter::property("age").gt(30i64) ← BinaryCmpNodeFilter (pure data) +//! .create_node_filter(graph)? +//! ──► BinaryCmpNodeOp { left: NodePropOp, right: ConstNodeOp(30), op: Gt } +//! apply: Prop::binary_cmp(Gt, age_value, 30) +//! +//! NodeFilter::temporal_property("score").sum() ← SumExpr (pure data) +//! .create_node_op(graph)? +//! ──► SumNodeOp { inner: TemporalNodePropOp { graph, prop_id: 7 } } +//! apply: collect Prop::List temporal values, then aggregate_values(Sum) +//! ``` +//! +//! # Quantified evaluation +//! +//! [`PropListCompareOp`] applies a [`BinaryOp`] element-wise to a `Prop::List`, +//! then [`AnyNodeOp`] / [`AllNodeOp`] reduce the boolean list: +//! +//! ```text +//! temporal values = [8, 12, 5], rhs = 10 +//! PropListCompareOp(Gt) → Prop::List([false, true, false]) +//! AnyNodeOp → true (at least one matched) +//! AllNodeOp → false (not all matched) +//! ``` + +use crate::{ + db::{ + api::{ + properties::PropertiesOps, + state::ops::NodeOp, + view::{internal::GraphView, NodeViewOps}, + }, + graph::views::filter::model::{ + filter_operator::{BinaryOp, Comparable, SetOp, StringComparable, StringOp, UnaryOp}, + property_filter::{evaluate::aggregate_values, Op}, + }, + }, + prelude::GraphViewOps, +}; +use raphtory_api::core::entities::{ + properties::prop::{Prop, PropArray, PropType}, + VID, +}; +use raphtory_storage::graph::graph::GraphStorage; +use std::{collections::HashSet, hash::Hash, sync::Arc}; + +// ───────────────────────────────────────────────────────────────────────────── +// NodePropOp — latest property value by pre-resolved column ID +// ───────────────────────────────────────────────────────────────────────────── + +/// Internal op produced by [`Property::create_node_op`] — not constructed directly. +/// +/// `Property("age")` resolves `"age"` → `prop_id` once at compile time; +/// every `apply` call then reads column `prop_id` in O(1). +#[derive(Clone)] +pub(crate) struct NodePropOp { + pub(crate) graph: G, + pub(crate) prop_id: usize, +} + +impl NodeOp for NodePropOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { + self.graph.node(node)?.properties().get_by_id(self.prop_id) + } + + fn prop_type(&self) -> PropType { + self.graph + .node_meta() + .temporal_prop_mapper() + .get_dtype(self.prop_id) + .unwrap_or_default() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// NodeMetaOp — static metadata field by pre-resolved column ID +// ───────────────────────────────────────────────────────────────────────────── + +/// Internal op produced by [`Metadata::create_node_op`] — not constructed directly. +/// +/// Same as [`NodePropOp`] but reads from the static metadata column instead of +/// temporal properties. +#[derive(Clone)] +pub(crate) struct NodeMetaOp { + pub(crate) graph: G, + pub(crate) prop_id: usize, +} + +impl NodeOp for NodeMetaOp { + type Output = Option; + + fn apply(&self, _storage: &GraphStorage, node: VID) -> Option { + self.graph.node(node)?.metadata().get_by_id(self.prop_id) + } + + fn prop_type(&self) -> PropType { + self.graph + .node_meta() + .metadata_mapper() + .get_dtype(self.prop_id) + .unwrap_or_default() + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// TemporalNodePropOp — all temporal values for a property within the window +// ───────────────────────────────────────────────────────────────────────────── + +/// Internal op produced by [`TemporalPropertyExpr::create_node_op`] — not constructed directly. +/// +/// Collects all recorded values within the current view window into a `Prop::List`. +/// That list is then consumed by aggregator ops (`SumNodeOp`, `LenNodeOp`, …) or +/// by `PropListCompareOp` for quantified comparisons. +#[derive(Clone)] +pub(crate) struct TemporalNodePropOp { + pub(crate) graph: G, + pub(crate) prop_id: usize, +} + +impl NodeOp for TemporalNodePropOp { + type Output = Prop; + + fn apply(&self, _storage: &GraphStorage, node: VID) -> Prop { + let vals: Vec = (&&self.graph) + .node(node) + .and_then(|n| { + n.properties() + .temporal() + .get_by_id(self.prop_id) + .map(|tpv| tpv.values().collect()) + }) + .unwrap_or_default(); + Prop::List(PropArray::from(vals)) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Aggregator NodeOps — compile-time resolved against a concrete graph view +// +// Each is an internal op produced by its corresponding expr's create_node_op: +// SumExpr::create_node_op → SumNodeOp (Output = Option) +// AvgExpr::create_node_op → AvgNodeOp (Output = Option) +// MinExpr::create_node_op → MinNodeOp (Output = Option) +// MaxExpr::create_node_op → MaxNodeOp (Output = Option) +// FirstExpr::create_node_op → FirstNodeOp (Output = Option) +// LastExpr::create_node_op → LastNodeOp (Output = Option) +// LenExpr::create_node_op → LenNodeOp (Output = usize) +// ───────────────────────────────────────────────────────────────────────────── + +macro_rules! impl_agg_node_op { + ($name:ident, $output:ty, $body:expr) => { + pub struct $name<'g> { + pub(crate) inner: Arc + 'g>, + } + + impl<'g> Clone for $name<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } + } + + impl<'g> NodeOp for $name<'g> { + type Output = $output; + + fn apply(&self, storage: &GraphStorage, node: VID) -> $output { + let vals: Vec = match self.inner.apply(storage, node) { + Prop::List(arr) => arr.iter().collect(), + _ => vec![], + }; + ($body)(vals) + } + } + }; +} + +impl_agg_node_op!(SumNodeOp, Option, |vals: Vec| { + aggregate_values(&vals, Op::Sum) +}); +impl_agg_node_op!(AvgNodeOp, Option, |vals: Vec| { + aggregate_values(&vals, Op::Avg) +}); +impl_agg_node_op!(MinNodeOp, Option, |vals: Vec| { + aggregate_values(&vals, Op::Min) +}); +impl_agg_node_op!(MaxNodeOp, Option, |vals: Vec| { + aggregate_values(&vals, Op::Max) +}); +impl_agg_node_op!(FirstNodeOp, Option, |vals: Vec| { + vals.into_iter().next() +}); +impl_agg_node_op!(LastNodeOp, Option, |vals: Vec| { + vals.into_iter().last() +}); +impl_agg_node_op!(LenNodeOp, usize, |vals: Vec| { vals.len() }); + +// ───────────────────────────────────────────────────────────────────────────── +// AnyNodeOp / AllNodeOp — unary reducers over a Prop::List of booleans +// ───────────────────────────────────────────────────────────────────────────── + +fn prop_any(prop: &Prop) -> bool { + match prop { + Prop::Bool(b) => *b, + Prop::List(arr) => arr.iter().any(|p| prop_any(&p)), + _ => false, + } +} + +fn prop_all(prop: &Prop) -> bool { + match prop { + Prop::Bool(b) => *b, + Prop::List(arr) => !arr.is_empty() && arr.iter().all(|p| prop_all(&p)), + _ => false, + } +} + +/// Internal op produced by `QuantifiedNodeFilter<_, AnyMode, _>::create_node_filter`. +/// +/// Wraps a `PropListCompareOp` and returns `true` if at least one element of the +/// resulting `Prop::List([Bool, …])` is `true`. +/// +/// e.g. `NodeFilter::temporal_property("score").any().gt(10i64)` ultimately compiles +/// to `AnyNodeOp { inner: PropListCompareOp { …, op: Gt } }`. +pub struct AnyNodeOp<'g> { + pub(crate) inner: Arc + 'g>, +} + +impl<'g> Clone for AnyNodeOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl<'g> NodeOp for AnyNodeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + prop_any(&self.inner.apply(storage, node)) + } +} + +/// Internal op produced by `QuantifiedNodeFilter<_, AllMode, _>::create_node_filter`. +/// +/// Like [`AnyNodeOp`] but returns `true` only if every element is `true` +/// (and the list is non-empty). +pub struct AllNodeOp<'g> { + pub(crate) inner: Arc + 'g>, +} + +impl<'g> Clone for AllNodeOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl<'g> NodeOp for AllNodeOp<'g> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + prop_all(&self.inner.apply(storage, node)) + } +} + +/// Internal op produced inside `QuantifiedNodeFilter::create_node_filter`. +/// +/// Applies `BinaryOp` element-wise to a `Prop::List` (from `TemporalNodePropOp`) +/// against a scalar RHS, producing `Prop::List([Bool, Bool, …])`. +/// That boolean list is then reduced by [`AnyNodeOp`] or [`AllNodeOp`]. +pub(crate) struct PropListCompareOp<'g> { + pub(crate) inner: Arc + 'g>, + pub(crate) rhs: Arc> + 'g>, + pub(crate) op: BinaryOp, +} + +impl<'g> Clone for PropListCompareOp<'g> { + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + rhs: self.rhs.clone(), + op: self.op, + } + } +} + +impl<'g> NodeOp for PropListCompareOp<'g> { + type Output = Prop; + + fn apply(&self, storage: &GraphStorage, node: VID) -> Prop { + let Some(rhs) = self.rhs.apply(storage, node) else { + return Prop::List(PropArray::from(vec![])); + }; + let prop = self.inner.apply(storage, node); + match prop { + Prop::List(arr) => { + let bools: Vec = arr + .iter() + .map(|v| Prop::Bool(Prop::binary_cmp(&self.op, &v, &rhs))) + .collect(); + Prop::List(PropArray::from(bools)) + } + other => Prop::Bool(Prop::binary_cmp(&self.op, &other, &rhs)), + } + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// BinaryCmpNodeOp<'g, T> — compares two NodeOp using BinaryOp +// ───────────────────────────────────────────────────────────────────────────── + +/// Internal op produced by [`BinaryCmpNodeFilter::create_node_filter`]. +/// +/// Holds two compiled `NodeOp` and applies `T::binary_cmp` per node. +/// The `'g` lifetime bounds both ops to the graph view they were compiled against. +/// +/// e.g. `NodeFilter::property("age").gt(30i64)` compiles to: +/// `BinaryCmpNodeOp { left: NodePropOp(prop_id=3), right: ConstNodeOp(30), op: Gt }` +#[derive(Clone)] +pub struct BinaryCmpNodeOp<'g, T: Comparable> { + pub(crate) left: Arc + 'g>, + pub(crate) right: Arc + 'g>, + pub(crate) op: BinaryOp, +} + +impl<'g, T: Comparable + Clone + Send + Sync + 'static> NodeOp for BinaryCmpNodeOp<'g, T> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + let lv = self.left.apply(storage, node); + let rv = self.right.apply(storage, node); + T::binary_cmp(&self.op, &lv, &rv) + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// StringNodeOp<'g, T> — applies a StringOp to two NodeOp +// ───────────────────────────────────────────────────────────────────────────── + +/// Internal op produced by [`StringNodeFilter::create_node_filter`]. +/// +/// e.g. `NodeFilter::name().starts_with("Al")` compiles to: +/// `StringNodeOp { left: NameOp, right: ConstNodeOp("Al"), op: StartsWith }` +#[derive(Clone)] +pub struct StringNodeOp<'g, T: StringComparable> { + pub(crate) left: Arc + 'g>, + pub(crate) right: Arc + 'g>, + pub(crate) op: StringOp, +} + +impl<'g, T: StringComparable> NodeOp for StringNodeOp<'g, T> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + T::string_cmp( + &self.op, + &self.left.apply(storage, node), + &self.right.apply(storage, node), + ) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// UnaryNodeOp<'g, T> — evaluates is_some / is_none +// ───────────────────────────────────────────────────────────────────────────── + +/// Internal op produced by [`UnaryNodeFilter::create_node_filter`]. +/// +/// e.g. `NodeFilter::property("age").is_some()` compiles to: +/// `UnaryNodeOp { inner: NodePropOp(prop_id=3), op: IsSome }` +#[derive(Clone)] +pub struct UnaryNodeOp<'g, I: Clone + Send + Sync + 'static> { + pub(crate) inner: Arc> + 'g>, + pub(crate) op: UnaryOp, +} + +impl<'g, I: Clone + Send + Sync + 'static> NodeOp for UnaryNodeOp<'g, I> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + let v = self.inner.apply(storage, node); + match self.op { + UnaryOp::IsSome => v.is_some(), + UnaryOp::IsNone => v.is_none(), + } + } + + fn prop_type(&self) -> PropType { + PropType::Bool + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// SetNodeOp<'g, T> — evaluates is_in / is_not_in +// ───────────────────────────────────────────────────────────────────────────── + +/// Internal op produced by [`SetNodeFilter::create_node_filter`]. +/// +/// e.g. `NodeFilter::node_type().is_in(["Person", "Account"])` compiles to: +/// `SetNodeOp { inner: TypeOp, op: IsIn, values: {"Person", "Account"} }` +#[derive(Clone)] +pub struct SetNodeOp<'g, I: Eq + Hash + Clone + Send + Sync + 'static> { + pub(crate) inner: Arc> + 'g>, + pub(crate) op: SetOp, + pub(crate) values: Arc>, +} + +impl<'g, I: Eq + Hash + Clone + Send + Sync + 'static> NodeOp for SetNodeOp<'g, I> { + type Output = bool; + + fn apply(&self, storage: &GraphStorage, node: VID) -> bool { + let v = self.inner.apply(storage, node); + match self.op { + SetOp::IsIn => v.as_ref().map(|x| self.values.contains(x)).unwrap_or(false), + SetOp::IsNotIn => v + .as_ref() + .map(|x| !self.values.contains(x)) + .unwrap_or(false), + } + } +} diff --git a/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs b/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs new file mode 100644 index 0000000000..498e5286e8 --- /dev/null +++ b/raphtory/src/db/graph/views/filter/model/node_expr/tests.rs @@ -0,0 +1,561 @@ +use super::*; +use crate::{ + db::{ + api::{state::ops::Id, view::filter_ops::NodeSelect}, + graph::views::filter::{ + model::{ + filter_operator::BinaryOp, node_filter::NodeFilter, PropertyFilterFactory, + ViewWrapOps, + }, + CreateFilter, + }, + }, + prelude::{AdditionOps, Graph, GraphViewOps, NodeViewOps, NO_PROPS}, +}; +use raphtory_api::core::{ + entities::{ + properties::prop::{IntoProp, Prop}, + GID, + }, + Direction, +}; + +// Test graph: a→b, a→c, b→c +// All nodes have total degree 2; in-degrees: a=0, b=1, c=2 +fn build_test_graph() -> Graph { + let g = Graph::new(); + g.add_edge(0, "a", "b", NO_PROPS, None).unwrap(); + g.add_edge(0, "a", "c", NO_PROPS, None).unwrap(); + g.add_edge(0, "b", "c", NO_PROPS, None).unwrap(); + g +} + +fn filtered_names(filter: F, g: Graph) -> Vec +where + F: CreateFilter, + for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, +{ + let mut names: Vec = filter + .create_filter(g) + .unwrap() + .nodes() + .iter() + .map(|n| n.name()) + .collect(); + names.sort(); + names +} + +// ── DegreeExpr comparison operators ────────────────────────────────────── + +#[test] +fn degree_ge_2_keeps_all_nodes() { + let g = build_test_graph(); + assert_eq!( + filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .ge(2usize), + g + ), + vec!["a", "b", "c"] + ); +} + +#[test] +fn degree_eq_1_keeps_no_nodes() { + let g = build_test_graph(); + assert!(filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .eq(1usize), + g + ) + .is_empty()); +} + +#[test] +fn degree_le_2_keeps_all_nodes() { + let g = build_test_graph(); + assert_eq!( + filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .le(2usize), + g + ), + vec!["a", "b", "c"] + ); +} + +#[test] +fn degree_gt_2_keeps_no_nodes() { + let g = build_test_graph(); + assert!(filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .gt(2usize), + g + ) + .is_empty()); +} + +#[test] +fn degree_ne_2_keeps_no_nodes_when_all_are_2() { + let g = build_test_graph(); + assert!(filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .ne(2usize), + g + ) + .is_empty()); +} + +// ── expression-vs-expression: RHS can be another NodeExpr ──────────────── + +#[test] +fn total_gt_in_degree_selects_nodes_with_outgoing_edges() { + // total=2, in-degrees: a=0, b=1, c=2 → total > in for a and b only + let g = build_test_graph(); + assert_eq!( + filtered_names( + DegreeExpr { + dir: Direction::BOTH, + view_expr: NodeFilter + } + .gt(DegreeExpr { + dir: Direction::IN, + view_expr: NodeFilter + }), + g + ), + vec!["a", "b"] + ); +} + +// ── ConstExpr for custom output types ──────────────────────────────────── + +#[test] +fn const_expr_works() { + let filter = BinaryCmpNodeFilter::new(ConstExpr(2usize), BinaryOp::Eq, ConstExpr(2usize)); + let g = build_test_graph(); + assert_eq!(filtered_names(filter, g), vec!["a", "b", "c"]); +} + +#[test] +fn test_id_filter_expr() { + let g = Graph::new(); + g.add_node(0, 1, NO_PROPS, None, None).unwrap(); + g.add_node(0, 6, NO_PROPS, None, None).unwrap(); + let filter = Id.ge(GID::U64(5u64)); + + assert_eq!(g.nodes().select(filter).unwrap().id(), [6u64]) +} + +// ── Temporal property helpers ───────────────────────────────────────────── + +/// Graph with three nodes; "alice" has scores [1, 5, 10] at times 1, 2, 3 +/// "bob" has scores [2, 3] at times 1, 2 +/// "carol" has no score property +fn build_temporal_graph() -> Graph { + let g = Graph::new(); + g.add_node(1, "alice", [("score", 1i64.into_prop())], None, None) + .unwrap(); + g.add_node(2, "alice", [("score", 5i64.into_prop())], None, None) + .unwrap(); + g.add_node(3, "alice", [("score", 10i64.into_prop())], None, None) + .unwrap(); + g.add_node(1, "bob", [("score", 2i64.into_prop())], None, None) + .unwrap(); + g.add_node(2, "bob", [("score", 3i64.into_prop())], None, None) + .unwrap(); + g.add_node(1, "carol", NO_PROPS, None, None).unwrap(); + let _ = NodeFilter; // suppress unused warning + g +} + +fn temporal_filtered_names(filter: F, g: Graph) -> Vec +where + F: CreateFilter, + for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, +{ + let mut names: Vec = filter + .create_filter(g) + .unwrap() + .nodes() + .iter() + .map(|n| n.name()) + .collect(); + names.sort(); + names +} + +// ── any() quantifier ───────────────────────────────────────────────────── + +#[test] +fn temporal_any_eq_selects_nodes_with_matching_value() { + // alice has 1, 5, 10; bob has 2, 3; carol has none + // any == 5 → alice only + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").any().eq(5i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +#[test] +fn temporal_any_gt_selects_nodes_with_at_least_one_value_above_threshold() { + // any > 4 → alice (has 5, 10), not bob (max 3), not carol (none) + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").any().gt(4i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +#[test] +fn temporal_any_gt_both_nodes_qualify() { + // any > 1 → alice (5, 10), bob (2, 3) — both qualify + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").any().gt(1i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); +} + +// ── all() quantifier ───────────────────────────────────────────────────── + +#[test] +fn temporal_all_gt_requires_every_value() { + // all > 0 → alice (1,5,10 all > 0 ✓), bob (2,3 all > 0 ✓), carol excluded (empty) + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").all().gt(0i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); +} + +#[test] +fn temporal_all_gt_rejects_if_any_value_fails() { + // all > 4 → alice (1 fails) not included, bob (2, 3 fail) not included + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").all().gt(4i64); + assert!(temporal_filtered_names(filter, g).is_empty()); +} + +#[test] +fn temporal_all_requires_non_empty_sequence() { + // carol has no score → "all" over empty sequence returns false + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").all().ge(0i64); + let names = temporal_filtered_names(filter, g); + assert!(!names.contains(&"carol".to_string())); +} + +// ── sum() aggregator ────────────────────────────────────────────────────── + +#[test] +fn temporal_sum_gt_threshold() { + // alice sum = 16, bob sum = 5 → sum > 10 → alice only + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").sum().gt(10i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +#[test] +fn temporal_sum_eq() { + // bob sum = 5 → sum == 5 → bob only + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").sum().eq(5i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["bob"]); +} + +// ── first() / last() aggregators ───────────────────────────────────────── + +#[test] +fn temporal_first_value() { + // alice first = 1, bob first = 2 → first == 1 → alice only + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").first().eq(1i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +#[test] +fn temporal_last_value() { + // alice last = 10 → last > 9 → alice only + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").last().gt(9i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +// ── len() aggregator ────────────────────────────────────────────────────── + +#[test] +fn temporal_len_count() { + // alice has 3 updates, bob has 2 → len == 3 → alice only + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").len().eq(3usize); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +#[test] +fn temporal_len_ge_2() { + // alice (3), bob (2) both have len >= 2; carol has 0 + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").len().ge(2usize); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice", "bob"]); +} + +// ── NodeFilter entry point ──────────────────────────────────────────────── + +#[test] +fn node_filter_temporal_property_entry_point() { + let g = build_temporal_graph(); + let filter = NodeFilter.property("score").temporal().any().eq(5i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +// ── TemporalExprOps blanket ─────────────────────────────────────────────── + +#[test] +fn temporal_expr_ops_blanket_any() { + // Using the blanket TemporalExprOps on TemporalPropertyExpr directly + let g = build_temporal_graph(); + let filter = TemporalPropertyExpr::new("score").any().eq(10i64); + assert_eq!(temporal_filtered_names(filter, g), vec!["alice"]); +} + +// ── Windowed temporal filter ────────────────────────────────────────────── + +/// Apply a windowed temporal filter directly (view is embedded in the expression). +fn windowed_filtered_names(filter: F, g: Graph) -> Vec +where + F: CreateFilter, + for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, +{ + let mut names: Vec = filter + .create_filter(g) + .unwrap() + .nodes() + .iter() + .map(|n| n.name()) + .collect(); + names.sort(); + names +} + +#[test] +fn windowed_temporal_any_restricts_to_window() { + // alice scores: t1=1, t2=5, t3=10 + // window [1, 2) → only t=1 visible → score=1 only + // any == 5 in window [1,2) → false for all nodes + let g = build_temporal_graph(); + let filter = NodeFilter + .window(1, 2) + .property("score") + .temporal() + .any() + .eq(5i64); + // window [1,2) shows t=1 only → alice has score=1, not 5 + assert!(windowed_filtered_names(filter, g).is_empty()); +} + +#[test] +fn windowed_temporal_any_matches_in_window() { + // window [2, 3) → alice has score=5 (t=2), bob has score=3 (t=2) + let g = build_temporal_graph(); + let filter = NodeFilter + .window(2, 3) + .property("score") + .temporal() + .any() + .eq(5i64); + assert_eq!(windowed_filtered_names(filter, g), vec!["alice"]); +} + +// ── Layered temporal filter ─────────────────────────────────────────────── + +/// Graph where temporal "score" updates are split across two named layers. +/// +/// alice: score [1, 5, 10] at t=1,2,3 — all added in "layer_a" +/// bob: score [2, 3] at t=1,2 — all added in "layer_b" +/// carol: no score property — added in "layer_a" (makes her visible there) +/// +/// Because updates added without an explicit layer go into the static layer +/// (and are always visible regardless of the active LayeredGraph), we must use +/// an explicit layer on every `add_node` call that carries a property we want +/// to isolate. +fn build_layered_temporal_graph() -> Graph { + let g = Graph::new(); + g.add_node( + 1, + "alice", + [("score", 1i64.into_prop())], + None, + Some("layer_a"), + ) + .unwrap(); + g.add_node( + 2, + "alice", + [("score", 5i64.into_prop())], + None, + Some("layer_a"), + ) + .unwrap(); + g.add_node( + 3, + "alice", + [("score", 10i64.into_prop())], + None, + Some("layer_a"), + ) + .unwrap(); + g.add_node( + 1, + "bob", + [("score", 2i64.into_prop())], + None, + Some("layer_b"), + ) + .unwrap(); + g.add_node( + 2, + "bob", + [("score", 3i64.into_prop())], + None, + Some("layer_b"), + ) + .unwrap(); + g.add_node(1, "carol", NO_PROPS, None, Some("layer_a")) + .unwrap(); + g +} + +/// Apply a layered temporal filter directly (view is embedded in the expression). +fn layered_filtered_names(filter: F, g: Graph) -> Vec +where + F: CreateFilter, + for<'graph> F::EntityFiltered<'graph, Graph>: GraphViewOps<'graph>, +{ + let mut names: Vec = filter + .create_filter(g) + .unwrap() + .nodes() + .iter() + .map(|n| n.name()) + .collect(); + names.sort(); + names +} + +#[test] +fn layered_temporal_any_restricts_to_layer_a_updates() { + // layer_a view: alice has scores [1, 5, 10], carol has none, bob has none + // any == 5 → only alice qualifies + let g = build_layered_temporal_graph(); + let filter = NodeFilter + .layer("layer_a") + .property("score") + .temporal() + .any() + .eq(5i64); + assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); +} + +#[test] +fn layered_temporal_any_restricts_to_layer_b_updates() { + // layer_b view: bob has scores [2, 3], alice has none, carol has none + // any > 2 → bob qualifies (score=3 > 2), alice and carol do not + let g = build_layered_temporal_graph(); + let filter = NodeFilter + .layer("layer_b") + .property("score") + .temporal() + .any() + .gt(2i64); + assert_eq!(layered_filtered_names(filter, g), vec!["bob"]); +} + +#[test] +fn layered_temporal_sum_is_layer_scoped() { + // layer_a: alice sum = 1+5+10 = 16; layer_b: bob sum = 2+3 = 5 + // layer_a sum > 10 → alice (16 > 10); carol (no score) excluded + let g = build_layered_temporal_graph(); + let filter = NodeFilter + .layer("layer_a") + .property("score") + .temporal() + .sum() + .gt(10i64); + assert_eq!(layered_filtered_names(filter, g), vec!["alice"]); +} + +// ── is_true() / is_false() ─────────────────────────────────────────────── + +/// Graph with bool "active" property: +/// "on" — active = true +/// "off" — active = false +/// "na" — no active property +fn build_bool_graph() -> Graph { + let g = Graph::new(); + g.add_node(0, "on", [("active", true.into_prop())], None, None) + .unwrap(); + g.add_node(0, "off", [("active", false.into_prop())], None, None) + .unwrap(); + g.add_node(0, "na", NO_PROPS, None, None).unwrap(); + g +} + +#[test] +fn is_true_keeps_only_true_nodes() { + let g = build_bool_graph(); + let filter = NodeFilter::property("active").is_true(); + assert_eq!(filtered_names(filter, g), vec!["on"]); +} + +#[test] +fn is_false_keeps_only_false_nodes() { + let g = build_bool_graph(); + let filter = NodeFilter::property("active").is_false(); + assert_eq!(filtered_names(filter, g), vec!["off"]); +} + +#[test] +fn is_true_excludes_absent_property() { + // "na" has no "active" property — must not appear + let g = build_bool_graph(); + let filter = NodeFilter::property("active").is_true(); + let names = filtered_names(filter, g); + assert!(!names.contains(&"na".to_string())); +} + +// ── Runtime validation via prop_type() ─────────────────────────────────── + +#[test] +fn string_op_on_numeric_prop_returns_error() { + let g = build_temporal_graph(); + let filter = NodeFilter::property("score").starts_with(Prop::Str("x".into())); + let result = filter.create_filter(g); + assert!( + result.is_err(), + "expected Err for string op on numeric property" + ); +} + +#[test] +fn ordering_op_on_bool_prop_returns_error() { + let g = Graph::new(); + g.add_node(0, "n", [("flag", true.into_prop())], None, None) + .unwrap(); + // Use Prop::Bool as rhs so both sides share Output = Option + let filter = NodeFilter::property("flag").gt(Prop::Bool(false)); + let result = filter.create_filter(g); + assert!( + result.is_err(), + "expected Err for ordering op on boolean property" + ); +} diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/builders.rs b/raphtory/src/db/graph/views/filter/model/node_filter/builders.rs index 6f8f996367..7f92d5e657 100644 --- a/raphtory/src/db/graph/views/filter/model/node_filter/builders.rs +++ b/raphtory/src/db/graph/views/filter/model/node_filter/builders.rs @@ -1,20 +1,15 @@ -use crate::db::graph::views::filter::model::{ - filter::Filter, - node_filter::{NodeNameFilter, NodeTypeFilter}, - Wrap, +use crate::db::{ + api::state::ops::{Name, Type}, + graph::views::filter::model::{ + filter::Filter, + node_filter::{NodeNameFilter, NodeTypeFilter}, + Wrap, + }, }; use std::{ops::Deref, sync::Arc}; -pub trait InternalNodeIdFilterBuilder: Send + Sync + Wrap { - fn field_name(&self) -> &'static str; -} - -impl InternalNodeIdFilterBuilder for Arc { - fn field_name(&self) -> &'static str { - self.deref().field_name() - } -} - +// TODO: remove this trait (and NodeFilterOps, NodeNameFilter, NodeTypeFilter, the search executors) +// when the Tantivy search feature is removed. pub trait InternalNodeFilterBuilder: Send + Sync + Wrap { type FilterType: From; fn field_name(&self) -> &'static str; @@ -28,10 +23,7 @@ impl InternalNodeFilterBuilder for Arc { } } -#[derive(Clone, Debug)] -pub struct NodeIdFilterBuilder; - -impl Wrap for NodeIdFilterBuilder { +impl Wrap for Name { type Wrapped = T; fn wrap(&self, value: T) -> Self::Wrapped { @@ -39,25 +31,7 @@ impl Wrap for NodeIdFilterBuilder { } } -impl InternalNodeIdFilterBuilder for NodeIdFilterBuilder { - #[inline] - fn field_name(&self) -> &'static str { - "node_id" - } -} - -#[derive(Clone, Debug)] -pub struct NodeNameFilterBuilder; - -impl Wrap for NodeNameFilterBuilder { - type Wrapped = T; - - fn wrap(&self, value: T) -> Self::Wrapped { - value - } -} - -impl InternalNodeFilterBuilder for NodeNameFilterBuilder { +impl InternalNodeFilterBuilder for Name { type FilterType = NodeNameFilter; fn field_name(&self) -> &'static str { @@ -65,10 +39,7 @@ impl InternalNodeFilterBuilder for NodeNameFilterBuilder { } } -#[derive(Clone, Debug)] -pub struct NodeTypeFilterBuilder; - -impl Wrap for NodeTypeFilterBuilder { +impl Wrap for Type { type Wrapped = T; fn wrap(&self, value: T) -> Self::Wrapped { @@ -76,8 +47,9 @@ impl Wrap for NodeTypeFilterBuilder { } } -impl InternalNodeFilterBuilder for NodeTypeFilterBuilder { +impl InternalNodeFilterBuilder for Type { type FilterType = NodeTypeFilter; + fn field_name(&self) -> &'static str { "node_type" } 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 6064c5e102..fe22e04944 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 @@ -7,7 +7,7 @@ use crate::{ AndOp, MaskOp, NodeIdFilterOp, NodeNameFilterOp, NodeTypeFilterOp, NotOp, OrOp, }, - NodeOp, TypeId, + Id, Name, NodeOp, Type, TypeId, }, NodeStateValue, TypedNodeState, }, @@ -20,17 +20,15 @@ use crate::{ is_active_node_filter::IsActiveNode, latest_filter::Latest, layered_filter::Layered, - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - validate::validate, - }, + node_expr::{DegreeExpr, Metadata, Property}, + node_filter::validate::validate, node_state_filter::NodeStateBoolColOp, property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, snapshot_filter::{SnapshotAt, SnapshotLatest}, windowed_filter::Windowed, AndFilter, CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - EntityMarker, InternalPropertyFilterFactory, InternalViewWrapOps, - NodeViewFilterOps, NotFilter, OrFilter, TryAsCompositeFilter, Wrap, + CreateView, EntityMarker, InternalViewWrapOps, NodeViewFilterOps, NotFilter, + OrFilter, PropertyFilterFactory, TryAsCompositeFilter, Wrap, }, node_filtered_graph::NodeFilteredGraph, CreateFilter, @@ -39,7 +37,7 @@ use crate::{ errors::GraphError, prelude::{GraphViewOps, PropertyFilter}, }; -use raphtory_api::core::storage::timeindex::EventTime; +use raphtory_api::core::{entities::GID, storage::timeindex::EventTime, Direction}; use std::{fmt, fmt::Display, sync::Arc}; pub mod builders; @@ -55,24 +53,32 @@ impl From for EntityMarker { } } -impl NodeFilter { +pub trait NodeFilterFactory: PropertyFilterFactory + Clone { #[inline] - pub fn id() -> NodeIdFilterBuilder { - NodeIdFilterBuilder + fn id(&self) -> Id { + Id } + /// Selects the node name field for filtering. + /// + /// Returns `Name` which implements `NodeExprFilterOps` — use `.eq("Alice")`, + /// `.contains("ali")`, `.is_in([…])`, etc. directly on the returned value. #[inline] - pub fn name() -> NodeNameFilterBuilder { - NodeNameFilterBuilder + fn name(&self) -> Name { + Name } + /// Selects the node type field for filtering. + /// + /// Returns `Type` which implements `NodeExprFilterOps`. #[inline] - pub fn node_type() -> NodeTypeFilterBuilder { - NodeTypeFilterBuilder + fn node_type(&self) -> Type { + Type } /// Build a filter from a boolean column inside a TypedNodeState. - pub fn by_column<'graph, V, G, T>( + fn by_column<'graph, V, G, T>( + &self, state: &TypedNodeState<'graph, V, G, T>, col: &str, ) -> Result @@ -82,47 +88,84 @@ impl NodeFilter { { state.bool_col_filter(col) } -} -impl Wrap for NodeFilter { - type Wrapped = T; + /// Total degree expression — supports `.gt(n)`, `.lt(n)`, etc. + fn degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::BOTH, + view_expr: self.clone(), + } + } - fn wrap(&self, value: T) -> Self::Wrapped { - value + /// In-degree expression. + fn in_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::IN, + view_expr: self.clone(), + } } -} -impl InternalViewWrapOps for NodeFilter { - type Window = Windowed; + /// Out-degree expression. + #[inline] + fn out_degree(&self) -> DegreeExpr { + DegreeExpr { + dir: Direction::OUT, + view_expr: self.clone(), + } + } - fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { - Windowed::from_times(start, end, self) + fn is_active(&self) -> IsActiveNode { + IsActiveNode { + view_expr: self.clone(), + } } } -impl InternalPropertyFilterFactory for NodeFilter { - type Entity = NodeFilter; - type PropertyBuilder = PropertyFilterBuilder; - type MetadataBuilder = MetadataFilterBuilder; +impl NodeFilterFactory for NodeFilter {} + +impl TryAsCompositeFilter for NodeFilter { + 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 { + Err(GraphError::NotSupported) + } +} - fn entity(&self) -> Self::Entity { - NodeFilter +impl NodeFilter { + /// Current (latest) value of a named property — serializable. + #[inline] + pub fn property(name: impl Into) -> Property { + Property::new(name) } - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - PropertyFilterBuilder(property, self.entity()) + /// Static metadata field — serializable. + #[inline] + pub fn metadata(name: impl Into) -> Metadata { + Metadata::new(name) } +} - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - MetadataFilterBuilder(property, self.entity()) +impl Wrap for NodeFilter { + type Wrapped = T; + + fn wrap(&self, value: T) -> Self::Wrapped { + value } } -impl NodeViewFilterOps for NodeFilter { - type Output = T; +impl InternalViewWrapOps for NodeFilter { + type Window = Windowed; - fn is_active(&self) -> Self::Output { - IsActiveNode + fn build_window(self, start: EventTime, end: EventTime) -> Self::Window { + Windowed::from_times(start, end, self) } } @@ -169,18 +212,11 @@ impl CreateFilter for NodeIdFilter { validate(graph.id_type(), &self.0)?; Ok(NodeIdFilterOp::new(self.0)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for NodeIdFilter { fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Node(self.0.clone())) + Ok(CompositeNodeFilter::Id(self.0.clone())) } fn try_as_composite_edge_filter(&self) -> Result { @@ -235,18 +271,11 @@ impl CreateFilter for NodeNameFilter { ) -> Result, GraphError> { Ok(NodeNameFilterOp::new(self.0)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for NodeNameFilter { fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Node(self.0.clone())) + Ok(CompositeNodeFilter::Name(self.0.clone())) } fn try_as_composite_edge_filter(&self) -> Result { @@ -318,18 +347,11 @@ impl CreateFilter for NodeTypeFilter { .collect::>(); Ok(TypeId.mask(node_types_filter.into())) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl TryAsCompositeFilter for NodeTypeFilter { fn try_as_composite_node_filter(&self) -> Result { - Ok(CompositeNodeFilter::Node(self.0.clone())) + Ok(CompositeNodeFilter::Type(self.0.clone())) } fn try_as_composite_edge_filter(&self) -> Result { @@ -345,14 +367,16 @@ impl TryAsCompositeFilter for NodeTypeFilter { #[derive(Debug, Clone, PartialEq, Eq)] pub enum CompositeNodeFilter { - Node(Filter), + Id(Filter), + Name(Filter), + Type(Filter), Property(PropertyFilter), Windowed(Box>), Latest(Box>), SnapshotAt(Box>), SnapshotLatest(Box>), Layered(Box>), - IsActiveNode(IsActiveNode), + IsActiveNode(Box), And(Box, Box), Or(Box, Box), Not(Box), @@ -368,7 +392,9 @@ impl Display for CompositeNodeFilter { CompositeNodeFilter::SnapshotAt(filter) => write!(f, "{}", filter), CompositeNodeFilter::SnapshotLatest(filter) => write!(f, "{}", filter), CompositeNodeFilter::IsActiveNode(filter) => write!(f, "{}", filter), - CompositeNodeFilter::Node(filter) => write!(f, "{}", filter), + CompositeNodeFilter::Id(filter) => write!(f, "{}", filter), + CompositeNodeFilter::Name(filter) => write!(f, "{}", filter), + CompositeNodeFilter::Type(filter) => write!(f, "{}", filter), CompositeNodeFilter::And(left, right) => write!(f, "({} AND {})", left, right), CompositeNodeFilter::Or(left, right) => write!(f, "({} OR {})", left, right), CompositeNodeFilter::Not(filter) => write!(f, "NOT({})", filter), @@ -401,14 +427,13 @@ impl CreateFilter for CompositeNodeFilter { graph: G, ) -> Result, GraphError> { match self { - CompositeNodeFilter::Node(i) => match i.field_name.as_str() { - "node_id" => Ok(Arc::new(NodeIdFilter(i).create_node_filter(graph)?)), - "node_name" => Ok(Arc::new(NodeNameFilter(i).create_node_filter(graph)?)), - "node_type" => Ok(Arc::new(NodeTypeFilter(i).create_node_filter(graph)?)), - _ => { - unreachable!() - } - }, + CompositeNodeFilter::Id(i) => Ok(Arc::new(NodeIdFilter(i).create_node_filter(graph)?)), + CompositeNodeFilter::Name(i) => { + Ok(Arc::new(NodeNameFilter(i).create_node_filter(graph)?)) + } + CompositeNodeFilter::Type(i) => { + Ok(Arc::new(NodeTypeFilter(i).create_node_filter(graph)?)) + } CompositeNodeFilter::Property(i) => Ok(Arc::new(i.create_node_filter(graph)?)), CompositeNodeFilter::Windowed(i) => { let dyn_graph: Arc = Arc::new(graph); @@ -444,45 +469,6 @@ impl CreateFilter for CompositeNodeFilter { } } } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - match self.clone() { - CompositeNodeFilter::Node(i) => match i.field_name.as_str() { - "node_id" => Ok(Arc::new(NodeIdFilter(i).filter_graph_view(graph)?)), - "node_name" => Ok(Arc::new(NodeNameFilter(i).filter_graph_view(graph)?)), - "node_type" => Ok(Arc::new(NodeTypeFilter(i).filter_graph_view(graph)?)), - _ => { - unreachable!() - } - }, - CompositeNodeFilter::Property(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::Windowed(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::Layered(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::Latest(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::SnapshotAt(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::SnapshotLatest(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::IsActiveNode(i) => Ok(Arc::new(i.filter_graph_view(graph)?)), - CompositeNodeFilter::And(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - AndFilter { left: l, right: r }.filter_graph_view(graph)?, - )) - } - CompositeNodeFilter::Or(l, r) => { - let (l, r) = (*l, *r); - Ok(Arc::new( - OrFilter { left: l, right: r }.filter_graph_view(graph)?, - )) - } - CompositeNodeFilter::Not(f) => { - let base = *f; - Ok(Arc::new(NotFilter(base).filter_graph_view(graph)?)) - } - } - } } impl TryAsCompositeFilter for CompositeNodeFilter { diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/ops.rs b/raphtory/src/db/graph/views/filter/model/node_filter/ops.rs index 41a1b819f6..1bd1965908 100644 --- a/raphtory/src/db/graph/views/filter/model/node_filter/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/node_filter/ops.rs @@ -1,94 +1,6 @@ use crate::db::graph::views::filter::model::{ - filter::Filter, - node_filter::{ - builders::{InternalNodeFilterBuilder, InternalNodeIdFilterBuilder}, - NodeIdFilter, - }, + filter::Filter, node_filter::builders::InternalNodeFilterBuilder, }; -use raphtory_api::core::entities::GID; - -pub trait NodeIdFilterOps: InternalNodeIdFilterBuilder { - fn eq>(&self, value: T) -> Self::Wrapped { - let filter = Filter::eq_id(self.field_name(), value); - self.wrap(NodeIdFilter(filter)) - } - - fn ne>(&self, value: T) -> Self::Wrapped { - let filter = Filter::ne_id(self.field_name(), value).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn is_in(&self, values: I) -> Self::Wrapped - where - I: IntoIterator, - T: Into, - { - let filter = Filter::is_in_id(self.field_name(), values).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn is_not_in(&self, values: I) -> Self::Wrapped - where - I: IntoIterator, - T: Into, - { - let filter = Filter::is_not_in_id(self.field_name(), values).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn lt>(&self, value: V) -> Self::Wrapped { - let filter = Filter::lt(self.field_name(), value).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn le>(&self, value: V) -> Self::Wrapped { - let filter = Filter::le(self.field_name(), value).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn gt>(&self, value: V) -> Self::Wrapped { - let filter = Filter::gt(self.field_name(), value).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn ge>(&self, value: V) -> Self::Wrapped { - let filter = Filter::ge(self.field_name(), value).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn starts_with>(&self, s: S) -> Self::Wrapped { - let filter = Filter::starts_with(self.field_name(), s.into()).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn ends_with>(&self, s: S) -> Self::Wrapped { - let filter = Filter::ends_with(self.field_name(), s.into()).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn contains>(&self, s: S) -> Self::Wrapped { - let filter = Filter::contains(self.field_name(), s.into()).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn not_contains>(&self, s: S) -> Self::Wrapped { - let filter = Filter::not_contains(self.field_name(), s.into()).into(); - self.wrap(NodeIdFilter(filter)) - } - - fn fuzzy_search>( - &self, - s: S, - levenshtein_distance: usize, - prefix_match: bool, - ) -> Self::Wrapped { - let filter = - Filter::fuzzy_search(self.field_name(), s, levenshtein_distance, prefix_match).into(); - self.wrap(NodeIdFilter(filter)) - } -} - -impl NodeIdFilterOps for T {} pub trait NodeFilterOps: InternalNodeFilterBuilder { fn eq(&self, value: impl Into) -> Self::Wrapped { diff --git a/raphtory/src/db/graph/views/filter/model/node_filter/validate.rs b/raphtory/src/db/graph/views/filter/model/node_filter/validate.rs index b567c2fd3c..191b974a96 100644 --- a/raphtory/src/db/graph/views/filter/model/node_filter/validate.rs +++ b/raphtory/src/db/graph/views/filter/model/node_filter/validate.rs @@ -1,6 +1,6 @@ use crate::{ db::graph::views::filter::model::{ - filter::FilterValue, + filter::FieldFilterValue, FilterOperator::{ Contains, EndsWith, Eq, Ge, Gt, IsIn, IsNone, IsNotIn, IsSome, Le, Lt, Ne, NotContains, StartsWith, *, @@ -20,11 +20,11 @@ pub fn validate(id_dtype: Option, filter: &Filter) -> Result<(), GraphE return Ok(()); }; - fn filter_value_kind(fv: &FilterValue) -> &'static str { + fn filter_value_kind(fv: &FieldFilterValue) -> &'static str { match fv { - FilterValue::ID(GID::U64(_)) => "U64", - FilterValue::ID(GID::Str(_)) => "Str", - FilterValue::IDSet(set) => { + FieldFilterValue::ID(GID::U64(_)) => "U64", + FieldFilterValue::ID(GID::Str(_)) => "Str", + FieldFilterValue::IDSet(set) => { if set.iter().all(|g| matches!(g, GID::U64(_))) { "U64" } else if set.iter().all(|g| matches!(g, GID::Str(_))) { @@ -33,20 +33,20 @@ pub fn validate(id_dtype: Option, filter: &Filter) -> Result<(), GraphE "heterogeneous id set" } } - FilterValue::Single(_) => "Str", - FilterValue::Set(_) => "Str", + FieldFilterValue::Single(_) => "Str", + FieldFilterValue::Set(_) => "Str", } } - let value_matches_kind = |fv: &FilterValue, expect: GidType| -> bool { + let value_matches_kind = |fv: &FieldFilterValue, expect: GidType| -> bool { match (fv, expect) { - (FilterValue::ID(GID::U64(_)), U64) => true, - (FilterValue::IDSet(set), U64) => set.iter().all(|g| matches!(g, GID::U64(_))), + (FieldFilterValue::ID(GID::U64(_)), U64) => true, + (FieldFilterValue::IDSet(set), U64) => set.iter().all(|g| matches!(g, GID::U64(_))), - (FilterValue::ID(GID::Str(_)), Str) => true, - (FilterValue::IDSet(set), Str) => set.iter().all(|g| matches!(g, GID::Str(_))), - (FilterValue::Single(_), Str) => true, - (FilterValue::Set(_), Str) => true, + (FieldFilterValue::ID(GID::Str(_)), Str) => true, + (FieldFilterValue::IDSet(set), Str) => set.iter().all(|g| matches!(g, GID::Str(_))), + (FieldFilterValue::Single(_), Str) => true, + (FieldFilterValue::Set(_), Str) => true, _ => false, } @@ -89,7 +89,7 @@ pub fn validate(id_dtype: Option, filter: &Filter) -> Result<(), GraphE IsIn | IsNotIn => { if !matches!( filter.field_value, - FilterValue::IDSet(_) | FilterValue::Set(_) + FieldFilterValue::IDSet(_) | FieldFilterValue::Set(_) ) { return Err(GraphError::InvalidGqlFilter( "IN/NOT_IN on ID expects a set of IDs".into(), @@ -99,7 +99,7 @@ pub fn validate(id_dtype: Option, filter: &Filter) -> Result<(), GraphE StartsWith | EndsWith | Contains | NotContains | FuzzySearch { .. } => { if !matches!( filter.field_value, - FilterValue::ID(GID::Str(_)) | FilterValue::Single(_) + FieldFilterValue::ID(GID::Str(_)) | FieldFilterValue::Single(_) ) { return Err(GraphError::InvalidGqlFilter( "String operators on ID expect a single string ID".into(), @@ -107,7 +107,7 @@ pub fn validate(id_dtype: Option, filter: &Filter) -> Result<(), GraphE } } Lt | Le | Gt | Ge => { - if !matches!(filter.field_value, FilterValue::ID(GID::U64(_))) { + if !matches!(filter.field_value, FieldFilterValue::ID(GID::U64(_))) { return Err(GraphError::InvalidGqlFilter( "Numeric operators on ID expect a single numeric (u64) ID".into(), )); diff --git a/raphtory/src/db/graph/views/filter/model/not_filter.rs b/raphtory/src/db/graph/views/filter/model/not_filter.rs index b5608968d9..4f01eda422 100644 --- a/raphtory/src/db/graph/views/filter/model/not_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/not_filter.rs @@ -32,12 +32,12 @@ impl ComposableFilter for NotFilter {} impl CreateFilter for NotFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> - = NotFilteredGraph>> + = NotFilteredGraph> where Self: 'graph; type NodeFilter<'graph, G: GraphView + 'graph> - = NotOp>> + = NotOp> where Self: 'graph; @@ -51,8 +51,7 @@ impl CreateFilter for NotFilter { self, graph: G, ) -> Result, GraphError> { - let f = self.0.filter_graph_view(graph.clone())?; - let filter = self.0.create_filter(f)?; + let filter = self.0.create_filter(graph.clone())?; Ok(NotFilteredGraph { graph, filter }) } @@ -63,18 +62,7 @@ impl CreateFilter for NotFilter { where Self: 'graph, { - let f = self.0.filter_graph_view(graph.clone())?; - Ok(self.0.create_node_filter(f)?.not()) - } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> - where - Self: 'graph, - { - Ok(graph) + Ok(self.0.create_node_filter(graph)?.not()) } } diff --git a/raphtory/src/db/graph/views/filter/model/or_filter.rs b/raphtory/src/db/graph/views/filter/model/or_filter.rs index f4080162a8..639fd0aa63 100644 --- a/raphtory/src/db/graph/views/filter/model/or_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/or_filter.rs @@ -35,21 +35,15 @@ impl ComposableFilter for OrFilter {} impl CreateFilter for OrFilter { type EntityFiltered<'graph, G: GraphViewOps<'graph>> - = OrFilteredGraph< - G, - L::EntityFiltered<'graph, L::FilteredGraph<'graph, G>>, - R::EntityFiltered<'graph, R::FilteredGraph<'graph, G>>, - > + = OrFilteredGraph, R::EntityFiltered<'graph, G>> where Self: 'graph; type NodeFilter<'graph, G: GraphView + 'graph> - = OrOp< - L::NodeFilter<'graph, L::FilteredGraph<'graph, G>>, - R::NodeFilter<'graph, R::FilteredGraph<'graph, G>>, - > + = OrOp, R::NodeFilter<'graph, G>> where Self: 'graph; + type FilteredGraph<'graph, G> = G where @@ -60,10 +54,8 @@ impl CreateFilter for OrFilter { self, graph: G, ) -> Result, GraphError> { - let l = self.left.filter_graph_view(graph.clone())?; - let r = self.right.filter_graph_view(graph.clone())?; - let left = self.left.create_filter(l)?; - let right = self.right.create_filter(r)?; + let left = self.left.create_filter(graph.clone())?; + let right = self.right.create_filter(graph.clone())?; Ok(OrFilteredGraph { graph, left, right }) } @@ -71,22 +63,10 @@ impl CreateFilter for OrFilter { self, graph: G, ) -> Result, GraphError> { - let l = self.left.filter_graph_view(graph.clone())?; - let r = self.right.filter_graph_view(graph.clone())?; - let left = self.left.create_node_filter(l)?; - let right = self.right.create_node_filter(r)?; + let left = self.left.create_node_filter(graph.clone())?; + let right = self.right.create_node_filter(graph)?; Ok(left.or(right)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> - where - Self: 'graph, - { - Ok(graph) - } } impl TryAsCompositeFilter for OrFilter { diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/builders.rs b/raphtory/src/db/graph/views/filter/model/property_filter/builders.rs index 2cfda22420..b9d639d8ee 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/builders.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/builders.rs @@ -1,9 +1,20 @@ use crate::db::graph::views::filter::model::{ property_filter::{Op, PropertyFilter, PropertyFilterInput, PropertyRef}, - CombinedFilter, EntityMarker, InternalPropertyFilterBuilder, TemporalPropertyFilterFactory, - Wrap, + CombinedFilter, EntityMarker, Wrap, }; +pub trait InternalPropertyFilterBuilder { + type Filter; + type ExprBuilder; + type Marker; + + fn property_ref(&self) -> PropertyRef; + fn ops(&self) -> &[Op]; + fn entity(&self) -> Self::Marker; + fn filter(&self, filter: PropertyFilterInput) -> Self::Filter; + fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder; +} + #[derive(Clone)] pub struct PropertyFilterBuilder(pub String, pub M); @@ -52,13 +63,6 @@ where } } -impl TemporalPropertyFilterFactory for PropertyFilterBuilder -where - T: Into + Send + Sync + Clone + 'static, - PropertyFilter: CombinedFilter, -{ -} - #[derive(Clone)] pub struct MetadataFilterBuilder(pub String, pub M); diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/mod.rs b/raphtory/src/db/graph/views/filter/model/property_filter/mod.rs index ce064a1048..8c416696d2 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/mod.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/mod.rs @@ -12,7 +12,7 @@ use crate::{ edge_property_filtered_graph::EdgePropertyFilteredGraph, exploded_edge_property_filter::ExplodedEdgePropertyFilteredGraph, model::{ - edge_filter::CompositeEdgeFilter, ComposableFilter, + edge_filter::CompositeEdgeFilter, filter_value::FilterValue, ComposableFilter, CompositeExplodedEdgeFilter, CompositeNodeFilter, ExplodedEdgeFilter, FilterOperator, TryAsCompositeFilter, }, @@ -42,10 +42,10 @@ use raphtory_storage::graph::{ edges::{edge_ref::EdgeEntryRef, edge_storage_ops::EdgeStorageOps}, nodes::{node_ref::NodeStorageRef, node_storage_ops::NodeStorageOps}, }; -use std::{collections::HashSet, fmt, fmt::Display, sync::Arc}; +use std::{fmt, fmt::Display, sync::Arc}; pub mod builders; -mod evaluate; +pub(crate) mod evaluate; pub mod ops; mod validate; @@ -109,12 +109,8 @@ impl PropertyRef { } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum PropertyFilterValue { - None, - Single(Prop), - Set(Arc>), -} +/// Property filter value: a specialisation of `FilterValue` for stored `Prop` values. +pub type PropertyFilterValue = FilterValue; pub struct PropertyFilterInput { pub prop_ref: PropertyRef, @@ -410,13 +406,6 @@ impl CreateFilter for PropertyFilter { let prop_id = self.resolve_prop_id(graph.node_meta(), false)?; Ok(NodePropertyFilterOp::new(graph, prop_id, self)) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl CreateFilter for PropertyFilter { @@ -444,13 +433,6 @@ impl CreateFilter for PropertyFilter { ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl CreateFilter for PropertyFilter { @@ -476,13 +458,6 @@ impl CreateFilter for PropertyFilter { ) -> Result, GraphError> { Err(GraphError::NotNodeFilter) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(graph) - } } impl ComposableFilter for PropertyFilter {} diff --git a/raphtory/src/db/graph/views/filter/model/property_filter/ops.rs b/raphtory/src/db/graph/views/filter/model/property_filter/ops.rs index 0e946406c8..977d37041a 100644 --- a/raphtory/src/db/graph/views/filter/model/property_filter/ops.rs +++ b/raphtory/src/db/graph/views/filter/model/property_filter/ops.rs @@ -1,8 +1,9 @@ use crate::db::graph::views::filter::model::{ property_filter::{ - builders::PropertyExprBuilderInput, Op, PropertyFilterInput, PropertyFilterValue, + builders::{InternalPropertyFilterBuilder, PropertyExprBuilderInput}, + Op, PropertyFilterInput, PropertyFilterValue, }, - FilterOperator, InternalPropertyFilterBuilder, + FilterOperator, }; use raphtory_api::core::{entities::properties::prop::Prop, storage::arc_str::ArcStr}; use std::sync::Arc; diff --git a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs index 4a3bd9e066..f34a9c5904 100644 --- a/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/snapshot_filter.rs @@ -1,25 +1,13 @@ use crate::{ db::{ - api::view::{internal::GraphView, time::TimeOps}, - graph::views::{ - filter::{ - model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - windowed_filter::Windowed, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, - PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, - }, - CreateFilter, + api::view::internal::GraphView, + graph::views::filter::{ + model::{ + edge_filter::CompositeEdgeFilter, windowed_filter::Windowed, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, InternalViewWrapOps, + TryAsCompositeFilter, Wrap, }, - window_graph::WindowedGraph, + CreateFilter, }, }, errors::GraphError, @@ -58,32 +46,6 @@ impl InternalViewWrapOps for SnapshotAt { } } -impl InternalPropertyFilterBuilder for SnapshotAt { - type Filter = SnapshotAt; - type ExprBuilder = SnapshotAt; - 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: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl TryAsCompositeFilter for SnapshotAt { fn try_as_composite_node_filter(&self) -> Result { Ok(CompositeNodeFilter::SnapshotAt(Box::new(SnapshotAt { @@ -123,7 +85,7 @@ impl CreateFilter for SnapshotA G: GraphView + 'graph; type FilteredGraph<'graph, G> - = WindowedGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -147,13 +109,6 @@ impl CreateFilter for SnapshotA { self.inner.create_node_filter(graph) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(self.inner.filter_graph_view(graph)?.snapshot_at(self.time)) - } } impl ComposableFilter for SnapshotAt {} @@ -168,54 +123,6 @@ impl Wrap for SnapshotAt { } } -impl InternalPropertyFilterFactory for SnapshotAt { - type Entity = T::Entity; - type PropertyBuilder = SnapshotAt; - type MetadataBuilder = SnapshotAt; - - fn entity(&self) -> Self::Entity { - self.inner.entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.wrap(self.inner.metadata_builder(property)) - } -} - -impl TemporalPropertyFilterFactory for SnapshotAt {} - -impl NodeViewFilterOps for SnapshotAt { - type Output = SnapshotAt>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } -} - -impl EdgeViewFilterOps for SnapshotAt { - type Output = SnapshotAt>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } - - fn is_valid(&self) -> Self::Output { - self.wrap(self.inner.is_valid()) - } - - fn is_deleted(&self) -> Self::Output { - self.wrap(self.inner.is_deleted()) - } - - fn is_self_loop(&self) -> Self::Output { - self.wrap(self.inner.is_self_loop()) - } -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct SnapshotLatest { pub inner: M, @@ -242,32 +149,6 @@ impl InternalViewWrapOps for SnapshotLatest { } } -impl InternalPropertyFilterBuilder for SnapshotLatest { - type Filter = SnapshotLatest; - type ExprBuilder = SnapshotLatest; - 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: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl TryAsCompositeFilter for SnapshotLatest { fn try_as_composite_node_filter(&self) -> Result { Ok(CompositeNodeFilter::SnapshotLatest(Box::new( @@ -300,8 +181,9 @@ impl CreateFilter for SnapshotL = T::NodeFilter<'graph, G> where G: GraphView + 'graph; + type FilteredGraph<'graph, G> - = WindowedGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -325,13 +207,6 @@ impl CreateFilter for SnapshotL { self.inner.create_node_filter(graph) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(self.inner.filter_graph_view(graph)?.snapshot_latest()) - } } impl ComposableFilter for SnapshotLatest {} @@ -342,51 +217,3 @@ impl Wrap for SnapshotLatest { SnapshotLatest::new(value) } } - -impl InternalPropertyFilterFactory for SnapshotLatest { - type Entity = T::Entity; - type PropertyBuilder = SnapshotLatest; - type MetadataBuilder = SnapshotLatest; - - fn entity(&self) -> Self::Entity { - self.inner.entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } - - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.wrap(self.inner.metadata_builder(property)) - } -} - -impl TemporalPropertyFilterFactory for SnapshotLatest {} - -impl NodeViewFilterOps for SnapshotLatest { - type Output = SnapshotLatest>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } -} - -impl EdgeViewFilterOps for SnapshotLatest { - type Output = SnapshotLatest>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } - - fn is_valid(&self) -> Self::Output { - self.wrap(self.inner.is_valid()) - } - - fn is_deleted(&self) -> Self::Output { - self.wrap(self.inner.is_deleted()) - } - - fn is_self_loop(&self) -> Self::Output { - self.wrap(self.inner.is_self_loop()) - } -} diff --git a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs index 41f284e5fc..5ecf957a2c 100644 --- a/raphtory/src/db/graph/views/filter/model/windowed_filter.rs +++ b/raphtory/src/db/graph/views/filter/model/windowed_filter.rs @@ -4,20 +4,9 @@ use crate::{ graph::views::{ filter::{ model::{ - edge_filter::CompositeEdgeFilter, - is_active_edge_filter::IsActiveEdge, - is_active_node_filter::IsActiveNode, - is_deleted_filter::IsDeletedEdge, - is_self_loop_filter::IsSelfLoopEdge, - is_valid_filter::IsValidEdge, - node_filter::builders::{ - InternalNodeFilterBuilder, InternalNodeIdFilterBuilder, - }, - property_filter::{builders::PropertyExprBuilderInput, PropertyFilterInput}, - CombinedFilter, ComposableFilter, CompositeExplodedEdgeFilter, - CompositeNodeFilter, EdgeViewFilterOps, InternalPropertyFilterBuilder, - InternalPropertyFilterFactory, InternalViewWrapOps, NodeViewFilterOps, Op, - PropertyRef, TemporalPropertyFilterFactory, TryAsCompositeFilter, Wrap, + edge_filter::CompositeEdgeFilter, ComposableFilter, + CompositeExplodedEdgeFilter, CompositeNodeFilter, CreateView, + InternalViewWrapOps, TryAsCompositeFilter, Wrap, }, CreateFilter, }, @@ -82,46 +71,6 @@ impl InternalViewWrapOps for Windowed { } } -impl InternalNodeFilterBuilder for Windowed { - type FilterType = T::FilterType; - - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalNodeIdFilterBuilder for Windowed { - fn field_name(&self) -> &'static str { - self.inner.field_name() - } -} - -impl InternalPropertyFilterBuilder for Windowed { - type Filter = Windowed; - type ExprBuilder = Windowed; - 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: PropertyFilterInput) -> Self::Filter { - self.wrap(self.inner.filter(filter)) - } - - fn with_expr_builder(&self, builder: PropertyExprBuilderInput) -> Self::ExprBuilder { - self.wrap(self.inner.with_expr_builder(builder)) - } -} - impl TryAsCompositeFilter for Windowed { fn try_as_composite_node_filter(&self) -> Result { let filter = self.inner.try_as_composite_node_filter()?; @@ -156,7 +105,7 @@ impl CreateFilter for Windowed< G: GraphView + 'graph; type FilteredGraph<'graph, G> - = WindowedGraph> + = G where Self: 'graph, G: GraphViewOps<'graph>; @@ -180,16 +129,6 @@ impl CreateFilter for Windowed< { self.inner.create_node_filter(graph) } - - fn filter_graph_view<'graph, G: GraphView + 'graph>( - &self, - graph: G, - ) -> Result, GraphError> { - Ok(self - .inner - .filter_graph_view(graph)? - .window(self.start.t(), self.end.t())) - } } impl ComposableFilter for Windowed {} @@ -202,50 +141,14 @@ impl Wrap for Windowed { } } -impl InternalPropertyFilterFactory for Windowed { - type Entity = T::Entity; - type PropertyBuilder = Windowed; - type MetadataBuilder = Windowed; - - fn entity(&self) -> Self::Entity { - self.inner.entity() - } - - fn property_builder(&self, property: String) -> Self::PropertyBuilder { - self.wrap(self.inner.property_builder(property)) - } +impl CreateView for Windowed { + type View<'graph, G: GraphView + 'graph> = WindowedGraph>; - fn metadata_builder(&self, property: String) -> Self::MetadataBuilder { - self.wrap(self.inner.metadata_builder(property)) - } -} - -impl TemporalPropertyFilterFactory for Windowed {} - -impl NodeViewFilterOps for Windowed { - type Output = Windowed>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } -} - -impl EdgeViewFilterOps for Windowed { - type Output = Windowed>; - - fn is_active(&self) -> Self::Output { - self.wrap(self.inner.is_active()) - } - - fn is_valid(&self) -> Self::Output { - self.wrap(self.inner.is_valid()) - } - - fn is_deleted(&self) -> Self::Output { - self.wrap(self.inner.is_deleted()) - } - - fn is_self_loop(&self) -> Self::Output { - self.wrap(self.inner.is_self_loop()) + fn create_view<'graph, G: GraphView + 'graph>( + &self, + view: G, + ) -> Result, GraphError> { + let inner = self.inner.create_view(view)?; + Ok(inner.window(self.start.t(), self.end.t())) } } diff --git a/raphtory/src/python/filter/edge_filter_builders.rs b/raphtory/src/python/filter/edge_filter_builders.rs index e4184e33e6..bc0239e404 100644 --- a/raphtory/src/python/filter/edge_filter_builders.rs +++ b/raphtory/src/python/filter/edge_filter_builders.rs @@ -1,15 +1,13 @@ use crate::{ - db::graph::views::filter::model::{ - edge_filter::{EdgeEndpointWrapper, EdgeFilter}, - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - ops::{NodeFilterOps, NodeIdFilterOps}, - NodeFilter, + db::{ + api::state::ops::{Id, Name, Type}, + graph::views::filter::model::{ + edge_filter::{EdgeEndpointWrapper, EdgeFilter}, + node_filter::{ops::NodeFilterOps, NodeFilter}, + property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, + EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - EdgeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, - impl_node_text_filter_builder, python::{ filter::{ filter_expr::PyFilterExpr, @@ -35,7 +33,7 @@ use std::sync::Arc; /// Edge.src().id().starts_with("user:") #[pyclass(frozen, name = "EdgeEndpointIdFilter", module = "raphtory.filter")] #[derive(Clone)] -pub struct PyEdgeEndpointIdFilterBuilder(pub EdgeEndpointWrapper); +pub struct PyEdgeEndpointIdFilterBuilder(pub EdgeEndpointWrapper); #[pymethods] impl PyEdgeEndpointIdFilterBuilder { @@ -47,7 +45,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating equality. fn __eq__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.eq(value))) + PyFilterExpr(Arc::new(self.0.clone().eq(value))) } /// Checks whether the endpoint ID is not equal to the given value. @@ -58,7 +56,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating inequality. fn __ne__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ne(value))) + PyFilterExpr(Arc::new(self.0.clone().ne(value))) } /// Checks whether the endpoint ID is less than the given value (exclusive). @@ -69,7 +67,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating a `<` comparison. fn __lt__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.lt(value))) + PyFilterExpr(Arc::new(self.0.clone().lt(value))) } /// Checks whether the endpoint ID is less than or equal to the given value. @@ -80,7 +78,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating a `<=` comparison. fn __le__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.le(value))) + PyFilterExpr(Arc::new(self.0.clone().le(value))) } /// Checks whether the endpoint ID is greater than the given value (exclusive). @@ -91,7 +89,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating a `>` comparison. fn __gt__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.gt(value))) + PyFilterExpr(Arc::new(self.0.clone().gt(value))) } /// Checks whether the endpoint ID is greater than or equal to the given value. @@ -102,7 +100,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating a `>=` comparison. fn __ge__(&self, value: GID) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ge(value))) + PyFilterExpr(Arc::new(self.0.clone().ge(value))) } /// Checks whether the endpoint ID is contained within the specified iterable of IDs. @@ -113,7 +111,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating membership. fn is_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.is_in(values))) + PyFilterExpr(Arc::new(self.0.clone().is_in(values))) } /// Checks whether the endpoint ID is **not** contained within the specified iterable of IDs. @@ -124,7 +122,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating non-membership. fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.is_not_in(values))) + PyFilterExpr(Arc::new(self.0.clone().is_not_in(values))) } /// Checks whether the string representation of the endpoint ID starts with the given prefix. @@ -135,7 +133,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating prefix matching. fn starts_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.starts_with(value))) + PyFilterExpr(Arc::new(self.0.clone().starts_with(value))) } /// Checks whether the string representation of the endpoint ID ends with the given suffix. @@ -146,7 +144,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating suffix matching. fn ends_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ends_with(value))) + PyFilterExpr(Arc::new(self.0.clone().ends_with(value))) } /// Checks whether the string representation of the endpoint ID contains the given substring. @@ -157,7 +155,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating substring search. fn contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.contains(value))) + PyFilterExpr(Arc::new(self.0.clone().contains(value))) } /// Checks whether the string representation of the endpoint ID **does not** contain the given substring. @@ -168,7 +166,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Returns: /// filter.FilterExpr: A filter expression evaluating substring exclusion. fn not_contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.not_contains(value))) + PyFilterExpr(Arc::new(self.0.clone().not_contains(value))) } /// Performs fuzzy matching against the string representation of the endpoint ID. @@ -188,7 +186,7 @@ impl PyEdgeEndpointIdFilterBuilder { levenshtein_distance: usize, prefix_match: bool, ) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.fuzzy_search( + PyFilterExpr(Arc::new(self.0.clone().fuzzy_search( value, levenshtein_distance, prefix_match, @@ -206,7 +204,7 @@ impl PyEdgeEndpointIdFilterBuilder { /// Edge.dst().name().contains("ali") #[pyclass(frozen, name = "EdgeEndpointNameFilter", module = "raphtory.filter")] #[derive(Clone)] -pub struct PyEdgeEndpointNameFilterBuilder(pub EdgeEndpointWrapper); +pub struct PyEdgeEndpointNameFilterBuilder(pub EdgeEndpointWrapper); /// Filters an edge endpoint by its node type. /// @@ -218,10 +216,56 @@ pub struct PyEdgeEndpointNameFilterBuilder(pub EdgeEndpointWrapper); +pub struct PyEdgeEndpointTypeFilterBuilder(pub EdgeEndpointWrapper); + +macro_rules! impl_edge_text_filter_builder { + ($py_ty:ident) => { + #[pymethods] + impl $py_ty { + fn __eq__(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.eq(value))) + } + fn __ne__(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.ne(value))) + } + fn is_in(&self, values: FromIterable) -> PyFilterExpr { + let vals: Vec = values.into_iter().collect(); + PyFilterExpr(Arc::new(self.0.is_in(vals))) + } + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + let vals: Vec = values.into_iter().collect(); + PyFilterExpr(Arc::new(self.0.is_not_in(vals))) + } + fn starts_with(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.starts_with(value))) + } + fn ends_with(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.ends_with(value))) + } + fn contains(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.contains(value))) + } + fn not_contains(&self, value: String) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.not_contains(value))) + } + fn fuzzy_search( + &self, + value: String, + levenshtein_distance: usize, + prefix_match: bool, + ) -> PyFilterExpr { + PyFilterExpr(Arc::new(self.0.fuzzy_search( + value, + levenshtein_distance, + prefix_match, + ))) + } + } + }; +} -impl_node_text_filter_builder!(PyEdgeEndpointNameFilterBuilder); -impl_node_text_filter_builder!(PyEdgeEndpointTypeFilterBuilder); +impl_edge_text_filter_builder!(PyEdgeEndpointNameFilterBuilder); +impl_edge_text_filter_builder!(PyEdgeEndpointTypeFilterBuilder); /// Entry point for filtering an edge endpoint (source or destination). /// diff --git a/raphtory/src/python/filter/node_filter_builders.rs b/raphtory/src/python/filter/node_filter_builders.rs index 01613b34b0..17908e106e 100644 --- a/raphtory/src/python/filter/node_filter_builders.rs +++ b/raphtory/src/python/filter/node_filter_builders.rs @@ -1,13 +1,12 @@ use crate::{ - db::graph::views::filter::model::{ - node_filter::{ - builders::{NodeIdFilterBuilder, NodeNameFilterBuilder, NodeTypeFilterBuilder}, - ops::{NodeFilterOps, NodeIdFilterOps}, - NodeFilter, + db::{ + api::state::ops::{Id, Name, Type}, + graph::views::filter::model::{ + node_filter::{ops::NodeFilterOps, NodeFilter}, + node_state_filter::NodeStateBoolColOp, + property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, + NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, - node_state_filter::NodeStateBoolColOp, - property_filter::builders::{MetadataFilterBuilder, PropertyFilterBuilder}, - NodeViewFilterOps, PropertyFilterFactory, ViewWrapOps, }, python::{ filter::{ @@ -21,7 +20,10 @@ use crate::{ }, }; use pyo3::{pyclass, pymethods, Bound, IntoPyObject, PyResult, Python}; -use raphtory_api::core::{entities::GID, storage::timeindex::EventTime}; +use raphtory_api::core::{ + entities::GID, + storage::{arc_str::ArcStr, timeindex::EventTime}, +}; use std::sync::Arc; /// Filters nodes by their ID value. @@ -35,7 +37,7 @@ use std::sync::Arc; /// Node.id().starts_with("user:") #[pyclass(frozen, name = "NodeIdFilterBuilder", module = "raphtory.filter")] #[derive(Clone)] -pub struct PyNodeIdFilterBuilder(Arc); +pub struct PyNodeIdFilterBuilder(Id); #[pymethods] impl PyNodeIdFilterBuilder { @@ -217,7 +219,7 @@ impl PyNodeIdFilterBuilder { /// Node.name().contains("ali") #[pyclass(frozen, name = "NodeNameFilterBuilder", module = "raphtory.filter")] #[derive(Clone)] -pub struct PyNodeNameFilterBuilder(Arc); +pub struct PyNodeNameFilterBuilder; /// Filters nodes by their node type. /// @@ -228,130 +230,43 @@ pub struct PyNodeNameFilterBuilder(Arc); /// Node.node_type().is_not_in(["air_nomads"]) #[pyclass(frozen, name = "NodeTypeFilterBuilder", module = "raphtory.filter")] #[derive(Clone)] -pub struct PyNodeTypeFilterBuilder(Arc); +pub struct PyNodeTypeFilterBuilder; -#[macro_export] macro_rules! impl_node_text_filter_builder { - ($py_ty:ident) => { + ($py_ty:ident, $expr:expr) => { #[pymethods] impl $py_ty { - /// Returns a filter expression that checks whether the entity's - /// string value is equal to the specified string. - /// - /// Arguments: - /// value (str): String value to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating equality. fn __eq__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.eq(value))) + PyFilterExpr(Arc::new($expr.eq(value))) } - /// Returns a filter expression that checks whether the entity's - /// string value is not equal to the specified string. - /// - /// Arguments: - /// value (str): String value to compare against. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating inequality. fn __ne__(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ne(value))) - } - - /// Returns a filter expression that checks whether the entity's - /// string value is contained within the given iterable of strings. - /// - /// Arguments: - /// values (list[str]): Iterable of allowed string values. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating membership. - fn is_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(self.0.is_in(vals))) - } - - /// Returns a filter expression that checks whether the entity's - /// string value is **not** contained within the given iterable of strings. - /// - /// Arguments: - /// values (list[str]): Iterable of string values to exclude. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating non-membership. - fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { - let vals: Vec = values.into_iter().collect(); - PyFilterExpr(Arc::new(self.0.is_not_in(vals))) + PyFilterExpr(Arc::new($expr.ne(value))) } - /// Returns a filter expression that checks whether the entity's - /// string value starts with the specified prefix. - /// - /// Arguments: - /// value (str): Prefix to check for. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating prefix matching. fn starts_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.starts_with(value))) + PyFilterExpr(Arc::new($expr.starts_with(value))) } - /// Returns a filter expression that checks whether the entity's - /// string value ends with the specified suffix. - /// - /// Arguments: - /// value (str): Suffix to check for. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating suffix matching. fn ends_with(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.ends_with(value))) + PyFilterExpr(Arc::new($expr.ends_with(value))) } - /// Returns a filter expression that checks whether the entity's - /// string value contains the given substring. - /// - /// Arguments: - /// value (str): Substring that must appear within the value. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating substring search. fn contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.contains(value))) + PyFilterExpr(Arc::new($expr.contains(value))) } - /// Returns a filter expression that checks whether the entity's - /// string value **does not** contain the given substring. - /// - /// Arguments: - /// value (str): Substring that must not appear within the value. - /// - /// Returns: - /// filter.FilterExpr: A filter expression evaluating substring exclusion. fn not_contains(&self, value: String) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.not_contains(value))) + PyFilterExpr(Arc::new($expr.not_contains(value))) } - /// Returns a filter expression that performs fuzzy matching - /// against the entity's string value. - /// - /// Uses a specified Levenshtein distance and optional prefix matching. - /// - /// Arguments: - /// value (str): String to approximately match against. - /// levenshtein_distance (int): Maximum allowed edit distance. - /// prefix_match (bool): If true, the value must also match as a prefix. - /// - /// Returns: - /// filter.FilterExpr: A filter expression performing approximate text matching. fn fuzzy_search( &self, value: String, levenshtein_distance: usize, prefix_match: bool, ) -> PyFilterExpr { - PyFilterExpr(Arc::new(self.0.fuzzy_search( + PyFilterExpr(Arc::new($expr.fuzzy_search( value, levenshtein_distance, prefix_match, @@ -361,8 +276,35 @@ macro_rules! impl_node_text_filter_builder { }; } -impl_node_text_filter_builder!(PyNodeNameFilterBuilder); -impl_node_text_filter_builder!(PyNodeTypeFilterBuilder); +impl_node_text_filter_builder!(PyNodeNameFilterBuilder, Name); + +#[pymethods] +impl PyNodeNameFilterBuilder { + fn is_in(&self, values: FromIterable) -> PyFilterExpr { + let vals: Vec = values.into_iter().collect(); + PyFilterExpr(Arc::new(Name.is_in(vals))) + } + + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + let vals: Vec = values.into_iter().collect(); + PyFilterExpr(Arc::new(Name.is_not_in(vals))) + } +} + +impl_node_text_filter_builder!(PyNodeTypeFilterBuilder, Type); + +#[pymethods] +impl PyNodeTypeFilterBuilder { + fn is_in(&self, values: FromIterable) -> PyFilterExpr { + let vals: Vec = values.into_iter().map(ArcStr::from).collect(); + PyFilterExpr(Arc::new(Type.is_in(vals))) + } + + fn is_not_in(&self, values: FromIterable) -> PyFilterExpr { + let vals: Vec = values.into_iter().map(ArcStr::from).collect(); + PyFilterExpr(Arc::new(Type.is_not_in(vals))) + } +} /// Constructs node filter expressions. /// @@ -382,7 +324,7 @@ impl PyNodeFilter { /// filter.NodeIdFilterBuilder: #[staticmethod] fn id() -> PyNodeIdFilterBuilder { - PyNodeIdFilterBuilder(Arc::new(NodeFilter::id())) + PyNodeIdFilterBuilder(NodeFilter::id()) } /// Selects the node name field for filtering. @@ -391,7 +333,7 @@ impl PyNodeFilter { /// filter.NodeNameFilterBuilder: #[staticmethod] fn name() -> PyNodeNameFilterBuilder { - PyNodeNameFilterBuilder(Arc::new(NodeFilter::name())) + PyNodeNameFilterBuilder } /// Selects the node type field for filtering. @@ -400,7 +342,7 @@ impl PyNodeFilter { /// filter.NodeTypeFilterBuilder: #[staticmethod] fn node_type() -> PyNodeTypeFilterBuilder { - PyNodeTypeFilterBuilder(Arc::new(NodeFilter::node_type())) + PyNodeTypeFilterBuilder } /// Filters a node property by name. diff --git a/raphtory/src/search/node_filter_executor.rs b/raphtory/src/search/node_filter_executor.rs index cf0be4d926..347f018b1a 100644 --- a/raphtory/src/search/node_filter_executor.rs +++ b/raphtory/src/search/node_filter_executor.rs @@ -5,7 +5,6 @@ use crate::{ node::NodeView, views::filter::{ model::{ - filter::Filter, node_filter::{CompositeNodeFilter, NodeFilter}, property_filter::PropertyRef, }, @@ -212,27 +211,23 @@ impl<'a> NodeFilterExecutor<'a> { fn filter_node_index( &self, graph: &G, - filter: &Filter, + composite: CompositeNodeFilter, limit: usize, offset: usize, ) -> Result>, GraphError> { + let filter = match &composite { + CompositeNodeFilter::Id(f) + | CompositeNodeFilter::Name(f) + | CompositeNodeFilter::Type(f) => f, + _ => unreachable!(), + }; let (node_index, query) = self.query_builder.build_node_query(filter)?; let reader = get_reader(&node_index.entity_index.index)?; let results = match query { - Some(query) => self.execute_filter_query( - CompositeNodeFilter::Node(filter.clone()), - graph, - query, - &reader, - limit, - offset, - )?, - None => fallback_filter_nodes( - graph, - &CompositeNodeFilter::Node(filter.clone()), - limit, - offset, - )?, + Some(query) => { + self.execute_filter_query(composite.clone(), graph, query, &reader, limit, offset)? + } + None => fallback_filter_nodes(graph, &composite, limit, offset)?, }; Ok(results) @@ -297,8 +292,14 @@ impl<'a> NodeFilterExecutor<'a> { .map(|x| NodeView::new_internal(graph.clone(), x.node)) .collect()) } - CompositeNodeFilter::Node(filter) => { - self.filter_node_index(graph, filter, limit, offset) + CompositeNodeFilter::Id(f) => { + self.filter_node_index(graph, CompositeNodeFilter::Id(f.clone()), limit, offset) + } + CompositeNodeFilter::Name(f) => { + self.filter_node_index(graph, CompositeNodeFilter::Name(f.clone()), limit, offset) + } + CompositeNodeFilter::Type(f) => { + self.filter_node_index(graph, CompositeNodeFilter::Type(f.clone()), limit, offset) } CompositeNodeFilter::IsActiveNode(filter) => { fallback_filter_nodes(graph, filter, limit, offset) diff --git a/raphtory/src/search/query_builder.rs b/raphtory/src/search/query_builder.rs index fde7ab3c0d..8a08539925 100644 --- a/raphtory/src/search/query_builder.rs +++ b/raphtory/src/search/query_builder.rs @@ -1,6 +1,6 @@ use crate::{ db::graph::views::filter::model::{ - filter::{Filter, FilterValue}, + filter::{FieldFilterValue, Filter}, filter_operator::FilterOperator, property_filter::PropertyFilterValue, }, @@ -43,7 +43,7 @@ impl<'a> QueryBuilder<'a> { let prop_name = filter.prop_ref.name(); let prop_value = &filter.prop_value; let query: Option> = match prop_value { - PropertyFilterValue::Single(prop_value) => match &filter.operator { + PropertyFieldFilterValue::Single(prop_value) => match &filter.operator { FilterOperator::Eq => { let term = create_property_exact_tantivy_term(property_index, prop_name, prop_value)?; @@ -105,7 +105,7 @@ impl<'a> QueryBuilder<'a> { } => None, _ => unreachable!(), }, - PropertyFilterValue::Set(prop_values) => { + PropertyFieldFilterValue::Set(prop_values) => { let terms: Result, GraphError> = prop_values .deref() .iter() @@ -120,7 +120,7 @@ impl<'a> QueryBuilder<'a> { _ => unreachable!(), } } - PropertyFilterValue::None => match &filter.operator { + PropertyFieldFilterValue::None => match &filter.operator { FilterOperator::IsSome => Some(Box::new(AllQuery)), FilterOperator::IsNone => None, _ => unreachable!(), @@ -140,7 +140,7 @@ impl<'a> QueryBuilder<'a> { let operator = &filter.operator; let query = match filter_value { - FilterValue::Single(node_value) => match operator { + FieldFilterValue::Single(node_value) => match operator { FilterOperator::Eq => { let term = create_node_exact_tantivy_term(node_index, field_name, node_value)?; create_eq_query(term) @@ -171,7 +171,7 @@ impl<'a> QueryBuilder<'a> { } => None, _ => unreachable!(), }, - FilterValue::Set(node_values) => { + FieldFilterValue::Set(node_values) => { let terms: Result, GraphError> = node_values .deref() .iter() @@ -205,7 +205,7 @@ impl<'a> QueryBuilder<'a> { let operator = &filter.operator; let query = match filter_value { - FilterValue::Single(node_value) => match operator { + FieldFilterValue::Single(node_value) => match operator { FilterOperator::Eq => { let term = create_edge_exact_tantivy_term(edge_index, field_name, node_value)?; create_eq_query(term) @@ -236,7 +236,7 @@ impl<'a> QueryBuilder<'a> { } => None, _ => unreachable!(), }, - FilterValue::Set(edge_values) => { + FieldFilterValue::Set(edge_values) => { let terms: Result, GraphError> = edge_values .deref() .iter() diff --git a/raphtory/src/search/searcher.rs b/raphtory/src/search/searcher.rs index cda7379fd9..e9265e461c 100644 --- a/raphtory/src/search/searcher.rs +++ b/raphtory/src/search/searcher.rs @@ -81,7 +81,7 @@ impl<'a> Searcher<'a> { #[cfg(test)] mod search_tests { use super::*; - use crate::{db::graph::views::filter::model::node_filter::ops::NodeFilterOps, prelude::*}; + use crate::prelude::*; use raphtory_api::core::utils::logging::global_info_logger; use std::time::SystemTime; use tracing::info; @@ -92,8 +92,7 @@ mod search_tests { db::{ api::view::SearchableGraphOps, graph::views::filter::model::{ - node_filter::{ops::NodeFilterOps, NodeFilter}, - property_filter::ops::PropertyFilterOps, + node_filter::NodeFilter, property_filter::ops::PropertyFilterOps, PropertyFilterFactory, TryAsCompositeFilter, }, }, @@ -181,9 +180,8 @@ mod search_tests { db::{ api::view::SearchableGraphOps, graph::views::filter::model::{ - edge_filter::EdgeFilter, node_filter::ops::NodeFilterOps, - property_filter::ops::PropertyFilterOps, PropertyFilterFactory, - TryAsCompositeFilter, + edge_filter::EdgeFilter, property_filter::ops::PropertyFilterOps, + PropertyFilterFactory, TryAsCompositeFilter, }, }, prelude::{AdditionOps, EdgeViewOps, Graph, IndexMutationOps, NodeViewOps, NO_PROPS},